使用IRONBARCODE

如何创建一个Blazor二维码扫描器

发布 2024年二月18日
分享:

介绍

本文探讨了快速反应代码扫描仪的集成问题(QR 码扫描器)在 Blazor 应用程序中使用 IronQR for .NET 库。 QR 码是一种二维条形码,与普通的一维条形码相比,它能存储更多的数据。

Blazor 是微软的一个框架,允许开发人员制作单页应用程序(使用 Blazor WebAssembly 应用程序)或使用 C# 来构建交互式网络界面、(本指南将重点介绍 Blazor 服务器).

IronQR 与 Blazor Server 在二维码扫描方面的集成是一种战略性组合,充分利用了两种技术的优势。 通过将 IronQR 与 Blazor 应用程序集成,您可以高效地处理二维码的生成和扫描。 二维码阅读器的这一功能在库存管理、票务系统和非接触式信息共享等各种商业环境中的需求日益增长。

了解基础知识

什么是 Blazor 服务器?

Blazor 服务器.NET Framework 是 ASP.NET Core 平台的网络应用程序框架。 它使开发人员能够使用 C# 而不是 JavaScript 构建交互式 Web UI。 这种服务器端模型通过 SignalR 连接(一种实时网络功能)处理用户交互。 这有助于开发人员创建有效的交互式网络应用程序。

IronQR 简介

IronQR是一个.NET 库,它的突出特点是能够读取、解释并生成二维码高准确性。 它提供了一系列功能,包括处理不同类型二维码内容的能力。 IronQR 的优势在于简单易用,易于集成到 .NET 应用程序中,是希望集成和创建 QR 代码功能的开发人员的首选。

如何创建一个Blazor二维码扫描器

  1. 在 Visual Studio 代码中创建 Blazor 服务器应用程序

  2. 使用 NuGet 软件包管理器安装 QR 代码类库

  3. 在 index.razor 中使用 HTML 和 CSS 创建用户界面

  4. 编写上传文件处理逻辑

  5. 使用 QR 库编写 QR 扫描逻辑

  6. 在文本框中显示结果

设置环境

创建新的 Blazor 服务器应用程序

启动 Visual Studio 并选择 "创建新项目"。在项目模板选择界面,找到并选择 "Blazor Server App "模板。 单击下一步。

如何创建 Blazor QR 码扫描器:图 1 - 找到正确的实施模板

选择模板后,输入项目名称和地点(其他一切保持默认值)然后单击 "下一步 "按钮。

如何创建 Blazor QR 码扫描器:图 2 - 配置项目详情

现在选择所需的 .NET Framework 并点击创建按钮。 它将创建一个 Blazor 服务器应用程序。

如何创建 Blazor QR 码扫描器:图 3 - 选择 .NET 框架并创建项目

安装 IronQR 库

单击菜单栏中的 "工具"。 从下拉菜单中选择 NuGet 包管理器。 从上下文菜单中选择 "管理解决方案的 NuGet 包"。 这将打开 NuGet 包管理器选项卡。

如何创建 Blazor QR 码扫描器:图 4 - 访问 NuGet 软件包管理器

在 NuGet 包管理器中,在 "浏览 "选项卡中搜索 "IronQR"。 然后在列表中找到 "IronQR "软件包。点击 "安装 "按钮。

如何创建 Blazor QR 码扫描器:图 5 - 通过 浏览 选项卡安装 IronQR 软件包

现在,您已经安装好了所有工具,我们可以了解一下项目结构以及如何在您的项目中实施所有工具。

实施 QR 码扫描

构建用户界面

二维码扫描仪的用户界面主要是在Index.razor文件中构建的。该文件是 Blazor 服务器项目的一部分,结合使用 HTML 和 Razor 语法创建动态交互式网页。 结构包括

