>웹 프론트엔드 >JS 튜토리얼 >캔버스 재미 계속하기: 막대 차트 플러그인 구축, 2부

캔버스 재미 계속하기: 막대 차트 플러그인 구축, 2부

王林
王林원래의
2023-08-30 08:45:10935검색

2부로 구성된 이 시리즈에서는 다목적 캔버스 요소와 강력한 jQuery 라이브러리를 결합하여 막대 차트 플러그인을 만들겠습니다. 2부에서는 이를 jQuery 플러그인으로 변환한 다음 눈요기 및 기타 기능을 추가하겠습니다.

Fun with Canvas 두 부분으로 구성된 시리즈를 마무리하면서 오늘은 막대 차트 플러그인을 만들 예정입니다. 이 플러그인은 일반적인 플러그인이 아닙니다. 우리는 매우 강력한 플러그인을 만들기 위해 캔버스 요소에 대한 jQuery의 사랑을 보여줄 것입니다.

첫 번째 부분에서는 플러그인의 로직을 독립형 스크립트로 구현하는 데만 중점을 둡니다. 첫 번째 부분이 끝나면 막대 차트는 다음과 같습니다.

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

1부 끝의 결과

이 마지막 부분에서는 코드를 변환하고 이를 적절한 jQuery 플러그인으로 만들고, 몇 가지 시각적 세부 정보를 추가하고 마지막으로 몇 가지 추가 기능을 포함시키는 작업을 할 것입니다. 최종적으로 출력은 다음과 같습니다.

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

완제품

다들 몸 풀고 계시나요? 시작하자!


플러그인 절차

코드를 플러그인으로 변환하기 전에 먼저 플러그인 생성 절차를 이해해야 합니다.


이름 플러그인

플러그인 이름을 선택하는 것부터 시작합니다. 저는 barGraph를 선택하고 JavaScript 파일 이름을 jquery.barGraph.js로 바꿨습니다. 이제 이전 기사의 모든 코드를 다음 코드 조각에 포함합니다.

으아아아

Settings에는 플러그인에 전달된 모든 선택적 매개변수가 포함되어 있습니다.


$ 기호 문제 해결

jQuery 플러그인 작성에서는 다른 Javascript 라이브러리와의 충돌을 최소화하기 위해 코드의 $ 별칭 대신 jQuery 사용을 고려하는 것이 일반적입니다. 이 모든 문제를 겪는 대신 jQuery 문서에 언급된 대로 사용자 정의 별칭을 사용할 수 있습니다. 우리는 다음과 같이 자체 실행 익명 함수에 모든 플러그인 코드를 래핑합니다.

으아아아

기본적으로 모든 코드를 함수로 래핑하고 그 안에 jQuery를 전달합니다. 이제 다른 JavaScript 라이브러리와 잠재적으로 충돌할 염려 없이 코드에서 $ 별칭을 자유롭게 사용할 수 있습니다.


기본값

플러그인을 디자인할 때 사용자에게 합리적인 수의 설정을 노출하는 동시에 사용자가 옵션을 전달하지 않고 플러그인을 사용하는 경우 합리적인 기본 옵션을 사용하는 것이 좋습니다. 이를 염두에 두고 이 시리즈의 이전 기사에서 언급한 각 그래픽 옵션 변수를 사용자가 변경할 수 있도록 허용하겠습니다. 이를 수행하는 것은 쉽습니다. 각 변수를 객체의 속성으로 정의한 다음 해당 변수에 액세스하면 됩니다.

으아아아

결국에는 기본 옵션을 전달된 옵션과 병합하고 전달된 옵션의 우선순위를 지정해야 합니다. 이 줄은 이 문제를 처리합니다.

으아아아

필요한 경우 변수 이름을 변경하는 것을 잊지 마세요. 예를 들어 -

으아아아

...다음으로 변경됨:

으아아아

리팩터링

