以下是 Spectre.Console 的核心功能示例,涵盖最常用的输出和交互场景:
1. 基础富文本输出

using Spectre.Console;
// 使用 Markup 语法(类似 BBCode)
AnsiConsole.Markup("[bold green]成功![/] 文件已保存。\n");
AnsiConsole.Markup("[red]错误:[/] 无法连接到服务器。\n");
// 混合样式
AnsiConsole.Markup("[underline blue]https://example.com[/]\n");
// 自动换行写
AnsiConsole.Write(new Panel("[yellow]警告[/] 磁盘空间不足")
.Header("系统通知")
.Border(BoxBorder.Rounded));2. 表格

var table = new Table();
table.AddColumn("[u]ID[/]");
table.AddColumn(new TableColumn("[u]名称[/]").Centered());
table.AddColumn("[u]状态[/]");
table.AddRow("1", "订单服务", "[green]运行中[/]");
table.AddRow("2", "支付网关", "[red]离线[/]");
table.AddRow("3", "消息队列", "[yellow]警告[/]");
AnsiConsole.Write(table);3. 进度条 / 状态指示


// 带进度条的循环任务
await AnsiConsole.Progress()
.StartAsync(async ctx =>
{
var task1 = ctx.AddTask("[green]下载文件[/]", maxValue: 100);
var task2 = ctx.AddTask("[green]处理数据[/]", maxValue: 100);
while (!ctx.IsFinished)
{
task1.Increment(1.5);
task2.Increment(0.8);
await Task.Delay(50);
}
});
// 不确定时长的旋转状态
await AnsiConsole.Status()
.StartAsync("正在连接...", async ctx =>
{
await Task.Delay(3000); // 模拟工作
AnsiConsole.MarkupLine("[green]连接成功![/]");
});4. 交互式提示

// 确认
if (AnsiConsole.Confirm("是否继续安装?"))
{
// 执行安装
}
// 文本输入(带验证)
var name = AnsiConsole.Prompt(
new TextPrompt<string>("请输入用户名:")
.ValidationErrorMessage("[red]用户名不能为空[/]")
.Validate(input => !string.IsNullOrWhiteSpace(input)));
// 选择列表
var fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("请选择最喜欢的水果")
.AddChoices(new[] { "苹果", "香蕉", "橙子", "葡萄" }));
AnsiConsole.MarkupLine($"你选择了:[green]{fruit}[/]");
// 多选
var colors = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
.Title("请选择颜色")
.AddChoices(new[] { "红色", "绿色", "蓝色", "黄色" }));5. 树形结构

var root = new Tree("[yellow]项目结构[/]");
var src = root.AddNode("src");
src.AddNode("Program.cs");
src.AddNode("Services");
src.AddNode("Models");
var tests = root.AddNode("tests");
tests.AddNode("UnitTests");
AnsiConsole.Write(root);6. 布局 / 网格

var layout = new Layout("Root")
.SplitColumns(
new Layout("Left").Size(30),
new Layout("Right")
.SplitRows(
new Layout("Top"),
new Layout("Bottom")));
layout["Left"].Update(new Panel("导航栏"));
layout["Top"].Update(new Panel("主内容区"));
layout["Bottom"].Update(new Panel("日志输出"));
AnsiConsole.Write(layout);7. 实时更新

var table = new Table().AddColumn("Time").AddColumn("Message");
table.AddRow("10:00", "系统启动");
await AnsiConsole.Live(table)
.StartAsync(async ctx =>
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000);
table.AddRow($"10:0{i}", $"事件 {i}");
ctx.Refresh(); // 刷新显示
}
});8. 带样式的异常显示

try
{
throw new InvalidOperationException("操作失败");
}
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths);
}9. 日历

var calendar = new Calendar(2026, 4);
calendar.AddCalendarEvent(2026, 4, 22); // 标记日期
calendar.HighlightStyle(Style.Parse("yellow bold"));
AnsiConsole.Write(calendar);10. 条形图 / 柱状图

AnsiConsole.Write(new BarChart()
.Width(60)
.Label("[green bold]项目进度[/]")
.CenterLabel()
.AddItem("后端", 80, Color.Green)
.AddItem("前端", 65, Color.Blue)
.AddItem("测试", 40, Color.Red));11. 分解图