@page "/"
@using System.IO
@using Microsoft.AspNetCore.Components.Forms
@using IronQr
@using IronSoftware.Drawing
@inject IJSRuntime JSRuntime
<PageTitle>QR Code Scanner</PageTitle>
<div>
    <h1>QR Code Scanner</h1> 
    <InputFile OnChange="HandleSelectedFile" accept="image/*" class="file-input" />
    @if (!string.IsNullOrEmpty(qrImageSrc))
    {
        <img src="@qrImageSrcForDisplay" alt="QR Code Image" class="qr-image" />
    }
    <button @onclick="ScanQRCode" disabled="@(!fileSelected)" class="button scan-button">Scan QR Code</button>
    @if (!string.IsNullOrEmpty(scannedText))
    {
        <div class="result-section">
            <button @onclick="CopyToClipboard" class="button copy-button">Copy</button>
        </div>
    }
</div>
@page "/"
@using System.IO
@using Microsoft.AspNetCore.Components.Forms
@using IronQr
@using IronSoftware.Drawing
@inject IJSRuntime JSRuntime
<PageTitle>QR Code Scanner</PageTitle>
<div>
    <h1>QR Code Scanner</h1> 
    <InputFile OnChange="HandleSelectedFile" accept="image/*" class="file-input" />
    @if (!string.IsNullOrEmpty(qrImageSrc))
    {
        <img src="@qrImageSrcForDisplay" alt="QR Code Image" class="qr-image" />
    }
    <button @onclick="ScanQRCode" disabled="@(!fileSelected)" class="button scan-button">Scan QR Code</button>
    @if (!string.IsNullOrEmpty(scannedText))
    {
        <div class="result-section">
            <button @onclick="CopyToClipboard" class="button copy-button">Copy</button>
        </div>
    }
</div>
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: @page "/" using System.IO using Microsoft.AspNetCore.Components.Forms using IronQr using IronSoftware.Drawing inject IJSRuntime JSRuntime <PageTitle> QR Code Scanner</PageTitle> <div> <h1> QR Code Scanner</h1> <InputFile OnChange="HandleSelectedFile" accept="image/*" class="file-input" /> if(!string.IsNullOrEmpty(qrImageSrc))
"image/*" Class="file-input" /> [if](Not String.IsNullOrEmpty(qrImageSrc))
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: Friend @page "/" using System.IO using Microsoft.AspNetCore.Components.Forms using IronQr using IronSoftware.Drawing inject IJSRuntime JSRuntime <PageTitle> QR Code Scanner</PageTitle> <div> <h1> QR Code Scanner</h1> <InputFile OnChange="HandleSelectedFile" accept="image/*" Class
"HandleSelectedFile" accept="image/*" Class
Friend page "/" [using] System.IO [using] Microsoft.AspNetCore.Components.Forms [using] IronQr [using] IronSoftware.Drawing inject IJSRuntime JSRuntime (Of PageTitle) QR Code Scanner</PageTitle> (Of div) (Of h1) QR Code Scanner</h1> <InputFile OnChange="HandleSelectedFile" accept
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: <img src="@qrImageSrcForDisplay" alt="QR Code Image" class="qr-image" />
		"QR Code Image" class="qr-image" />
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: <img src="@qrImageSrcForDisplay" alt="QR Code Image" class
		"@qrImageSrcForDisplay" alt="QR Code Image" class
		<img src="@qrImageSrcForDisplay" alt
End Class
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: <button onclick="ScanQRCode" disabled="@(!fileSelected)" class="button scan-button"> Scan QR Code</button> if(!string.IsNullOrEmpty(scannedText))
	"@(!fileSelected)" class="button scan-button"> Scan QR Code</button> [if](Not String.IsNullOrEmpty(scannedText))
	If True Then
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: <button onclick="ScanQRCode" disabled="@(!fileSelected)" class
	"ScanQRCode" disabled="@(!fileSelected)" class
	<button onclick="ScanQRCode" disabled
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: <div class="result-section"> <button onclick="CopyToClipboard" class="button copy-button"> Copy</button> </div>
		"CopyToClipboard" class="button copy-button"> Copy</button> </div>
'INSTANT VB WARNING: An assignment within expression was extracted from the following statement:
'ORIGINAL LINE: <div class="result-section"> <button onclick="CopyToClipboard" class
		"result-section"> <button onclick="CopyToClipboard" class
		<div class="result-section"> <button onclick
	End If
'INSTANT VB TODO TASK: The following line uses invalid syntax:
'</div>
VB   C#