여기서 플러그인이 완성됩니다. 이전 구현에서는 페이지에서 단일 그래픽만 생성할 수 있었으며, 페이지에서 여러 그래픽을 생성할 수 있는 기능이 이 기능을 위한 플러그인을 만든 주된 이유였습니다. 또한 사용자가 생성하려는 각 차트에 대해 캔버스 요소를 생성할 필요가 없는지 확인해야 합니다. 이를 염두에 두고 필요에 따라 캔버스 요소를 동적으로 생성하겠습니다. 계속해보자. 코드 관련 부분의 이전 버전과 최신 버전을 살펴보겠습니다.


전화 플러그인

시작하기 전에, 플러그인이 어떻게 호출되는지 알려드리고 싶습니다.

으아아아

정말 간단해요. years는 우리의 모든 값을 담고 있는 테이블의 ID입니다. 필요에 따라 옵션을 전달합니다.


데이터 소스 가져오기

먼저 차트의 데이터 소스를 참조해야 합니다. 이제 소스 요소에 액세스하여 해당 ID를 얻습니다. 이전에 선언한 그래픽 변수 세트에 다음 줄을 추가합니다.

으아아아

새 변수를 정의하고 전달된 요소의 ID 속성 값을 할당합니다. 코드에서 this는 현재 선택된 DOM 요소를 나타냅니다. 이 예에서는 ID가 years인 테이블을 나타냅니다.

이전 구현에서는 데이터 소스의 ID가 하드 코딩되었습니다. 이제 이전에 추출한 ID 속성으로 바꿉니다. grabValues 함수의 초기 버전은 다음과 같습니다.

으아아아

업데이트:

function grabValues ()
	 {
     	// Access the required table cell, extract and add its value to the values array.
	 	$("#"+dataSource+" tr td:nth-child(2)").each(function(){
		 gValues.push($(this).text());
	 	 });
	 
		 // Access the required table cell, extract and add its value to the xLabels array.
		 $("#"+dataSource+" tr td:nth-child(1)").each(function(){
	 	xLabels.push($(this).text());
	 	 });
	 }

注入 Canvas 元素

function initCanvas ()
	 {
		 $("#"+dataSource).after("<canvas id=\"bargraph-"+dataSource+"\" class=\"barGraph\"> </canvas>");
		 
         // Try to access the canvas element 
     	cv = $("#bargraph-"+dataSource).get(0);
        
	 	if (!cv.getContext) 
	 	{ return; }
	 
     	// Try to get a 2D context for the canvas and throw an error if unable to
     	ctx = cv.getContext('2d');
	 	if (!ctx) 
	 	{ return; }
	 }

我们创建一个canvas元素并将其注入到表格之后的DOM中,该表格充当数据源。 jQuery 的 after 函数在这里非常方便。还应用了 barGraph 的类属性和 barGraph-dataSourceID 格式的 ID 属性,以使用户能够根据需要将它们全部设置为组或单独设置样式。 p>


循环传递的元素

有两种方法可以实际调用此插件。您可以单独创建每个图表并仅传入一个数据源,也可以传入多个数据源。在后一种情况下,我们当前的构造将遇到错误并退出。为了纠正这个问题,我们使用 each 构造来迭代传递的元素集。

(function($){
	$.fn.barGraph = function(settings) {
	
	// Option variables
	var defaults = {  
	         // options here
           };  
		   
	// Merge the passed parameters with the defaults	   
    var option = $.extend(defaults, settings);  
	
	// Cycle through each passed object
	this.each(function() { 
	
	// Implementation code here
	});
              
	// Returns the jQuery object to allow for chainability.
	return this;
	}
})(jQuery);

我们在获取并合并 this.each 构造中的设置后封装了所有代码。我们还确保最后返回 jQuery 对象以实现可链接性。

至此,我们的重构就完成了。我们应该能够调用我们的插件并根据需要创建尽可能多的图表。


添加养眼效果

现在我们的转换已经完成,我们可以努力使其视觉效果更好。我们将在这里做很多事情。我们将分别研究它们。


主题

旧版本使用温和的灰色来绘制图表。我们现在将为酒吧实施主题机制。这本身由一系列步骤组成。

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

海洋:默认主题 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

树叶 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

樱花 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

频谱

将其添加到选项