AnsiConsole.Write(new BreakdownChart()
.Width(60)
.AddItem("CPU", 45, Color.Red)
.AddItem("内存", 30, Color.Blue)
.AddItem("磁盘", 25, Color.Green));12. 规则线

AnsiConsole.Write(new Rule("[red]警告区域[/]").RuleStyle("red").LeftJustified());
AnsiConsole.WriteLine("内容");
AnsiConsole.Write(new Rule().RuleStyle("green"));13. 文本样式

// 大字标题
AnsiConsole.Write(new FigletText("Hello").Color(Color.Green));
// Emoji
AnsiConsole.Markup(":check_mark_button: 成功 :cross_mark: 失败");14. 网格

var grid = new Grid();
grid.AddColumn(new GridColumn().NoWrap().PadRight(4));
grid.AddColumn();
grid.AddRow("[b]名称[/]", "Spectre.Console");
grid.AddRow("[b]版本[/]", "0.49.1");
grid.AddRow("[b]许可[/]", "MIT");
AnsiConsole.Write(grid);在 Linux 上运行 .NET 网站,通过
HttpContext.Connection.RemoteIpAddress获取客户端的 IP 地址,结果是
::ffff:127.0.0.1解决方法:
打开 Program.cs 文件,在 var app = builder.Build(); 之前(尽量往前)添加以下代码:
if (OperatingSystem.IsLinux())
{
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto
| ForwardedHeaders.XForwardedHost;
// 清除 KnownNetworks 和 KnownProxies,表示信任来自本机的代理(如 Nginx)
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Console.WriteLine("ForwardedHeaders enabled (Running on Linux)");
}然后在 app.UseRouting(); 之前添加以下代码:
if (OperatingSystem.IsLinux())
{
app.UseForwardedHeaders();
Console.WriteLine("UseForwardedHeaders() applied.");
}其中,OperatingSystem.IsLinux() 用于判断只在 Linux 环境中生效,你可以视自身情况作判断。
IIS 中使用 HttpPlatformHandler 模块部署 python web 项目时遇到 502.3 网关错误:
HTTP 错误 502.3 - Bad Gateway
There was a connection error while trying to route the request.
最可能的原因:
CGI 应用程序没有返回一组有效的 HTTP 错误。
由于父网关中出现错误,充当代理或网关的服务器无法处理该请求。
可尝试的操作:
使用 DebugDiag 排查 CGI 应用程序。
确定此错误是否由代理或网关引起。
详细错误信息:
模块 httpPlatformHandler
通知 ExecuteRequestHandler
处理程序 httpplatformhandler
错误代码 见下方表格
详细信息:
当 CGI 应用程序未返回一组有效的 HTTP 头,或者代理或网关无法将请求发送至父网关时,便会出现此错误。您可能需要获取一个网络跟踪,或者与代理服务器管理员联系(如果不是 CGI 的问题)。
| 错误代码 | 可能的错误原因 |
|---|---|
| 0x8007053d | 未安装相关组件库,建议执行命令:pip install -r requirements.txt |
| 0x80070005 | 若显示配置错误“由于权限不足而无法读取配置文件”则是应用代码目录未配置相应用户的读取权限; 访问受限,请正确配置 IIS 中的网站凭据、应用程序池标识、项目目录“读取”、解释器目录“读取和执行”权限等 |
| 0x80072ee2 | 执行超时,一般发生在批量读写的时候 |
| 0x8007042b | |
| 任何错误 | 查看日志文件。 可能的错误原因:
|
在代码中添加“不跟踪”(No-Tracking)功能,以提高查询性能(避免实体状态跟踪)。
确保后续操作无需更新返回的实体(如没有 SaveChanges 操作)。
在条件(Where)、排序(OrderBy、OrderByDescending)、分页(Skip、Take)等之前添加 .AsNoTracking(),如:db.Table.AsNoTracking().Where(...).ToList()。
如果查询中包含导航属性,它们也会因主查询的不跟踪而保持不跟踪状态。
AsNoTracking() 适用于查询简单无嵌套关系,若查询包含 Include/ThenInclude,建议用 AsNoTrackingWithIdentityResolution() 代替,后者更适合处理树形结构或循环引用数据。
添加(Add/AddRange)、修改、删除(Remove/RemoveRange)等有 SaveChanges 操作的场景不能加 .AsNoTracking()。
查询时是否需要加 .AsNoTracking() 参考下表:
数据查询操作体系
├── 限定符 (Any, All) ✔️ 推荐
├── 聚合函数 (Count, Sum, Min, Max, Average) ❌ 不需要(但在分页逻辑中 Count 与 ToList 使用共同条件筛选时建议加)
├── 集合操作 (Distinct, Union) ⚠️ 推荐用于只读
├── 元素操作 (First, Single) ⚠️ 推荐用于只读
└── 即时执行操作 (ToList, ToArray, ToDictionary) ⚠️ 推荐用于只读
临界区与 lock 关键字
核心作用:
通过将多线程访问串行化,保护共享资源或代码段。lock 关键字是 Monitor 类的语法糖,提供异常安全的临界区实现。
实现示例:
// 创建私有静态只读对象 // private static readonly object _lockObj = new object(); private static readonly System.Threading.Lock _locker = new(); // .NET 9+ 推荐使用 Lock 类型,避免传统 object 的性能损耗 public void ThreadSafeMethod() { lock (_lockObj) { // 临界区代码(每次仅一个线程可进入) } }超时机制:
高并发场景可结合 Monitor.TryEnter 设置超时,避免无限等待:
if (Monitor.TryEnter(lockObject, TimeSpan.FromSeconds(1))) { try { /* 操作 */ } finally { Monitor.Exit(lockObject); } }关键特性:
用户态锁(无内核切换开销)
自动调用Monitor.Enter和Monitor.Exit
必须使用专用私有对象作为锁标识
注意事项:
❌ 避免锁定this、Type实例或字符串(易引发死锁)
❌ 避免嵌套锁(需严格按顺序释放)
✅ 推荐readonly修饰锁对象
❌ lock 不适用于异步代码(async/await),需使用 SemaphoreSlim 实现异步锁
互斥锁(Mutex)
核心作用:
系统级内核锁,支持跨进程同步,但性能开销较高(用户态/内核态切换)。
实现示例:
using var mutex = new Mutex(false, "Global\\MyAppMutex"); try { // 等待锁(最大等待时间500ms) if (mutex.WaitOne(500)) { // 临界区代码 } } finally { if (mutex != null) { mutex.ReleaseMutex(); } }关键特性:
支持跨应用程序域同步
线程终止时自动释放锁
支持命名互斥体(系统全局可见)
适用场景:
单实例应用程序控制
进程间共享文件访问
硬件设备独占访问
信号量(Semaphore)
核心作用:
通过许可计数器控制并发线程数,SemaphoreSlim为轻量级版本(用户态实现)。
实现对比:
类型 跨进程 性能 最大许可数 Semaphore ✔️ 低 系统限制 SemaphoreSlim ❌ 高 Int32.Max 代码示例:
// 创建初始3许可、最大5许可的信号量 var semaphore = new SemaphoreSlim(3, 5); semaphore.Wait(); // 获取许可 try { // 资源访问代码 } finally { semaphore.Release(); }异步编程
private readonly SemaphoreSlim _asyncLock = new(1, 1); public async Task UpdateAsync() { await _asyncLock.WaitAsync(); try { /* 异步操作 */ } finally { _asyncLock.Release(); } }典型应用:
数据库连接池(限制最大连接数)
API 请求限流
批量任务并发控制
事件(Event)
核心机制:
通过信号机制实现线程间通知,分为两种类型:
类型 信号重置方式 唤醒线程数 AutoResetEvent 自动 单个 ManualResetEvent 手动 所有 使用示例:
var autoEvent = new AutoResetEvent(false); // 等待线程 Task.Run(() => { autoEvent.WaitOne(); // 收到信号后执行 }); // 信号发送线程 autoEvent.Set(); // 唤醒一个等待线程高级用法:
配合WaitHandle.WaitAll实现多事件等待
使用ManualResetEventSlim提升性能
读写锁(ReaderWriterLockSlim)
核心优势:
实现读写分离的并发策略,适合读多写少场景(如缓存系统)。
锁模式对比:
模式 并发性 升级支持 读模式(EnterReadLock) 多线程并发读 ❌ 写模式(EnterWriteLock) 独占访问 ❌ 可升级模式 单线程读→写 ✔️ 代码示例:
var rwLock = new ReaderWriterLockSlim(); // 读操作 rwLock.EnterReadLock(); try { // 只读访问 } finally { rwLock.ExitReadLock(); } // 写操作 rwLock.EnterWriteLock(); try { // 排他写入 } finally { rwLock.ExitWriteLock(); }最佳实践:
优先使用ReaderWriterLockSlim(旧版有死锁风险)
避免长时间持有读锁(可能饿死写线程)
原子操作(Interlocked)
原理:
通过CPU指令实现无锁线程安全操作。
常用方法:
int counter = 0; Interlocked.Increment(ref counter); // 原子递增 Interlocked.Decrement(ref counter); // 原子递减 Interlocked.CompareExchange(ref value, newVal, oldVal); // CAS操作适用场景:
简单计数器
标志位状态切换
无锁数据结构实现
自旋锁(SpinLock)
核心特点:
通过忙等待(busy-wait)避免上下文切换,适用极短临界区(<1微秒)。
实现示例:
private SpinLock _spinLock = new SpinLock(); public void CriticalOperation() { bool lockTaken = false; try { _spinLock.Enter(ref lockTaken); // 极短临界区代码 } finally { if (lockTaken) _spinLock.Exit(); } }优化技巧:
单核CPU需调用Thread.SpinWait或Thread.Yield
配合SpinWait结构实现自适应等待
同步机制对比指南
机制 跨进程 开销级别 最佳适用场景 lock ❌ 低 通用临界区保护 Mutex ✔️ 高 进程间资源独占 Semaphore ✔️ 中 并发数限制(跨进程) SemaphoreSlim ❌ 低 并发数限制(进程内) ReaderWriterLockSlim ❌ 中 读多写少场景 SpinLock ❌ 极低 纳秒级临界区 Interlocked - 无锁 简单原子操作
选择原则:
优先考虑用户态锁(lock/SpinLock/SemaphoreSlim)
跨进程需求必须使用内核对象(Mutex/Semaphore)
读写比例超过10:1时考虑读写锁
自旋锁仅用于高频短操作(如链表指针修改)
通过以上结构化的分类和对比,开发者可以更精准地选择适合特定场景的线程同步方案。建议在实际使用中配合性能分析工具(如BenchmarkDotNet)进行量化验证。
💡 ASP.NET 的异步编程(async/await)本质是单进程内的线程调度,不算“跨进程”。每个 IIS 应用程序池对应一个独立的工作进程(w3wp.exe),不同用户访问同一应用程序池下的 ASP.NET 网站,两者的请求均由同一个 w3wp.exe 进程处理。可能跨进程的场景有:Web Garden 配置、多应用程序池部署等。
在 C# 中,除了常规锁机制(如 lock、Mutex、Semaphore 等),还有一些内置类型通过内部锁或无锁设计实现线程安全。以下是常见的几类:
线程安全集合(System.Collections.Concurrent)这些集合通过细粒度锁或无锁算法(如 CAS)实现线程安全,适合高并发场景。
ConcurrentDictionary:分段锁机制,将数据分片存储,每个分片独立加锁,减少锁竞争。
ConcurrentQueue / ConcurrentStack:基于原子操作(Interlocked)保证线程安全。
ConcurrentBag:每个线程维护本地存储,减少争用,适合频繁添加和移除的场景。
BlockingCollection:基于 ConcurrentQueue 和信号量(SemaphoreSlim)实现生产-消费者模式,支持阻塞和超时。
不可变集合(System.Collections.Immutable) 通过数据不可变性实现线程安全(无需锁),每次修改返回新对象。
Lazy 的线程安全初始化(Lazy<T>) 通过锁或 Interlocked 确保延迟初始化的线程安全。
通道(System.Threading.Channels)用于异步生产-消费者模型,内部通过锁和信号量管理容量限制。
内存缓存(System.Runtime.Caching.MemoryCache)内部使用锁保护共享状态,确保线程安全。
原子操作类型(Interlocked 类、Volatile 关键字、Unsafe 类)通过 CPU 指令实现无锁线程安全。
其他同步工具(Barrier、CountdownEvent)虽然不是严格意义上的锁,但用于协调线程。
前几天实现了在 nginx 中使用 lua 实现远程鉴权,今天想试试在 IIS 中能不能实现相同的功能。查询资料发现需要使用 URL 重写和 HTTP 请求模块,没有深究。干脆使用 ASP.NET 中间件来实现吧。
在 StratUp.cs 的 Configure 方法中,或 Program.cs 文件中添加以下代码:
// 远程鉴权
app.Use(async (context, next) =>
{
var ip = context.Connection.RemoteIpAddress!.ToString();
var ua = context.Request.Headers.UserAgent.ToString();
var host = context.Request.Host.Host;
var uri = new Uri(context.Request.GetDisplayUrl()).PathAndQuery;
var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(1); // 设置超时时间
try
{
var requestUrl = "https://鉴权地址/";
var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUrl);
requestMessage.Headers.Add("X-Real-IP", ip);
requestMessage.Headers.Add("User-Agent", ua);
requestMessage.Headers.Add("X-Forwarded-Host", host);
requestMessage.Headers.Add("X-Forwarded-Uri", uri);
// 发送请求
var response = await client.SendAsync(requestMessage);
// 检查响应状态码
if (response.StatusCode == HttpStatusCode.Forbidden)
{
// 如果返回403,则拒绝访问
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
await context.Response.WriteAsync("Access Denied");
}
else
{
// 如果返回其他状态码,则继续执行管道中的下一个中间件
await next();
}
}
catch (TaskCanceledException ex) when (ex.CancellationToken.IsCancellationRequested)
{
// 如果请求超时(任务被取消),则继续执行管道中的下一个中间件
await next();
}
catch
{
// 如果遇到错误,则继续执行管道中的下一个中间件
await next();
}
});代码很简单,使用 HttpClient 发送请求,若返回 403 则拒绝访问,其它情况继续执行业务逻辑,超时或报错的情况按需修改即可。
若鉴权接口在私网中,建议将鉴权接口域名和私网 IP 添加到 hosts 文件中。
-
在 access_by_lua_block 代码块中实现远程鉴权:
#鉴权-START #resolver 223.5.5.5; # cat /etc/resolv.conf access_by_lua_block { local http = require("resty.http") local httpc = http.new() httpc:set_timeout(500) -- 连接超时 local res, err = httpc:request_uri("https://鉴权地址/", { method = "GET", headers = { ["X-Real-IP"] = ngx.var.remote_addr, ["User-Agent"] = ngx.var.http_user_agent, ["X-Forwarded-Host"] = ngx.var.host, ["X-Forwarded-Uri"] = ngx.var.request_uri, }, ssl_verify = false, -- 禁用 SSL 验证 timeout = 500, -- 读取超时 }) if not res then ngx.log(ngx.ERR, "Failed to request: " .. err) end if res and res.status == 403 then ngx.exit(ngx.HTTP_FORBIDDEN) -- return ngx.redirect("https://一个显示403友好信息的页面.html") end } #鉴权-END注意更改接口地址和友情显示 403 页面地址。
本示例仅捕获 403 状态码,500、408 等其它异常情况视为允许访问,请根据业务需求自行添加状态码的判断。
若超时也会进入
if not res then代码块。建议将此代码部署在 nginx 主配置文件的 http 代码块中(宝塔面板中的路径:/www/server/nginx/nginx/conf/nginx.conf),如果你只想为单个网站鉴权,也可以放在网站配置文件的 server 块中。
-
若鉴权接口在私网中,建议将鉴权接口域名和私网 IP 添加到 hosts 文件中。
附
-
直接输出字符串
ngx.header.content_type = "text/plain"; ngx.say("hello world!") -
输出到日志
ngx.log(ngx.ERR, "Response status: " .. res.status)日志在网站的 站名.error.log 中查看。
宝塔面板查看方式:日志 - 网站日志 - 异常
-
若想获取服务器 CPU 使用率等信息并传递给远程鉴权接口,请参考此文。
-
常见问题
no resolver defined to resolve
原因:没有定义 DNS 解析器
解决方法:在 http 块或 server 块中添加
resolver 8.8.8.8 valid=30s;,推荐使用接入商自己的公共 DNS,如果走内网,推荐使用本机 DNS。unable to get local issuer certificate
原因:没有配置 SSL 证书信息
解决方法:添加 request_uri 参数:
ssl_verify = true, -- 启用 SSL 验证 ssl_trusted_certificate = "证书路径", -- 指定 CA 证书路径或
ssl_verify = false, -- 禁用 SSL 验证 -
若您不想用 lua,可以用 nginx 原生自带的 auth_request 模块来实现。
-
若 WAF 返回拒绝访问时需要 302 跳转(即 return ngx.redirect(...)),那么最好加上禁止缓存的标识,防止 CDN 污染:
if res and res.status == 403 then -- ===== 禁止 CDN 缓存此响应 ===== ngx.header["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate" ngx.header["Pragma"] = "no-cache" ngx.header["Expires"] = "0" -- 可选:添加标记头,方便调试 ngx.header["X-WAF-Action"] = "blocked" return ngx.redirect("https://...") end
鉴权方式有许多,譬如可以用 lua 的 access_by_lua 来实现,这里用 nginx 自带的 auth_request,虽然功能简单,但效率更高。
第一步,确保 nginx 已编译安装 auth_request 模块,见此文。
第二步,打开需要鉴权的网站的 nginx 配置文件,添加以下代码块:
#鉴权-START
location / {
auth_request /auth;
error_page 403 = @error403;
#PHP-INFO-START PHP引用配置,可以注释或修改
include enable-php-**.conf;
#PHP-INFO-END
}
location = /auth {
internal;
proxy_pass https://鉴权地址;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_intercept_errors on;
#proxy_read_timeout 1s; # 超时会报 500
}
location @error403 {
#default_type text/plain;
#return 403 'forbidden';
return 302 https://一个显示403友好信息的页面.html;
}
#鉴权-END一般放在所有的 location 之前。
这里自定义请求头 X-Forwarded-Host 与 X-Forwarded-Uri 用来传递 host 与 uri。API 应从 Header 中相应取值。
宝塔面板中是通过 include enable-php-**.conf; 的方式调用 PHP ,那么可以将此行移入上面的 location / 代码块中,因为此代码块能匹配所有的请求路径。
最后,若鉴权接口在私网中,将鉴权接口域名和私网 IP 添加到 hosts 文件中。
实例化方法一:DbSearcher(String dbFile, QueryType queryType, String key)
实例化方法二:DbSearcher(InputStream is, QueryType queryType, String key)
方法一通过传入文件路径来实例化,方法二通过传入文件流来实例化;
当 queryType 为 MEMORY 时,方法一和方法二只有在实例化时有性能差别,差别在于将指定路径的文件读到内存中保存;在执行 Search 方法时无性能差别,因为都是在内存中执行查找;
queryType 为 MEMORY 和 BTREE 的差别在于前者是在内存中查找,速度快但占内存,后者是在文件中查找,速度慢但省内存。
错误:Padding is invalid and cannot be removed.
原因:可能是数据库文件与密钥不一致。
今天忘了带鼠标,正巧笔记本电脑的触摸板驱动坏了,一时也用不了,所以想到用 iPhone 当作电脑的触控板来用。电脑操作系统是 Windows。
一、电脑上下载并安装 Remote Helper(此步需要会使用纯键盘来操作电脑,常用的键是 TAB、方向键、回车键)
二、手机上 App Store 中搜索并获取 鼠标·键盘
电脑和手机需要连接同一局域网,如果没有局域网,可以将电脑连接 iPhone 的热点。
打开 App,搜索 PC,授权,就可以控制光标了。
如果电脑未显示光标,尝试双击 CTRL 键来确定当前光标的位置,前提是安装了 PowerToys。