标题和标题:标题和 `

标签分别定义页面标题和主标题,为用户设置上下文。

图片上传控件:一个组件 "用于上传二维码图像。 该元素只接受图像文件,通过过滤掉不相关的文件类型来增强用户体验。

图片显示:一旦上传了图片,就会使用".NET "和 "Python "显示。 related to 构建用户界面将内容转换为Chinese:

使用 <strong> 标签。 这种视觉反馈对于确保用户上传了正确的文件至关重要。

扫描按钮:带有 @onclick="ScanQRCode 标记的按钮会触发扫描过程。 其可用性取决于文件是否被选中,从而增强界面的直观性。

结果显示:扫描的 QR 代码文本将显示在文本输入框中,便于查看。 用户可以通过一个单独的按钮将这些文本复制到剪贴板。

site.css 中的 CSS 样式

QR 码扫描器的视觉美感和布局在 site.css 文件中定义。

.content {
    padding: 20px;
    margin: 10px auto; /* Centers the content */
    max-width: 500px; /* Sets a max width for the content */
    border-radius: 10px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
    text-align: center;
}
.file-input, .result-input {
    margin: 10px 0;
    padding: 10px;
    border-radius: 5px;
    border: 1px solid #ddd;
    width: 100%;
}
.button {
    background-color: #4CAF50;
    color: white;
    border: none;
    cursor: pointer;
    padding: 10px;
    margin: 10px 0;
    border-radius: 5px;
    transition: background-color 0.3s, box-shadow 0.3s;
    width: auto; /* Adjusts button width */
    display: inline-block; /* Allows the width to adjust to content */
}
.button:hover {
    background-color: #45a049;
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
} 
.qr-image {
    max-width: 300px;
    max-height: 300px;
    display: block;
    margin: 10px auto;
    border-radius: 10px;
}
.result-section {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
}
.result-input {
    width: 100%;
    box-sizing: border-box;
}
.copy-button {
    margin-top: 10px;
    white-space: nowrap;
}

.content:该类对主要内容区域进行样式设计,使其具有确定的宽度、居中对齐方式和微妙的阴影效果。

.file-input, .result-input:这些类对文件输入和结果显示元素进行样式设计,确保它们在视觉上保持一致,并完全占据容器的宽度。

.button:按钮采用独特的绿色背景、圆角和悬停效果,以实现更好的用户交互。

.qr-image:应用于二维码图像的样式包括尺寸限制和自动边距居中,使图像突出但不压倒性。

.result-section:该类确保结果部分中的元素居中对齐,间距适当。

处理文件上传

HandleSelectedFile "方法是二维码扫描过程的关键部分,用于处理用户上传的文件并为扫描做好准备。 当用户通过".NET "选择文件时,将触发该方法。组件。 这一点在以下代码中有所体现:

private async Task HandleSelectedFile(InputFileChangeEventArgs e)
{
    selectedFile = e.File;
    fileSelected = true;
    var imagesDirectory = Path.Combine(Directory.GetCurrentDirectory(), "UploadedImages");
    Directory.CreateDirectory(imagesDirectory); // Ensure the directory exists
    // Use a GUID as the unique file name
    var uniqueFileName = Guid.NewGuid().ToString() + Path.GetExtension(selectedFile.Name);
    var fullPath = Path.Combine(imagesDirectory, uniqueFileName);
    await using (var fileStream = new FileStream(fullPath, FileMode.Create))
    {
        await selectedFile.OpenReadStream().CopyToAsync(fileStream);
    }
    // Store the full path in qrImageSrc for scanning
    qrImageSrc = fullPath;
    // Optionally, create a base64 string for displaying the image (if needed)
    byte [] imageBytes = await File.ReadAllBytesAsync(fullPath);
    var base64String = Convert.ToBase64String(imageBytes);
    qrImageSrcForDisplay = $"data:image/{Path.GetExtension(selectedFile.Name).TrimStart('.')};base64,{base64String}";
}
private async Task HandleSelectedFile(InputFileChangeEventArgs e)
{
    selectedFile = e.File;
    fileSelected = true;
    var imagesDirectory = Path.Combine(Directory.GetCurrentDirectory(), "UploadedImages");
    Directory.CreateDirectory(imagesDirectory); // Ensure the directory exists
    // Use a GUID as the unique file name
    var uniqueFileName = Guid.NewGuid().ToString() + Path.GetExtension(selectedFile.Name);
    var fullPath = Path.Combine(imagesDirectory, uniqueFileName);
    await using (var fileStream = new FileStream(fullPath, FileMode.Create))
    {
        await selectedFile.OpenReadStream().CopyToAsync(fileStream);
    }
    // Store the full path in qrImageSrc for scanning
    qrImageSrc = fullPath;
    // Optionally, create a base64 string for displaying the image (if needed)
    byte [] imageBytes = await File.ReadAllBytesAsync(fullPath);
    var base64String = Convert.ToBase64String(imageBytes);
    qrImageSrcForDisplay = $"data:image/{Path.GetExtension(selectedFile.Name).TrimStart('.')};base64,{base64String}";
}
Private Async Function HandleSelectedFile(ByVal e As InputFileChangeEventArgs) As Task
	selectedFile = e.File
	fileSelected = True
	Dim imagesDirectory = Path.Combine(Directory.GetCurrentDirectory(), "UploadedImages")
	Directory.CreateDirectory(imagesDirectory) ' Ensure the directory exists
	' Use a GUID as the unique file name
	Dim uniqueFileName = Guid.NewGuid().ToString() & Path.GetExtension(selectedFile.Name)
	Dim fullPath = Path.Combine(imagesDirectory, uniqueFileName)
'INSTANT VB TODO TASK: Local functions are not converted by Instant VB:
'	await using(var fileStream = New FileStream(fullPath, FileMode.Create))
'	{
'		await selectedFile.OpenReadStream().CopyToAsync(fileStream);
'	}
	' Store the full path in qrImageSrc for scanning
	qrImageSrc = fullPath
	' Optionally, create a base64 string for displaying the image (if needed)
	Dim imageBytes() As Byte = Await File.ReadAllBytesAsync(fullPath)
	Dim base64String = Convert.ToBase64String(imageBytes)
	qrImageSrcForDisplay = $"data:image/{Path.GetExtension(selectedFile.Name).TrimStart("."c)};base64,{base64String}"
End Function
VB   C#

以下是其功能的详细分类:

文件选择和验证:当用户上传文件时,该方法会使用 InputFileChangeEventArgs e.捕获文件的详细信息。 然后将 selectedFile 变量分配给该文件,并将布尔值 fileSelected 设为 true,表示输入的数据/文件已准备好进行处理。

创建文件路径:该方法准备一个目录来存储上传的图像。 它使用 Path.Combine 创建通往 "UploadedImages "目录的路径,并通过 Directory.CreateDirectory确保该目录存在。 这一步对于系统整理上传的文件至关重要。

生成唯一的文件名:为避免与现有文件冲突,将使用 GUID 生成唯一的文件名(全球唯一标识符)在译文后附上原始文件的扩展名。 这将确保上传的每个文件都是 "唯一路径识别 "的。

保存文件:然后将文件保存到服务器。 该方法将创建一个指向新生成文件路径的文件流,并使用 await selectedFile.OpenReadStream 将上传文件的内容复制到该文件流中。().CopyToAsync(文件流). 这一步完成了上传过程。

准备显示图像:文件保存后,有必要将图像显示给用户确认。 该方法将文件读入一个字节数组,然后将其转换为 base64 字符串,适合直接嵌入到.NET'中。<img>标签的src` 属性。 这种转换可以显示图像,而不需要向服务器单独请求图像文件。