var defaults = {  
             // Other defaults here 
	 	 	 theme: "Ocean",
           };

我们在默认设置中添加了一个主题选项,使用户能够将主题更改为四个可用预设中的任何一个。

设置当前选择的主题

function grabValues ()
	 {
	 	// Previous code
		 
		switch(option.theme)
		{
			case 'Ocean':
			gTheme = thBlue;
			break;
			case 'Foliage':
			gTheme = thGreen;
			break;
			case 'Cherry Blossom':
			gTheme = thPink;
			break;
			case 'Spectrum':
			gTheme = thAssorted;
			break;
		} 
	 }

一个简单的switch构造查看option.theme设置并将gTheme变量指向必要的颜色数组。我们对主题使用描述性名称,而不是通用名称。

定义颜色数组

// Themes
	var thPink = ['#FFCCCC','#FFCCCC','#FFC0C0','#FFB5B5','#FFADAD','#FFA4A4','#FF9A9A','#FF8989','#FF6D6D'];
	var thBlue = ['#ACE0FF','#9CDAFF','#90D6FF','#86D2FF','#7FCFFF','#79CDFF','#72CAFF','#6CC8FF','#57C0FF'];
	var thGreen = ['#D1FFA6','#C6FF91','#C0FF86','#BCFF7D','#B6FF72','#B2FF6B','#AAFE5D','#A5FF51','#9FFF46'];
	var thAssorted = ['#FF93C2','#FF93F6','#E193FF','#B893FF','#93A0FF','#93D7FF','#93F6FF','#ABFF93','#FF9B93'];

然后我们定义许多数组,每个数组保存一系列特定颜色的色调。它们从较浅的色调开始并不断增加。稍后我们将循环遍历这些数组。添加主题就像添加您需要的特定颜色的数组一样简单,然后修改之前的开关以反映更改。

辅助函数

function getColour (param)
      {
         return Math.ceil(Math.abs(((gValues.length/2) -param)));
	  }

这是一个很小的函数,可以让我们实现类似渐变的效果并将其应用于图表。本质上,我们计算要渲染的值数量的一半与传递的参数(即数组中当前所选项目的索引)之间的绝对差。这样,我们就能够创建平滑的渐变。由于我们只在每个颜色数组中定义了九种颜色,因此我们的图表仅限于十八个值。扩展这个数字应该是相当微不足道的。

设置fillStyle

function drawGraph ()
	 {
	    for(index=0; index<gValues.length; index++)
	      {
		    ctx.save();
			ctx.fillStyle = gTheme[getColour(index)];
	        ctx.fillRect( x(index), y(gValues[index]), width(), height(gValues[index]));  
		    ctx.restore();
	      }
	 }

这是我们实际为图表设置主题的地方。我们没有为 fillStyle 属性设置静态值,而是使用 getColour 函数来检索当前所选主题数组中元素的必要索引。


不透明度

接下来,我们将让用户能够控制所绘制条形的不透明度。设置过程分为两步。

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

不透明 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

值为 0.8

将其添加到选项

var defaults = {  
            // Other defaults here 
	 	 	 barOpacity : 0.8,
           };

我们在默认值中添加了一个 barOpacity 选项,使用户能够将图形的不透明度更改为 0 到 1 之间的值,其中 0 是完全透明,1 是完全不透明。

设置globalAlpha

function drawGraph ()
	 {
	    for(index=0; index<gValues.length; index++)
	      {
		    ctx.save();
			ctx.fillStyle = gTheme[getColour(index)];
            ctx.globalAlpha = option.barOpacity;
	        ctx.fillRect( x(index), y(gValues[index]), width(), height(gValues[index]));  
		    ctx.restore();
	      }
	 }

globalAlpha 属性控制渲染元素的不透明度或透明度。我们将此属性的值设置为传递的值或默认值以增加一点透明度。作为合理的默认值,我们使用值 0.8 使其稍微透明。


网格

网格对于处理图表中呈现的数据非常有用。虽然我最初想要一个合适的网格,但后来我选择了一系列与 Y 轴标签对齐的水平线,并完全抛弃了垂直线,因为它们只是妨碍了数据。解决了这个问题,让我们来实现一种渲染它的方法。

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

禁用网格 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

启用网格

使用路径和lineTo方法创建线条似乎是绘制图形最明显的解决方案,但我碰巧遇到了一个渲染错误,这使得这种方法不适合。因此,我也坚持使用 fillRect 方法来创建这些线。这是完整的函数。

function drawGrid ()
      {
		  for(index=0; index<option.numYlabels; index++)
	      {
		   ctx.fillStyle = "#AAA";
		   ctx.fillRect( option.xOffset, y(yLabels[index])+3, gWidth, 1);
		  }
      }

这与绘制 Y 轴标签非常相似,只不过我们不是渲染标签,而是绘制一条横跨图形宽度、宽度为 1 px 的水平线。 y 函数帮助我们定位。

将其添加到选项

var defaults = {  
             // Other defaults here 
	 	 	 disableGrid : false,
           };

我们在默认值中添加了一个 disableGrid 选项,使用户能够控制是否渲染网格。默认情况下,它是渲染的。

    // Function calls
    	if(!option.disableGrid) { drawGrid(); }

我们只是检查用户是否希望渲染网格并进行相应操作。


大纲

现在条形图都已着色,在较浅的背景下缺乏强调。为了纠正这个问题,我们需要 1px 的描边。有两种方法可以做到这一点。第一种也是最简单的方法是在 drawGraph 方法中添加一个 strokeRect 方法;或者,我们可以使用 lineTo 方法来快速绘制矩形。我选择了前一条路线,因为像之前一样,lineTo 方法向我抛出了一些奇怪的渲染错误。

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

没有抚摸 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

抚摸

将其添加到选项

首先,我们将其添加到defaults对象中,以便用户控制是否应用它。

var defaults = {  
             // Other defaults here 
	 	 	 showOutline : true,
           };
function drawGraph ()
	 {
	       // Previous code
			if (option.showOutline)
			{
			ctx.fillStyle = "#000";
			ctx.strokeRect( x(index), y(gValues[index]), width(), height(gValues[index]));  
			}
			// Rest of the code
	      }
	 }

我们检查用户是否想要渲染轮廓,如果是,我们继续。这与渲染实际条形几乎相同,只是我们使用 tripleRect 方法而不是使用 fillRect 方法。


阴影

在原始实现中,画布元素本身和条形的实际渲染空间之间没有区别。我们现在就纠正这个问题。

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

无底纹 继续 Canvas 的乐趣:构建条形图插件,第 2 部分

有底纹

function shadeGraphArea ()
      {
	    ctx.fillStyle = "#F2F2F2";
	    ctx.fillRect(option.xOffset, 0, gWidth-option.xOffset, gHeight); 
      }

这是一个很小的函数,可以遮蔽所需区域。我们覆盖画布元素减去两个轴标签覆盖的区域。前两个参数指向起点的x和y坐标,后两个参数指向所需的宽度和高度。从 option.offset 开始,我们消除了 Y 轴标签覆盖的区域,并通过将高度限制为 gHeight,我们消除了 X 轴标签。


添加功能

现在我们的图表看起来足够漂亮了,我们可以集中精力向我们的插件添加一些新功能。我们将分别讨论每一个。

考虑这张著名的 8K 峰值图。

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

当最高值足够高,并且大多数值落在最大值的 10% 以内时,图表就不再有用。我们有两种方法来纠正这个问题。


显示值

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

我们将首先从更简单的解决方案开始。通过将各个图表的值呈现在顶部,实际上解决了问题,因为可以轻松地区分各个值。下面是它的实现方式。

var defaults = {  
             // Other defaults here 
	 	 	 showValue: true,
           };

首先,我们向 defaults 对象添加一个条目,以使用户能够随意打开和关闭它。

    // Function calls
	if(option.showValue) { drawValue(); }

我们检查用户是否希望显示该值并进行相应处理。