扫描二维码

ScanQRCode "方法是 Blazor 服务器应用程序中二维码扫描功能的核心。 该方法获取上传的图片,并使用 IronQR 提取二维码数据。

private async Task ScanQRCode()
{   
    // Check if there is a valid image to work with
    if (string.IsNullOrEmpty(qrImageSrc)) return;
    try
    {
        var inputBmp = AnyBitmap.FromFile(qrImageSrc);
        QrImageInput imageInput = new QrImageInput(inputBmp);
        QrReader reader = new QrReader();
        IEnumerable<QrResult> results = reader.Read(imageInput);
        // Check if there are any results and if the first result contains text
        var firstResult = results.FirstOrDefault();
        if (firstResult != null && !string.IsNullOrWhiteSpace(firstResult.Value.ToString()))
        {
            scannedText = firstResult.Value.ToString();
        }
        else
        {
            scannedText = "QR value not found!";
        }
    }
    catch (Exception ex)
    {
        scannedText = "Error scanning QR code: " + ex.Message;
    }
}
private async Task ScanQRCode()
{   
    // Check if there is a valid image to work with
    if (string.IsNullOrEmpty(qrImageSrc)) return;
    try
    {
        var inputBmp = AnyBitmap.FromFile(qrImageSrc);
        QrImageInput imageInput = new QrImageInput(inputBmp);
        QrReader reader = new QrReader();
        IEnumerable<QrResult> results = reader.Read(imageInput);
        // Check if there are any results and if the first result contains text
        var firstResult = results.FirstOrDefault();
        if (firstResult != null && !string.IsNullOrWhiteSpace(firstResult.Value.ToString()))
        {
            scannedText = firstResult.Value.ToString();
        }
        else
        {
            scannedText = "QR value not found!";
        }
    }
    catch (Exception ex)
    {
        scannedText = "Error scanning QR code: " + ex.Message;
    }
}
Private Async Function ScanQRCode() As Task
	' Check if there is a valid image to work with
	If String.IsNullOrEmpty(qrImageSrc) Then
		Return
	End If
	Try
		Dim inputBmp = AnyBitmap.FromFile(qrImageSrc)
		Dim imageInput As New QrImageInput(inputBmp)
		Dim reader As New QrReader()
		Dim results As IEnumerable(Of QrResult) = reader.Read(imageInput)
		' Check if there are any results and if the first result contains text
		Dim firstResult = results.FirstOrDefault()
		If firstResult IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(firstResult.Value.ToString()) Then
			scannedText = firstResult.Value.ToString()
		Else
			scannedText = "QR value not found!"
		End If
	Catch ex As Exception
		scannedText = "Error scanning QR code: " & ex.Message
	End Try