function drawValue ()
      {
		  for(index=0; index<gValues.length; index++)
	      {
		      ctx.save();
			  ctx.fillStyle= "#000";
			  ctx.font = "10px 'arial'";
			  var valAsString = gValues[index].toString();
		      var valX = (option.barWidth/2)-(valAsString.length*3);
		      ctx.fillText(gValues[index], x(index)+valX,  y(gValues[index])-4);
			  ctx.restore();
		  }
      }

我们迭代gValues数组并单独渲染每个值。涉及 valAsStringvalX 的计算只不过是帮助我们正确缩进的微小计算,因此它看起来并不不合适。


规模

继续 Canvas 的乐趣:构建条形图插件,第 2 部分

这是两个解决方案中更难的一个。在此方法中,我们不是从 0 开始 Y 轴标签,而是从更接近最小值的位置开始。我们边走边解释。请注意,在上面的示例中,后续值相对于最大值之间的差异非常微不足道,并且没有显示出其有效性。其他数据集应该更容易解析结果。

将其添加到选项

var defaults = {  
             // Other defaults here 
	 	 	 scale: false
           };

更新比例函数

由于scale函数是渲染过程中不可或缺的一部分,因此我们需要更新它以允许缩放功能。我们像这样更新它:

function scale (param)
      {
	   return ((option.scale) ? Math.round(((param-minVal)/(maxVal-minVal))*gHeight) : Math.round((param/maxVal)*gHeight));
      }

我知道这看起来有点复杂,但它看起来只是因为使用了三元条件运算符。本质上,我们检查 option.scale 的值,如果它为 false,则执行旧代码。如果为真,我们现在不会将值标准化为数组中最大值的函数,而是将其标准化为最大值和最小值之差的函数。这让我们想到:

更新maxValues函数

我们现在需要找出最大值和最小值,而不是之前只能找出最大值。函数更新为:

function minmaxValues (arr)
     {
		maxVal=0;
		
	    for(i=0; i<arr.length; i++)
	    {
		 if (maxVal<parseInt(arr[i]))
		 {
		 maxVal=parseInt(arr[i]);
	     } 
	    }
		minVal=maxVal;
		for(i=0; i<arr.length; i++)
	    {
		 if (minVal>parseInt(arr[i]))
		 {
		 minVal=parseInt(arr[i]);
	     }  
		}
	   maxVal*= 1.1;
       minVal = minVal - Math.round((maxVal/10));
	 }

我确信您可以在一个循环中完成相同的任务,而无需使用像我一样多的代码行,但当时我感觉特别没有创造力,所以请耐心等待。完成计算手续后,我们将 maxVal 变量增加 5%,将 minVal 变量增加 5%,减去 minVal 的 5% >maxVal 的 值。这是为了确保条形不会每次都接触顶部,并且每个 Y 轴标签之间的差异是均匀的。

更新drawYlabels函数

完成所有基础工作后,我们现在继续更新 Y 轴标签渲染例程以反映缩放。

function drawYlabels()
      {
		 ctx.save(); 
	     for(index=0; index<option.numYlabels; index++)
	      {
			  if (!option.scale)
			  {
		  		 yLabels.push(Math.round(maxVal/option.numYlabels*(index+1)));
			  }
			  else
			  {
				  var val= minVal+Math.ceil(((maxVal-minVal)/option.numYlabels)*(index+1));
		  		  yLabels.push(Math.ceil(val));  
			  }
		   ctx.fillStyle = option.labelColour;
		   var valAsString = yLabels[index].toString();
		   var lblX = option.xOffset - (valAsString.length*7);
		   ctx.fillText(yLabels[index], lblX, y(yLabels[index])+10);
	      }
		   if (!option.scale)
		   {
	        	ctx.fillText("0", option.xOffset -7, gHeight+7);
		   }
		  else
		  {
		    var valAsString = minVal.toString();
		    var lblX = option.xOffset - (valAsString.length*7);
		    ctx.fillText(minVal, lblX, gHeight+7);  
		  }
		  ctx.restore();
      }

如果你问我的话,更新内容相当丰富!功能的核心保持不变。我们只是检查用户是否启用了扩展并根据需要分支代码。如果启用,我们会更改 Y 标签的分配方式,以确保它们遵循新算法。现在,我们不再将最大值划分为 n 个均匀间隔的数字,而是计算最大值和最小值之间的差值,将其划分为均匀间隔的数字,并将其添加到最小值以构建 Y 轴标签数组。之后,我们照常进行,单独渲染每个标签。由于我们手动渲染了最底部的 0,因此我们必须检查是否启用了缩放,然后在其位置渲染最小值。不要介意每个传递参数的小数字添加;只是为了确保图表的每个元素都按预期排列。


动态调整大小

在我们之前的实现中,我们对图表的维度进行了硬编码,当值的数量发生变化时,这会带来很大的困难。我们现在要纠正这个问题。

将其添加到选项

var defaults = {  
            // Other defaults here 
	 	 	 cvHeight: 250, //In px 
           };

我们让用户单独设置canvas元素的高度。所有其他值都是动态计算的并根据需要应用。

更新initCanvas函数

initCanvas 函数处理所有画布初始化,因此需要更新以实现新功能。

function initCanvas ()
	 {
		 $("#"+dataSource).after("<canvas id=\"bargraph-"+dataSource+"\" class=\"barGraph\"> </canvas>");
		 
	 	// Try to access the canvas element 
     	cv = $("#bargraph-"+dataSource).get(0);
	 	cv.width=gValues.length*(option.barSpacing+option.barWidth)+option.xOffset+option.barSpacing;
		cv.height=option.cvHeight;
		gWidth=cv.width;
		gHeight=option.cvHeight-20;
	 
	 	if (!cv.getContext) 
	 	{ return; }
	 
     	// Try to get a 2D context for the canvas and throw an error if unable to
     	ctx = cv.getContext('2d');
	 	if (!ctx) 
	 	{ return; }
	 }

注入canvas元素后,我们获得了对所创建元素的引用。画布元素的宽度计算为数组中元素数量的函数 - gValues ,每个条之间的空间 - option.barSpacing ,每个条本身的宽度- option.barWidth 和最后option.xOffset。图表的宽度根据每个参数动态变化。高度是用户可修改的,默认为 220 像素,栏本身的渲染区域为 220 像素。 20px 分配给 X 轴标签。


隐藏来源

创建图表后,用户可能希望隐藏源表,这是有道理的。考虑到这一点,我们让用户决定是否删除该表。

var defaults = {  
            // Other defaults here 
			 hideDataSource: true,
           };
	if (option.hideDataSource) { $("#"+dataSource).remove();}

我们检查用户是否想要隐藏表格,如果是,我们使用 jQuery 的 remove 方法将其从 DOM 中完全删除。


优化我们的代码

现在所有的艰苦工作都已经完成,我们可以回顾一下如何优化我们的代码。由于该代码完全是为了教学目的而编写的,因此大部分工作都被封装为单独的函数,而且它们比需要的要冗长得多。

如果您确实想要尽可能精简的代码,我们的整个插件(不包括初始化和计算)可以在两个循环内重写。一个循环遍历 gValues 数组来绘制条形本身和 X 轴标签;第二个循环从 0 迭代到 numYlabels 以渲染网格和 Y 轴标签。代码看起来会更加混乱,但是,它应该会导致代码库明显更小。


摘要

就是这样,伙计们!我们完全从头开始创建了一个高级插件。我们研究了本系列中的许多主题,包括:

  • 查看画布元素的渲染方案。
  • canvas 元素的一些渲染方法。
  • 标准化值使我们能够将其表达为另一个值的函数。
  • 使用 jQuery 的一些有用的数据提取技术。
  • 渲染图表的核心逻辑。
  • 将我们的脚本转换为成熟的 jQuery 插件。
  • 如何增强视觉效果并进一步扩展其功能。

我希望您在读这篇文章时和我在写它时一样享受乐趣。这是一个 270 多行的作品,我确信我遗漏了一些东西。欢迎点击评论并询问我。或者批评我。或者夸奖我。你知道,这是你的决定!快乐编码!

위 내용은 캔버스 재미 계속하기: 막대 차트 플러그인 구축, 2부의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.