End Function
VB   C#

首先,该方法会检查 qrImageSrc 变量是否为空,该变量包含上传图片的路径。 该检查可确保在继续翻译之前有一个有效的图像可供使用。

一旦确认图像可以处理,该方法就会进入二维码读取的核心功能。 这涉及几个关键步骤,首先是将图像从其存储位置加载到适合 QR 代码分析的格式。 这种转换是通过 `AnyBitmap.FromFile(qrImageSrc)方法,为扫描过程准备图像。

下一步是创建一个 QrReader 对象。 该对象是 IronQR 库不可或缺的一部分,是从图像中解码二维码的主要工具。 在 "QrReader "实例准备就绪后,应用程序开始扫描上传的图像。 读者.阅读(图像输入)` 功能负责此操作,有条不紊地搜索图片中的二维码并提取其数据。

扫描结果存储在一个 IEnumerable<QrResult>收集。 然后,我们会仔细检查这组译文,以找到第一个二维码结果。 如果检测到 QR 代码并包含可读文本,则会捕获该文本并将其存储到scannedText` 变量中。 不过,在未找到二维码或二维码不包含文本的情况下,应用程序会设置一条默认消息,告知用户未检测到二维码值。

一旦成功扫描二维码,文本字符串就会显示在文本输入框中,这要归功于 Blazor 的双向数据绑定功能。 这是通过将 scannedText 变量绑定到文本输入元素来实现的。 输入字段设置为禁用,只读。 这种设计选择将用户交互的重点放在查看结果和复制结果上,而不是编辑内容。

整个扫描过程都包含在一个 try-catch 块中,以防止在扫描操作过程中出现意外错误。 这可能包括与图像文件格式相关的问题或阅读过程中的意外错误。 如果出现异常,就会捕捉到异常,并编写错误信息显示给用户。 这种方法不仅有助于发现问题,还能保持对用户的透明度,提高应用程序的可靠性。

复制结果

要启用复制到剪贴板功能,需要在 _Host.cshtml 文件中定义一个名为 "copyTextToClipboard "的 JavaScript 函数。该脚本是与剪贴板进行交互的一种简单而有效的方法:

<script>
    function copyTextToClipboard(text) {
        navigator.clipboard.writeText(text).then(function () {
            console.log('Copying to clipboard was successful!');
        }, function (err) {
            console.error('Could not copy text: ', err);
        });
    }
</script>
JAVASCRIPT

该函数接受一个文本参数,即要复制的文本。 它使用了 navigator.clipboard.writeText 方法,这是一种与剪贴板交互的现代方法。 这种方法因其简单性和符合网络标准而受到青睐。 设计的目的是在复制成功后在控制台中记录一条成功信息,以帮助调试并确保功能顺畅。 如果出现错误,错误信息将记录到控制台,以便深入了解操作过程中遇到的任何问题。

CopyToClipboard "方法位于 index.razor 的"@code "部分,是 Blazor 应用程序与 JavaScript 函数之间的桥梁。 用户界面中的按钮可触发该方法的点击。 激活后,它将使用 Blazor 的 JavaScript InterOp 功能调用 copyTextToClipboard JavaScript 函数。 扫描文本 "作为参数传递给该函数,可有效地将文本复制到用户的剪贴板。

private async Task CopyToClipboard()
{
    await JSRuntime.InvokeVoidAsync("copyTextToClipboard", scannedText);
}
private async Task CopyToClipboard()
{
    await JSRuntime.InvokeVoidAsync("copyTextToClipboard", scannedText);
}
Private Async Function CopyToClipboard() As Task
	Await JSRuntime.InvokeVoidAsync("copyTextToClipboard", scannedText)
End Function
VB   C#

执行应用程序

运行该项目后,用户将看到以下简洁的界面。 初始屏幕突出展示了 QR 码扫描器模块。 该模块包含一个上传二维码图像文件的按钮(选择文件)另一个用于启动扫描程序(扫描 QR 码). 最初,没有选择任何文件,扫描区域为空白,等待用户输入。

如何创建 Blazor QR 码扫描器:图 6 - 项目初始执行结果

用户使用 "Choose File"(选择文件)按钮选择并上传 QR 代码图像,此时会显示所选文件的名称(例如,"qrvalue.png). 上传的 QR 代码在界面上的指定区域可见,向用户确认图像已准备好扫描。

如何创建 Blazor QR 码扫描器:图 7 - 用户输入二维码的结果

用户点击 "扫描二维码 "按钮后,应用程序将处理图像。 如果扫描成功,二维码内的编码文本就会显示在图片下方。 在这种情况下,扫描结果('<https://ironsoftware.com/csharp/qr/>')是一个 URL,表示用户使用 QR 阅读器扫描 QR 代码时的指向。 结果旁边会出现 "复制 "按钮,用户可以轻松地将扫描文本复制到剪贴板,以便进一步使用。

如何创建 Blazor QR 码扫描器:图 8 - 显示二维码文本和复制按钮

结论

如何创建 Blazor QR 码扫描器:图 9

总之,将 IronQR 集成到 Blazor 服务器应用程序中的过程是顺利而有效的,从而形成了一个二维码扫描解决方案。 由于混合使用了 IronQR 的强大处理功能和 Blazor 的动态用户界面渲染功能,从开始设置这个项目到实现扫描功能,都具有响应性和易用性。 从设置环境到部署的整个过程都要强调这种集成在实际应用中的实用性和有效性。 虽然 IronQR 擅长二维码,但对于需要扫描 BarCode 功能的项目来说、IronBarcode在此基础上,我们建议您使用.NET、Java、Python 或 Node.js 工具,因为它们具有类似的易用性和集成性。

IronQR 提供了一个免费试用翻译的目的是让开发人员在购买前了解其功能。 要在生产中扩展使用并获得其所有专业功能,IronQR 许可证的起价为 $749。

< 前一页
如何在 C# 中打印条形码
下一步 >
如何在VB .NET中打印条形码标签

准备开始了吗? 版本: 2024.11 刚刚发布

免费NuGet下载 总下载量: 1,290,353 查看许可证 >