当前位置: 首页 > article >正文

在人工智能软件的帮助下学习编程实例

1 引言

本文记录在人工智能软件的帮助下学习一种全新的编程环境的实例,之所以提人工智能软件而不是单指DeepSeek,一方面DeepSeek太火了,经常服务器繁忙,用本机本地部署的最多运行70b模型,又似乎稍差。另一方面也作为一个比较,除了DeepSeek,我还用豆包来学习,当然还有其他软件也是可以使用的。
另外,既然是学习试验,要选一个从来没有编过程序的语言和环境,当然还要有趣一点的才好。微信小程序的编程应该是一个比较合适的,一方面似乎能做的事情比较多,而且与手机操作系统无关,做出来的小程序在安卓下和苹果下都能运行。另外,一直想在手机下编程,仔细想想,除了早年在赛班系统下试着编过一个小软件后,确实没有在现在的智能手机下编过程序了,真是不可思议。
当然还需要找个例子来做,学习的最好办法就是实战。这里先用一个简单的记账程序入门,再来个稍微复杂的,后面有空再挑战能够与服务器交流的小程序。

2 微信小程序要如何开发?

这个问题直接问豆包吧:

请问微信小程序如何开发

微信小程序开发一般有以下步骤:

搭建开发环境

  • 前往微信官方网站下载适配电脑操作系统的微信开发者工具。

  • 打开微信开发者工具,使用微信账号登录,点击“新建项目”,填写项目名称、AppID(在微信公众平台申请)、项目目录等信息,即可创建项目。

编写代码

  • 页面布局:利用微信小程序提供的视图容器、文本组件、按钮组件等,在 .wxml 文件中进行页面结构搭建。如使用 标签作为容器, 标签显示文本, 标签创建按钮。

  • 样式设计:在 .wxss 文件中对页面进行样式设置,可使用类似于CSS的样式属性,如 width (宽度)、 height (高度)、 color (颜色)等。

  • 逻辑处理:在 .js 文件中实现页面的逻辑功能。例如,定义页面的数据对象,使用 Page() 函数注册页面,在函数内部通过 data 属性定义数据,通过 onLoad 等生命周期函数执行初始化操作,还可定义按钮点击等事件处理函数。

调试测试

  • 开发者工具调试:微信开发者工具提供断点调试、控制台输出等功能。可在代码中设置断点,当程序运行到断点处会暂停,方便查看变量值、程序执行路径等。

  • 真机测试:将小程序代码上传到微信公众平台,提交审核,审核通过后即可发布上线。在手机上体验小程序,查看在不同手机型号和系统上的兼容性和功能是否正常。

发布上线

  • 在微信开发者工具中,点击“上传”按钮,将代码上传到微信公众平台。

  • 登录微信公众平台,在“开发管理”-“开发版本”中找到上传的版本,提交审核。

  • 审核通过后,在“发布”页面点击“发布”按钮,小程序即可正式上线供用户使用。

这个说得比较清楚了,大概的意思就是要使用微信提供的微信开发者工具进行开发,下载安装后,微信开发者工具是这样的:
在这里插入图片描述点加号增加项目后,出现下面的界面:
在这里插入图片描述
项目名称以及项目目录根据情况设置,AppID得先注册一个,点击注册,打开浏览器,出现:
在这里插入图片描述
还得专门为一个小程序申请一个邮箱才行,这个是有限制的,豆包这样讲:
微信小程序申请有什么限制条件?
微信小程序申请的限制条件如下:

主体相关限制

  • 主体类型与数量:企业、政府、媒体、其他组织等非个体户类型公众号,一个月可复用资质注册5个小程序;个体户一个月只能复用资质注册1个小程序。

  • 个人主体功能:个人主体注册的小程序无微信支付功能,可选择的类目少,不能对接第三方技术,代码审核通过率低。

信息与资质限制

  • 邮箱:需使用未被个人微信号以及任何微信平台绑定的新邮箱。

  • 营业执照:企业或商家申请需提供营业执照,且小程序经营范围应与营业执照上的经营范围一致;其他类别的组织(如政府、NGO等),需提供组织机构代码证。

  • 特殊许可证:若小程序涉及售卖食品、药品、保健品等特殊类目,需准备相应的特殊许可证。

名称限制

  • 重名问题:小程序名称不能与已存在的小程序、公众号等重名,也不能使用微信平台限制的词汇。
  • 改名费用:小程序一次认证可永久使用,但每次改名则需要重新支付300元认证费用。

内容与功能限制

  • 内容规范:内容不得含有违法、低俗、暴力等内容,不得侵犯他人知识产权。
  • 功能正常:小程序的功能需正常、稳定,且不存在漏洞和安全隐患。

为何这么限制,估计是腾讯的管理政策,其实后面还有一个注意事项,那就是每个小程序要在工信部备案,这一步比较慢,得同步进行,否则你的小程序只有三个月的生命时间。
不管怎么说,你已经注册了一个小程序ID了,就可以用开发工具开发你的小程序,并且通过https://mp.weixin.qq.com进行管理了。
整个过程大概是用开发工具开发并上传到微信的服务器,然后在管理工具中提交审核,审核通过后可以提交发布。管理工具可以提供很多工具来看小程序在微信下的运行情况(比如多少人使用等等),称为WE分析。这似乎把小程序看成是一个网络内容提供服务者了,所以审核比较严格,有些类别还得有资质,因此跟做网站类似,还得在工信部备案,一定要说服自己,这都是小麻烦,不要影响在手机上编程序的热情。
每个小程序要指定一个微信号作为主管人员,当然这个号有实名认证要求,所有管理都是主管用微信扫二维码进行的。简单的小程序的审核速度还可以,虽然说1到7天可以审核通过,实际上工作日基本上当天能通过,晚上似乎也有人值班,不知道是不是机器人在通宵干活。

3 第一个小程序

微信开发者工具中,生成一个小程序工程之后,系统自动生成了一组文件。一个微信小程序的架构如下:
有一个app的对象,用来存放小程序相关的全局数据以及相应的代码,并指定小程序中的各个界面,可以指定一个初始界面,各个界面之间可以切换,任何地方可以用getApp得到app对象,然后访问全局数据和程序。
每个界面称为Page,描述界面上显示的内容,定义自己的数据结构,以及处理代码。每个界面由四个文件组成,一个.json文件,用来配置界面,一个.js或.ts文件,用来放界面相关的数据定义和代码定义,一个.wxml文件,用来存放界面内容的描述,可以出现各种HTML5的语句和控件,一个.wxss文件,用来存放界面上公共的控件属性,控件属性可以在.wxml中直接用class指定。这样一方面可以重用属性描述,另一方面让.wxml文件显得比较简洁。.wxml中的控件可以将动作绑定在.js或.ts文件中的页面函数上,这样用户进行操作时调用相应的函数,完成动作。四个文件相互支持,就可以完成需要的界面功能。界面中的数据可以来自于app全局数据,也可以来自于系统的其他方面,比如网络,存储器,剪裁板等等。还有界面调入,卸载等时机的回调函数,让你有初始化和退出的机会。
值得注意的是,微信小程序的接口似乎变化很快,很多本地部署的模型都无法得到最新的内容,用DeepSeek或豆包联网搜索好一点。
我们来做第一个小程序,这是三家人出去玩AA制的记账程序,由于每次不是现场结清,因此需要一个记录消费项目和备用金注入的软件,由于对微信小程序没有多少概念,我们尝试让人工智能生成整个程序。
这次来折腾豆包吧,下面是给豆包描述的软件需求:

1.三家人经常聚在一起玩,设立一个虚拟账户,并记录日常流水,随时可以进行账目核算和账目清算,三家可称为杨家,喻家和刘家。
2.日常流水包括两类,一类是备用金转入,包括日期,出资方,金额;另一类是消费记录,包括日期,出资方,金额,消费项目描述,其中出资方就是三家之一,
3.账目核算根据日常流水进行,每一项消费记录中的金额平均分摊到三家消费金额上,核算时每家有个出资汇总,消费汇总,每一家的出资汇总减去消费汇总就是每一家的余额。
4.账目清算时按照每一家的余额,正数退回,负数补足。

请编制一个微信小程序,完成下面功能:
1.主界面,显示三家名称和当前出资汇总,消费汇总和余额汇总,并提供备用金入账登记,消费登记,账目导出,账目导入,流水账查询等功能按钮
2.备用金入账登记功能,弹出一个对话框,输入日期(默认当天,可修改),出资方(在三家中选择),金额,有确认按钮可以确认备用金出资记录
3.消费登记弹出一个对话框,输入日期(默认当天,可修改),出资方,金额,消费项目描述,有确认按钮可以确认消费记录
4.流水账记录在文件f3.txt中,在每一笔流水账记入时更新文件内容,进入小程序时读入,流水账更新立即进行各家出资额汇总和消费汇总。
5.流水账查询到一个新的页面,显示流水账列表,并可以选择某一项进行修改,删除。该页面也有备用金入账登记和消费登记,账目导出,账目导入等功能按钮,还有一个返回按钮返回到主界面
6.账目导出就是将流水账转换成文本,拷贝到剪贴板上,方便用户比如通过微信粘贴发送
7.账目导入就是从剪贴板得到账目的文本格式然后导入到本地流水账

豆包的回答如下:

在这里插入图片描述
后面还有index.wxss文件,index.js文件, records.wxml,records.wxss, records.js文件等,似乎有点问题,比如内部的变量名中有汉字,有些汉字好像编译不过,全部改成英文就没有问题了。这个小程序后面修改后能够实用了,在日常生活中也算一个小工具。不过练手用的,又是自己专用,非常个性化,不具备通用价值,因此这里就不发布出来了。也没有花30块钱进行小程序注册,因此微信上也搜索不到。
最后调整后界面如下,当然功能也有变化,比如消费项目增加了消费参与人的选项,这样平时不见得三家都参加的项目也可以记录上去,虽然看起来比较简陋,其实还是蛮实用的,感兴趣的可以自己琢磨一个:
在这里插入图片描述
这里值得指出的是,人工智能在其中最大的好处是将一个新手引入到正常编程作业过程中,在编程过程中可以随时问很多编程语言以及环境相关的问题,得到的回答比直接到搜索引擎中去问好很多,应该是综合了很多网络搜索结果得到的,当然,不管怎么说,是因为网络上本来有很多相关的内容才行,因此应该鼓励大家网络上多写文章才行,否则人工智能也不能凭空变出知识吧。光靠腾讯的编程手册可能不够,我感觉人工智能光学习规则并不能作出例子,这点跟人一样,他的各种编程例子应该是学习了大量编程实例的基础上才可以的。

4 围棋打谱的例子

我一直想做一个自己的围棋打谱程序,网上有很多围棋打谱软件,总是感觉有不满意的地方。这次就用微信小程序方式来实现吧。这个例子要稍微复杂一点,要记住多个相关联的围棋谱数据,要知道围棋规则,要能够绘制自己特定的内容,而不是完全靠控件,还要能在手机上方便地编辑围棋棋谱,嗯,似乎还是有点麻烦的。
当然首先想到的是要能够由程序控制画一个棋盘吧,前面的感觉是小程序就想一个网页,用HTML好像不能画图啊。如何用软件控制画图呢,问问豆包吧:

请生成一个微信小程序,能够绘制19路围棋的棋盘

以下是一个简单的微信小程序代码示例,用于绘制 19 路围棋棋盘,包含 .wxml 、 .wxss 和 .js 文件的代码:

  1. pages/index/index.wxml (页面结构文件)

xml

  1. pages/index/index.wxss (页面样式文件,这里简单设置样式,可根据需求调整)

css

canvas {
border: 1px solid black;
}

  1. pages/index/index.js (页面逻辑文件,实现棋盘绘制)

javascript

Page({
onLoad: function () {
const query = wx.createSelectorQuery();
query.select(’#goBoard’).fields({ node: true, size: true }, (res) => {
const canvas = res.node;
const ctx = canvas.getContext(‘2d’);
const width = res.width;
const height = res.height;

  // 计算每个交叉点的间距
  const gridSize = Math.min(width / 19, height / 19);

  // 绘制横线
  for (let i = 0; i < 19; i++) {
    ctx.beginPath();
    ctx.moveTo(0, i * gridSize);
    ctx.lineTo(width, i * gridSize);
    ctx.stroke();
  }

  // 绘制竖线
  for (let i = 0; i < 19; i++) {
    ctx.beginPath();
    ctx.moveTo(i * gridSize, 0);
    ctx.lineTo(i * gridSize, height);
    ctx.stroke();
  }

  // 绘制星位(可根据需求调整星位的绘制)
  const starPositions = [[3, 3], [3, 15], [15, 3], [15, 15], [9, 9]];
  starPositions.forEach(([x, y]) => {
    const starX = x * gridSize;
    const starY = y * gridSize;
    ctx.beginPath();
    ctx.arc(starX, starY, 3, 0, 2 * Math.PI);
    ctx.fill();
  });
}).exec();

}
});

将上述代码分别放置在对应的文件中,然后在微信开发者工具中运行该小程序,就可以看到绘制出来的 19 路围棋棋盘了。请注意,这只是一个简单的绘制示例,实际应用中可能还需要添加更多的交互功能等。

通过豆包这个例子,似乎学会了不少东西,比如如何在微信小程序中生成画布,然后在上面绘制东西(类似于Windows下编程的GDI?),当然,问题还是不少的,不过通过迭代,不断可以改进的。
然后来设计复杂点的数据结构,当然这个过程中通过DeepSeek和豆包问了很多初学者的问题,比如数字,字符串,数组,映射,树状数据结构等数据结构如何转换,如何遍历,如何增删改,慢慢才形成了最后的数据结构。
再然后学习如何组织界面,包括如何组织按钮,输入文字,显示对话框,显示列表,显示富文本,包括剪裁板操作,本地文件存取,如何将界面元素与数据关联,界面元素与代码关联等等。
可以想象如果没有人工智能软件的支持,这个学习过程会比较漫长,至少也得21天从入门到精通吧。但是在人工智能支持下,只不过两三天,就基本上迭代完成,应该算是学会了微信小程序的主体框架,细节部分不敢说精通。然而为何要事事精通呢,不懂的问题随时问DeepSeek或者豆包就是了,不用一次性全部学会吧。因此将来考察一个工程师能力的关键可能知识面的重要性可能要往后靠了,更关键的可能是如何提出合适的问题,然后从AI这里得到和判别合适的答案,与人工智能沟通得到更好的结果。这是传说中除了智商,情商之外的的搜商么?这个要求其实还是比较高的,要不是前面有多年的编程经验,估计问不出合适问题的。如何培养这个能力?似乎没什么好办法欸,不过基础的数据结构,算法似乎还是得学习。早年流行的著名公式
软件=数据结构+算法
似乎又可以流行了,语言及编程环境可以随时问人工智能嘛。当然,个人认为还是需要精通一门计算机语言才好,否则编程的概念很难建立起来。
不管如何,现在学编程要容易得多了,直接有个不知疲倦,不会不耐烦的老师随时待命啊,想当年为了找个资料,坐一天一夜火车到北京中关村买本编程资料的日子,现在何等幸福,恨不晚点生啊!
考虑到围棋打谱应该是一个比较简单的应用程序,只在一个页面就可以完成,因此就把所有数据代码放在一个页面中,app中也只是指定一个页面。因此废话少说,下面直接放出相关的设计内容,中间与人工智能的交互就不赘述了,其实这个过程中与人工智能交互是非常多的,感觉将来用老的比如c语言编程时,也可以得到人工智能帮助了。
这次主要是DeepSeek和豆包,本地部署了一个70b的大模型没有怎么用上,感觉此时人工智能是作为一个能够理解自然语言的搜索引擎加内容综合整理的工具来用了。由于网络上内容发展得快,大模型训练更新速度慢,没有网络上的知识支持,光凭大模型本身可能也不够,特别是本地部署的模型,知识更新更慢,因此更加需要网络搜索和本地知识库来支持了。希望在人工智能时代,网络上的内容不要很快充斥大量的AI生成的内容,还是要鼓励原创的内容出现,这样才能助力人工智能的发展。

4.1 操作界面

经过几次迭代,最终的界面如下(华为手机鸿蒙系统下的截图,苹果手机似乎下面的树状图稍微有点区别,由于树状图是用制表符号生成的富文本做的,可能不同系统下的字库不同导致的):
华为鸿蒙系统下的截图:
华为手机鸿蒙系统
苹果手机截图:
苹果手机IOS系统

其中,晓敏定式是本小程序的名称。注意需要选择一个有个性化点的名字,避免与其他小程序重名。另外如果包括围棋之类的通用字样,还需要提供相关的资质(围棋培训老师资质?老是看到柯洁摆路边摊做围棋培训,不知道他有没有这种资质)。个人用户交30块钱注册后,可以让全网在微信小程序中找到这个小程序,如果是自己使用或者几个朋友内部使用,那就不用注册,直接通过小程序码分享就可以了。
现在可以在微信小程序中通过找晓敏定式这个名字找到这个小程序了。对应的小程序码不能在这里贴出来,CSDN说是违规了,不知道什么情况。
这个界面上面是一个棋盘,为了方便找到坐标,横坐标显示没有采用常用的ABCDE FGHJK LMNO PQST坐标显示,而是直接用1…19显示了。
棋盘下面是功能按钮,具体功能说明选中棋谱后会显示在最下面的信息框中:


基本功能:
前后:可以查看当前定式的前后落子
增:在当前选择的节点下增加一个子节点
删:删除当前节点以及它的子节点
改:修改当前节点的名称等
①:设置当前节点以当前落子位置为第一个序号起始点
拷:将当前节点及其子节点数据拷贝到剪裁板
贴:从剪裁板得到节点数据,作为子节点加到当前节点下面
落:当前选择的落子点加到当前选择节点的当前位置,落子操作只能在选中叶节点时进行
点击棋盘:设置落子点位置

再下面是显示当前步数和总步数的信息,也显示了当前棋谱节点的名称。
再下面是一个棋谱中所有谱的列表,这是一个树状结构,显示各个棋谱节点的名称及父子关系。每个节点的子节点可以展开和关闭,子节点的棋是跟着父节点往后下的。可以点击棋谱中的某一行,选中某个棋谱。此时最下面的信息框中显示当前棋谱的信息。
最下面的信息框中显示当前选中节点的情况,前面时当前节点从根节点开始的全路径名,有点像电脑上的文件全路径名。

4.2 数据结构

我们设计了一个全局的棋谱数据结构,每个节点的设计如下(后面会给出每个设计文件代码):

// 定义棋谱节点的数据结构
export type QPNode = {
  i: number; /*节点ID */
  p: number; /*父节点ID */
  f: number; /*①标志位置,相对于该节点起点 */
  n: string; /*节点名称 */
  d: string; /*节点描述 */
  s: [number, number][]; /*节点中落子位置 */
  t: number; /*节点是否展开 */
  y: {n:string; /*节点特别标志名称 */
      d:[number, number] /*节点特别标志位置 */
     }[];
};

type QPData = { [key: number]: QPNode }; /*棋谱是由若干节点构成的,使用它的ID号作为关键字组成一个映射 */

这里每个节点靠ID号唯一标识,按照父节点ID号来形成一个树状结构,所有的节点放在一个映射中,用ID号作为KEY。
对这个数据定义几个通用的操作,详细实现看后面的完整的源代码:

export class QiPuManager {
  private qpData: QPData; /*棋谱数据*/
  constructor(); /*构造函数,生成根节点,填入默认的示例棋谱*/
  getFullPath(itemid); /*给出指定节点的全路径名*/
  addChild(obj, pid):number; /*在指定的父节点下添加子节点*/
  countChildren(itemid): number;/*计算给定节点的子节点数量,如果为0,表明该节点是一个叶节点*/
  idUsable() : number;/*给出该棋谱集合中可以用的ID号,不会与当前的节点重复*/
  isDescendant(itemid, descendid) : boolean; /*判断一个节点是否为另一个节点的子孙节点*/
}

界面中数据结构如下:

import { QiPuManager, QPNode } from '../../utils/wqqp_data';
Page({

  data: {
    canvasWidth: 65, // 默认宽度,可以根据需要调整
    size : 19, // 棋盘大小为19x19
    xstart : 10,
    ystart : 10,
    qp : [],  /*棋谱的数据*/
    
    /*最下面的信息显示控件关联的数据*/
    dispInfo: "请选择一个项目",

	/*棋谱树关联的数据*/
    listData: [
      {},
    ],

	/*棋谱树中选中的项目id和名称*/
    selectid: 0,
    curname: "围棋",
     
    /*选中节点后,从根节点开始的各个走棋步子,包括坐标和显示序号,序号为0标识不显示*/
    setA: [],
   
   /*通过前后控制当前走的步数,这些步数根据围棋规则加入到W集合和B集合中,当然如果出现提子,则从相应的集合中删除,每个元素包括坐标和序号*/
    setW: [],
    setB: [],
	
	/*当前节点的总步数和当前走到的步数*/
    totalsteps: 0,
    cursteps: 0,
	
	/*围棋规则中,如果某一步提了对方一子,那么这个子的位置对方在下一步是不能走的,这就是打劫的来由,避免了直接交替提子的情况,
	这里记录了这个点的位置,如果是[0,0],表示没有禁止落子点*/
    disablepos: [0, 0],
    /*落子点,用户点击了棋盘时,记录下点击点位置,作为用户选择落子功能时的位置,在棋盘上用一个棋子加三角形显示*/
    clickpos: [0, 0],

	/*增加子节点时,输入新的子节点名称的隐藏界面关联的数据*/
    isInputNameModalShow:false,
    inputName:"",

    /*修改子节点时,修改界面关联的数据*/
    isEditModalShow: false,
    editName:"",
    editDesc:"",
    editFlags:""
  },

这个界面实际包括两个隐藏界面,就是增加子节点的名称输入界面和修改子节点的修改界面。需要时用程序修改打开标志,就可以显示隐藏界面,用完后可以设置标志,又进入隐藏状态。
其中修改子节点的修改界面如下:
在这里插入图片描述
其中名称,描述和标签都可以修改。标签一栏中,可以输入多行标签,每一行包括三个部分,用逗号隔开,第一部分是显示的内容,后面两个部分是坐标,注意坐标必须在1–19之间,显示内容最好只有一个字符(包括汉字),软件中处理时考虑了全角半角逗号兼容的情况,格式不对和坐标不对的行会被自动丢弃。

4.3 棋盘绘制

棋盘绘制在drawBoard函数中实现,棋盘内容修改后就调用该函数进行重新绘制。流程如下:

得到画布,计算画布宽度和围棋网格间距离
绘制背景颜色(可选)
绘制棋盘线及坐标刻度
绘制星,天元等九个棋盘特别位置
绘制setB和setW中的棋子,如果需要绘制序号,则绘制棋子上的序号
绘制标志字符
如果禁止点有效,则绘制禁止点
如果落子点有效,则绘制落子点

具体实现见后面的代码,这里涉及到图片显示,矩形填充,字符显示,绘制线段等内容,都是通过与豆包交流后得到的示例代码修改得到的。

4.4 树状数据的显示及处理

树状数据的显示采用比较简单的文本显示方式。大概的办法是,从树根开始把数据加入进来,每次先加入当前节点,然后是它的子节点,用一个递归函数实现,最后生成一个列表中。这个列表每一项用一个富文本控件显示,并将每一项与棋谱中的id号关联起来。选中列表中的某一项时,就调用相应的函数,更新当前选择棋谱内容即可。界面处理部分如下:

<scroll-view class="listview"
   scroll-y="true" scroll-x="true">
   <view wx:for="{{listData}}"
         wx:key="index"
         bindtap="handleItemTap"
         data-item="{{item}}"> 
         <view class ="list-item">
            <rich-text nodes="{{item.name}}"> </rich-text>
         </view>
    </view>
</scroll-view>

注意到这是一个水平方向和垂直方向都带滚动条的控件,中间是若干个富文本控件,富文本内部不自动换行,内容放在listData中,我们必须在棋谱有变化时重新生成这个数据。这个列表点击后调用handleItemTap用来完成用户选择棋谱项目的变化,并切换当前项的子节点是否展开。listData的生成代码如下:

updateItem(displayData, itemid, ident, isfirst, islast) {
    let node:{} = {};
    let childrencount = 0;
    const item = this.data.qp.qpData[itemid];
    if (!item)
      return;
    const tabs = "┌┬┐├┼┤└┴┘│─";
    Object.entries(this.data.qp.qpData).forEach(([key, value]) => {
      if (value.p == itemid) {
        childrencount += 1;
      }
    });
    let name = "<p style='";
    if (item.i == this.data.selectid) {
      name += " color: #1ff;'>";
    } else {
      name += " color: #fff;'>";
    }
    if (islast) {
      name += ident + tabs.charAt(6);
    } else if (isfirst) {
      name += ident + tabs.charAt(3);
    } else {
      name += ident + tabs.charAt(3);
    }

    if (childrencount > 0) {
      if (item.t == 1) name += '┬'; else name += '+';
    } 
    name += item.n;
    node.name = name + "</p>";
    node.itemid = item.i;
    node.itemname = item.n;
    displayData.push(node);
    if (item.t == 1) {
      let childrenindex = 0;
      Object.entries(this.data.qp.qpData).forEach(([key, value]) => {
        if (value.p == itemid) {
          this.updateItem(displayData, value.i, islast?(ident + "&emsp;"):(ident+tabs.charAt(9)), 
            childrenindex == 0, childrenindex == (childrencount - 1));
          childrenindex += 1;
        }
      });
    }
  }

基本的做法是,用递归的办法来将整个棋谱的加入,当然只加入展开的部分。具体调用在棋谱变化和选择项变化(影响是否展开)进行。调用如下:

let displayData:[] = [];
this.updateItem(displayData, 0, "", true, true);
this.setData({listData: displayData});

最后一句是微信小程序中通知控件更新的代码。
递归过程是先加入指定项,然后如果当前项是展开的,就加入它的每个子项。
加入项时指出前导字符串,以及加入的项是不是同父项下的第一项和最后一项。如果是当前选中项,颜色用color: #1ff,其他项用color: #fff,这样能够高亮显示当前项。最后一项前面加个└,第一项和中间项前面加个├字符,如果有子项,那么展开展开状态加┬字符,非展开状态加+字符,然后加当前项的名称,这样当前项就准备完成,把准备好的富文本与项的ID号和名字组成一个节点,加入到listData中,供上面的树状数据界面代码中使用。

加入各个子项时除了要给出是否为第一项还是最后一项,还需要更新前导字符串。前导字符串如果是最后一项,则在输入的前导字符串后面加一个空格,注意空格的显示宽度不同字体下不一样,这可能导致不同系统中稍有差别。由于不是每个系统下的字体都有等宽字体,问豆包也每什么好办法,将来再找机会修改吧,不是最后一项则增加一个│字符,让中间项的子项前面都有向下的竖线,表示还有其他叔伯项。
注意选择列表控件中的字体和高度时,字体大小不能比项目高度小,否则导致树中的上下竖线无法连接在一起。因此目前的wxss是这么设置的:

.list-item {
  display: flex;
  align-items: center;
  font-size: 18px;
  height : 18px;
  padding: 0px;
}

这样处理后,就能显示一个树状数据列表了,而且在点击某一项时,还能切换点击项目是否展开:

handleItemTap: function(e) {
  const item = e.currentTarget.dataset.item;
  let dataitem = this.data.qp.qpData[item.itemid];
  dataitem.t = 1 - dataitem.t;
  this.setData({selectid: dataitem.i});
  this.setData({curname: dataitem.n});
  this.data.clickpos[0] = 0;
  this.data.clickpos[1] = 0;  
  this.updateSelect();
}, 

其中的.t成员就是开合标志。用 dataitem.t = 1 - dataitem.t;可以切换选中项是否展开。updateSelect中调用了上面的代码重新生成listData。
updateSelect还实现可当前选择的棋谱的棋子下法的收集,重新生成setA,这部分在updateA中实现,也是一个递归过程:

updateSelect() : boolean {
    let displayData:[] = [];
    this.updateItem(displayData, 0, "", true, true);
    this.data.setB = [];
    this.data.setW = [];
    this.data.setA = [];
    const obj = this.data.qp.qpData[this.data.selectid];
    
    this.updateA(this.data.selectid);
    this.setData({totalsteps: this.data.setA.length});
    this.setData({cursteps: this.data.setA.length});
    this.setData({listData: displayData});
    this.setData({dispInfo: this.data.qp.getFullPath(obj.i) + ": " + obj.d + "(" +this.data.qp.countChildren(obj.i)+")"});
    return this.updateData();
  },
 
  updateA(id) {
    const obj = this.data.qp.qpData[id];
    if (obj) {
      for (let i = 0;i<obj.s.length;i++) {
        const oldobj = obj.s[obj.s.length-1-i];
        const newobj = [oldobj[0], oldobj[1], -1];
        this.data.setA.unshift(newobj);
      }
      if (obj.p > 0)
        this.updateA(obj.p);
    }
  },

其实就是先把当前的节点中的落子按后到前的顺序加到setA的最前面(unshift实现),然后递归调用updateA将父节点的走法也加到setA的前面,这样就形成一个从根节点开始的走法数组,其中的第3个元素表示显示序号的值,这里默认-1表示不显示序号,后面再通过节点的序号相对位置进行修正:

const obj = this.data.qp.qpData[this.data.selectid];
    if (obj) {
      const alen = this.data.setA.length;
      const flagpos = obj.f + alen - obj.s.length;
      for (let i = 0;i<alen;i++) {
        if (i < flagpos)
          this.data.setA[i][2] = 0;
        else 
          this.data.setA[i][2] = i - flagpos + 1;
      }
    }

这段代码在updateData的实现中。
这样,选择一项后我们就收集到该项对应的棋谱的所有走法位置以及显示序号的相关数据了。

4.5 围棋规则的实现

我们有了棋谱,就可以让用户通过前/后选择走到某一步。用户选择后,内部从头开始模拟围棋规则,将每一步的走法增加到setB和setW中,如果导致提子,还要从setB和setW中去掉需要提的棋子。如果用户选择落子,还要判断落子位置是否合乎围棋规则,当然如果从剪裁板贴入棋谱时,需要检查每一步是否合法,这个检查时通过调用updateBW来完成的,下面是调用代码:

updateData() : boolean {
    const obj = this.data.qp.qpData[this.data.selectid];
    if (obj) {
      const alen = this.data.setA.length;
      const flagpos = obj.f + alen - obj.s.length;
      for (let i = 0;i<alen;i++) {
        if (i < flagpos)
          this.data.setA[i][2] = 0;
        else 
          this.data.setA[i][2] = i - flagpos + 1;
      }
    }
    const ret = this.updateBW();
    if (!ret) {
      this.data.setA.pop();
    }
    this.drawBoard();
    return ret;
  },

注意到其中如果updateBW返回走法非法时,还需要从setA中删除,将来就不会走到这一步了。updateBW的过程如下:

updateBW():boolean{
    this.data.setB = [];
    this.data.setW = [];
    let i = 0;
    for (i = 0;i < this.data.cursteps;i++) {
      if (!this.checkBW(i, this.data.setA[i])) {
        this.setData({cursteps: i});
        return false;
      }
    }
    return true;
  }

具体的过程是:清空setB和setW,然后从0开始到cursteps(用户选择的最后一步位置),调用checkBW,检查该步是否合法。如果某一步非法则修改当前cursteps,返回非法。如果每一步都合法,则返回合法。下面是checkBW的过程:

checkBW(step, pos):boolean {
落子位置是否在棋盘内
如果禁着点有效,落子位置是否为禁着点
落子位置处是否已经有子
落子是否造成对方提子
如果没有导致对方提子,则判断是否判断本方的某一块提子(自己找死在围棋中也是不允许的),如果导致本方提子,则该步也非法。
如果落子导致对方提掉一子,则该子的位置禁止对方落子,记录了这个位置为禁着点
如果没有提子或者提的子不只一颗,那就取消禁止对方落子
没问题就执行落子,将相应的位置加到setB或setW中(假设黑方先行,偶数步加到setB中,奇数步就加到setW中,注意从0开始算步数)
}

判断是否造成提子并返回提子数目是算法的关键,实现在checkRemoveBlock中,具体过程如下,注意第三个参数表示是否确实提子,也代表是判断对方提子还是判断本方提子:

checkRemoveBlock(step, pos, doremove) {
先做预落子,将给出的位置根据步数加到setB或setW中,
然后根据doremove将对方或本方的棋子收集到临时的数组other中,后面就考虑other中每一块的气是否为0,如果为0则提子
具体的办法是,循环操作,知道other为空集合,操作如下:
找到other中第一个子所在的块
将ohter的第一个子取下来,放在block中,
然后检查other中剩余的每个子,看它是否跟block中的每个子是否相连(同一水平线左右相邻或者同一垂直线上下相邻),如果相连接,则从other中取下来,加入到block中,最终从other中取出了一个相连的块block。
然后判断这个块是否是死棋,也就是它的所有元素的相邻位置至少有一个没有落子的位置就是活的,否则就是死的。
如果这个block是死的,按照doremove判断如果是检查是否导致本方提子,就可以直接返回非法了(返回提子1就行,外部来做判断)。
如果这个block是死的,而且正在检查对方是否提子,此时就累加加提子个数,记录block中的第一个子位置为禁着点位置(如果只提一子,那就是这个位置了,否则外部会取消禁着点)
在对应的setW和setB中删除所有block中的子,执行提子动作
继续循环
循环结束则恢复预落子,返回提子数目。实际的落子由外部来做。
}

这样,我们就实现了围棋的落子规则,当然没有实现判断输赢的规则,这个将来有机会再来实现好了。

4.6 与剪裁板交互数据

我们提供了拷和贴两个功能。拷用来将选中的棋谱节点及其子节点拷贝到剪裁板。注意是文本格式,用户可以通过剪裁板把内容记录下来,通过微信或者其他工具记录或者共享自己的棋谱。也可以在某个文本工具中对棋谱进行修改,然后贴回到打谱软件中来。注意,某些系统对剪裁板的内容大小做了限制,因此选中的节点及子节点个数不要太多,可能导致拷贝失败。下面是一个选中了手筋死活下面的黄莺扑蝶拷贝到剪裁板的例子:

[
	{
		"i":30,
		"p":29,
		"f":0,
		"n":"黄莺扑蝶",
		"d":"黄莺扑蝶",
		"s":[
			[4,16],[4,17],[5,16],[5,17],[6,17],
			[6,18],[7,17],[3,16],[3,17],[3,15],
			[4,15],[3,14],[4,14],[3,13],[4,13],
			[4,12],[2,16],[3,12],[2,17],[6,16],
			[8,16],[6,15],[16,16],[6,13]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":31,
		"p":30,
		"f":0,
		"n":"失败1",
		"d":"失败1",
		"s":[
			[4,18],[5,18],[3,18],[7,18]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":32,
		"p":30,
		"f":0,
		"n":"失败2",
		"d":"失败2",
		"s":[
			[5,18],[4,18],[3,18],[5,19],[3,19],
			[4,19]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":33,
		"p":30,
		"f":0,
		"n":"成功",
		"d":"以下就是紧气,始终保持白棋只有两口气就可以",
		"s":[
			[5,19]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":34,
		"p":33,
		"f":-1,
		"n":"成功1",
		"d":"成功1",
		"s":[
			[5,18],[7,18],[6,19],[7,19],[4,19],
			[3,18],[3,19],[4,18]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":35,
		"p":33,
		"f":-1,
		"n":"成功2",
		"d":"成功2",
		"s":[
			[3,18],[4,19]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":36,
		"p":35,
		"f":-3,
		"n":"成功21",
		"d":"成功21",
		"s":[
			[7,18],[5,18],[4,18],[2,18]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":37,
		"p":35,
		"f":-3,
		"n":"成功22",
		"d":"成功22",
		"s":[
			[2,18],[4,18],[5,18],[7,18]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":38,
		"p":35,
		"f":-3,
		"n":"成功23",
		"d":"成功23",
		"s":[
			[4,18],[2,18],[3,19],[2,19],[5,18],
			[7,18],[6,19],[7,19]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":39,
		"p":35,
		"f":-3,
		"n":"成功24",
		"d":"成功24",
		"s":[
			[3,19],[2,18],[5,18],[7,18],[4,18],
			[2,19],[6,19],[7,19]
		],
		"t":1,
		"y":[
		]
	},

	{
		"i":40,
		"p":35,
		"f":-3,
		"n":"成功25",
		"d":"成功25",
		"s":[
			[5,18],[7,18],[4,18],[2,18],[3,19],
			[2,19],[6,19],[7,19]
		],
		"t":1,
		"y":[
		]
	}

]

注意其中的格式非常重要,各种括号的对应,引号,冒号和逗号都必须是半角形式,字符串周围必须是引号,数字则不能有。如果格式非法,贴回去就会报格式错误。哪怕格式正确,也必须保证每一项的i,p,f,n,d,s,t,y分量都齐全。另外,必须保证父子关系正确,ID号在一个文本中不重复。

4.6.1拷贝过程

拷贝过程先将当前选中节点及子节点转变成字符串,这是一个递归过程,从选中的节点开始,先输出当前节点,然后逐个输出其子节点:

genNode(id):String {
  let data = "";
  let i = 0;
  const value = this.data.qp.qpData[id];
  if (!value) return "";
  /* 先生成当前节点的输出字符串 */
  data += "\t{\n";
  data += '\t\t"i":'+String(value.i) + ',\n';
  data += '\t\t"p":'+String(value.p) + ',\n';
  data += '\t\t"f":'+String(value.f) + ',\n';
  data += '\t\t"n":"'+value.n + '",\n';
  data += '\t\t"d":"'+value.d + '",\n';
  data += '\t\t"s":[\n';
  for (i = 0;i<value.s.length;i++) {
    if (i % 5 == 0) {
      data += "\t\t\t";
    }
    data += "[" + String(value.s[i][0]) + ","+ String(value.s[i][1]) + "]";
    if (i != value.s.length-1) {
      data += ",";
    }
    if (i % 5 == 4) {
      data += "\n";
    }
  }
  if (i % 5 != 0) {
    data += "\n";
  }
  data += "\t\t],\n";
  data += '\t\t"t":1,\n';
  data += '\t\t"y":[\n';
  for (i = 0;i<value.y.length;i++) {
    data += '\t\t\t{"n":"' + value.y[i].n + '","d":['+ String(value.y[i].d[0])  + ','+ String(value.y[i].d[1])+ ']}';
    if (i != value.y.length-1) {
      data += ",";
    }
    data += "\n";
  }
  if (i % 5 != 0) {
    data += "\n";
  }
  data += "\t\t]\n\t}";
  /*再生成其每个子节点的字符串*/
  Object.entries(this.data.qp.qpData).forEach(([key, v]) => {
    if (v.p == id) {
      data += ",\n\n";
      data += this.genNode(v.i);
    }
  });
  return data;
},

然后再将字符串拷贝到剪裁板中,再提示用户是否成功(这就是拷按钮的相应过程):

button_copy(): void {
  let data = "[\n";
  data += this.genNode(this.data.selectid);
  data += "\n\n]\n\r";
  wx.setClipboardData({
    data: data,
    success: function (res) {
      wx.showToast({
        title: '复制成功',
        icon: 'success'
      }); 
    },
    fail: function(err) {
      wx.showToast({
        title: '复制失败',
        icon: 'fail'
      }); 
    }
  })  
},

4.6.2 贴回过程

贴回过程的处理稍微复杂一点。 在贴回过程中,作为当前选中节点的子节点增加到当前选中节点之下。此时要求贴回的内容中第一个元素是所有其他元素的祖先节点,此时每个节点的ID号可能会有变化(因为来源不一样,可能导致ID号与现有的项目重复)。虽然内容复杂,代码却比较稍,因为从字符串变换成对象的过程简单了:

addChildren(objarray, obj, pid): void {
  const objid = obj.i;
  const newid = this.data.qp.addChild(obj, pid);
  for (let i = 0;i<objarray.length;i++) {
    if (objarray[i].p == objid) {
      this.addChildren(objarray, objarray[i], newid);
    }
  }
},
button_paste(): void {
  wx.getClipboardData()
  .then((res) => {
    //console.log(res.data);
    const obj = JSON.parse(res.data);
    if (obj.length == 0)
      return ;
    //console.log(this.data.selectid, obj[0], obj);
    this.addChildren(obj, obj[0], this.data.selectid);
    this.saveChanged();
    this.updateSelect();
  })
  .catch((err) => {
    console.log(err.message);
    this.setData({dispInfo: "格式错误:" + err.message});
    wx.showToast({
      title: '格式错误',
      icon: 'fail'
    }); 
  })
},

主要也是通过addChildren递归调用实现的。注意其中调用了addChild,这个过程中为确保外部加入的项ID号的正确性,给每个增加的节点分配一个新的id号。后面注意区分这个过程分配的id号和新生成的id号,以便维持贴回的诸节点之间的父子关系。下面是addChild的实现:

addChild(obj, pid):number {
    let item : QPNode = {
      "i": 0,
      "p": pid,
      "f": obj.f,
      "n": obj.n,
      "d": obj.d,
      "s": obj.s,
      "t": 1,
      "y": obj.y
    };
    item.i = this.idUsable();
    this.qpData[item.i] = item;
    return item.i;
  }
  idUsable() : number {
    for (let i = 1;i<10000;i++) {
      if (!this.qpData[i]) {
        return i;
      }
    }
    return 0;
  }

注意其中idUsable从1到10000找一个当前没有用过的id号作为下一个可用id号,按说这个小程序中不至于存放超过10000个节点吧,因此应该问题不大。

4.7 数据存储

我们将当前的棋谱数据存放在小程序的用户数据目录下,注意到微信小程序对文件系统的访问有限制,其中之余就是只能访问微信系统指定目录下的文件,因此不可能让用户指定存取其文件系统的文件,这样我们只能选择把当前的数据放在用户数据目录下的一个固定文件中,这个文件名是dsqp.txt,注意到这个数据目录是AppID隔离的,因此不会与其他小程序造成重名,应该还是安全的。文件中的内容其实就是棋谱的内容,不过是以映射的格式存放的。如果一开始用户文件系统中没有这个文件,那就是一个系统预生成示例棋谱内容。
存储过程比较简单,它在每次棋谱变化时被调用,以实现数据存储。

saveChanged() {
  const data = JSON.stringify(this.data.qp.qpData);
  //return;
  wx.getFileSystemManager().writeFile({
      filePath: wx.env.USER_DATA_PATH + '/dsqp.txt',
      data: data,
      encoding: 'utf8'
  });
},

在小程序页面调入时,会尝试从该文件中读入数据:

onReady() {
    const that = this;
    this.data.qp = new QiPuManager();
    this.getCanvasWidth(); 
    wx.getFileSystemManager().readFile({
      filePath: wx.env.USER_DATA_PATH + '/dsqp.txt',
      encoding: 'utf8',
      success: (res) => {
        that.data.qp.qpData = JSON.parse(res.data);
        that.updateSelect();
        console.log("文件已经载入");
      },
      fail: () => {
        that.updateSelect();
        console.log("文件载入错误");
      }
    });
  },

如果文件不存在,或者格式不对,则用数据生成时生成的示例数据。具体看源代码。

4.8 完整的源代码

更多的内容这里就不多解释了。这里给出源代码,感兴趣的可以自己建立一个微信小程序工程玩玩,编程序重在动手,我相信只要动手,就会有收获。任何人可以任意使用这些代码,当然如果能够提一下出处就感谢了。如果用出什么问题本人申请免责,将来版本更新后这里的代码可能不会更新。有什么建议或者有什么新棋谱可以给这个小程序的专用邮箱发邮件:rao73120996@163.com,我可能会在将来的更新中将有意义的棋谱或建议修订在新的版本中。
其中几个定式示例素材来自于江维杰主编的【AI围棋定式大全】。抄的不一定准确,见谅了。如果认为知识产权受到侵犯请邮件通知我,我将及时在新版本中处理。黄莺扑蝶的例子来自于抖音的一个视频中,引用一下多谢了。

黄莺扑蝶的出处:
黄莺扑蝶的出处

定式示例的出处:
定式示例的出处

源代码结构如下:
在这里插入图片描述

这里只给出其中board.json,board.ts, board.wxml, board.wxss, wqqp_data.ts的内容black.jpg和white.jpg直接贴在下面。
在这里插入图片描述

这是black.jpg,为了避免CSDN打水印影响效果,我人为将大小扩大了点,实际的图是64x64像素的。
在这里插入图片描述
white.jpg。这两个图来自于野狐围棋这个app的界面截图,如果你不满意,自己做一个也行。

board.json:

{
  "usingComponents": {
      
  }
}

board.ts

import { QiPuManager, QPNode } from '../../utils/wqqp_data';
Page({

  data: {
    canvasWidth: 65, // 默认宽度,可以根据需要调整
    size : 19, // 棋盘大小为19x19
    xstart : 10,
    ystart : 10,
    qp : [],  /*棋谱的数据*/
    
    /*最下面的信息显示控件关联的数据*/
    dispInfo: "请选择一个项目",

	/*棋谱树关联的数据*/
    listData: [
      {},
    ],

	/*棋谱树中选中的项目id和名称*/
    selectid: 0,
    curname: "围棋",
     
    /*选中节点后,从根节点开始的各个走棋步子,包括坐标和显示序号,序号为0标识不显示*/
    setA: [],
   
   /*通过前后控制当前走的步数,这些步数根据围棋规则加入到W集合和B集合中,当然如果出现提子,则从相应的集合中删除,每个元素包括坐标和序号*/
    setW: [],
    setB: [],
	
	/*当前节点的总步数和当前走到的步数*/
    totalsteps: 0,
    cursteps: 0,
	
	/*围棋规则中,如果某一步提了对方一子,那么这个子的位置对方在下一步是不能走的,这就是打劫的来由,避免了直接交替提子的情况,
	这里记录了这个点的位置,如果是[0,0],表示没有禁止落子点*/
    disablepos: [0, 0],
    /*落子点,用户点击了棋盘时,记录下点击点位置,作为用户选择落子功能时的位置,在棋盘上用一个棋子加三角形显示*/
    clickpos: [0, 0],

	/*增加子节点时,输入新的子节点名称的隐藏界面关联的数据*/
    isInputNameModalShow:false,
    inputName:"",

    /*修改子节点时,修改界面关联的数据*/
    isEditModalShow: false,
    editName:"",
    editDesc:"",
    editFlags:""
  },

  onReady() {
    const that = this;
    this.data.qp = new QiPuManager();
    this.getCanvasWidth(); 
    wx.getFileSystemManager().readFile({
      filePath: wx.env.USER_DATA_PATH + '/dsqp.txt',
      encoding: 'utf8',
      success: (res) => {
        that.data.qp.qpData = JSON.parse(res.data);
        that.updateSelect();
        console.log("文件已经载入");
      },
      fail: () => {
        that.updateSelect();
        console.log("文件载入错误");
      }
    });
  },

  updateItem(displayData, itemid, ident, isfirst, islast) {
    let node:{} = {};
    let childrencount = 0;
    const item = this.data.qp.qpData[itemid];
    if (!item)
      return;
    const tabs = "┌┬┐├┼┤└┴┘│─";
    Object.entries(this.data.qp.qpData).forEach(([key, value]) => {
      if (value.p == itemid) {
        childrencount += 1;
      }
    });
    let name = "<p style='";
    if (item.i == this.data.selectid) {
      name += " color: #1ff;'>";
    } else {
      name += " color: #fff;'>";
    }
    if (islast) {
      name += ident + tabs.charAt(6);
    } else if (isfirst) {
      name += ident + tabs.charAt(3);
    } else {
      name += ident + tabs.charAt(3);
    }

    if (childrencount > 0) {
      if (item.t == 1) name += '┬'; else name += '+';
    } 
    name += item.n;
    node.name = name + "</p>";
    node.itemid = item.i;
    node.itemname = item.n;
    displayData.push(node);
    if (item.t == 1) {
      let childrenindex = 0;
      Object.entries(this.data.qp.qpData).forEach(([key, value]) => {
        if (value.p == itemid) {
          this.updateItem(displayData, value.i, islast?(ident + "&emsp;"):(ident+tabs.charAt(9)), 
            childrenindex == 0, childrenindex == (childrencount - 1));
          childrenindex += 1;
        }
      });
    }
  },

  updateA(id) {
    const obj = this.data.qp.qpData[id];
    if (obj) {
      for (let i = 0;i<obj.s.length;i++) {
        const oldobj = obj.s[obj.s.length-1-i];
        const newobj = [oldobj[0], oldobj[1], -1];
        this.data.setA.unshift(newobj);
      }
      if (obj.p > 0)
        this.updateA(obj.p);
    }
  },

  isPosBlank(x, y) : boolean {
    for (let i = 0;i<this.data.setB.length;i++) {
      if (x == this.data.setB[i][0] && y == this.data.setB[i][1])
        return false;
    }
    for (let i = 0;i<this.data.setW.length;i++) {
      if (x == this.data.setW[i][0] && y == this.data.setW[i][1])
        return false;
    }
    return true;
  },

  checkRemoveBlock(step, pos, doremove) {
    let removecount = 0;
    let other = [];
    if (step % 2 == 0) {
      /*预落子,返回前恢复*/
      this.data.setB.push(pos);
      if (doremove) {
        for (let i = 0;i<this.data.setW.length;i++)
          other.push(this.data.setW[i]);
      } else {
        for (let i = 0;i<this.data.setB.length;i++)
          other.push(this.data.setB[i]);
      }
    } else {
      this.data.setW.push(pos);
      if (doremove) {
        for (let i = 0;i<this.data.setB.length;i++)
          other.push(this.data.setB[i]);
      } else {
        for (let i = 0;i<this.data.setW.length;i++)
          other.push(this.data.setW[i]);
      }
    }
    while (other.length > 0) {
      let block = [];
      /*找到other中第一个子所在的块*/
      block.push(other[0]);
      other.splice(0, 1);
      for (let i = 0;i<other.length;i++) {
        for (let j = 0;j<block.length;j++) {
          if ( ( other[i][0] == block[j][0] && (((other[i][1] - block[j][1])==1) || ((other[i][1] - block[j][1])==-1))) 
            || ( other[i][1] == block[j][1] && (((other[i][0] - block[j][0])==1) || ((other[i][0] - block[j][0])==-1))) 
             ) {
            block.push(other[i]);
            other.splice(i, 1);
            i = -1; /* 注意有i++ */
            break;
          }
        }
      }
      /* 找到other中的一个连续块在block中 */
      /* 看看这个块是否有气, 看每个子上下左右是否没有子 */
      let isblocklive = false;
      for (let i = 0;i<block.length;i++) {
        let x = 0;
        let y = 0;
        x = block[i][0];
        y = block[i][1] + 1;
        if (y <= 19) {
          if (this.isPosBlank(x, y)) {
            isblocklive = true;
            break;
          }
        }
        x = block[i][0];
        y = block[i][1] - 1;
        if (y >= 1) {
          if (this.isPosBlank(x, y)) {
            isblocklive = true;
            break;
          }
        }
        x = block[i][0] + 1;
        y = block[i][1];
        if (x <= 19) {
          if (this.isPosBlank(x, y)) {
            isblocklive = true;
            break;
          }
        }
        x = block[i][0] - 1;
        y = block[i][1];
        if (x >= 1) {
          if (this.isPosBlank(x, y)) {
            isblocklive = true;
            break;
          }
        }
      }
     // console.log(step, ":", isblocklive, block, "other: ", other.length, other, "[0]:", (other.length>0)?other[0][0]:-1, (other.length>0)?other[0][1]:-1);
      if (!isblocklive) {
        if (!doremove) {
          /* 返回前恢复BW */
          if (step % 2 == 0) {
            this.data.setB.pop();
          } else {
            this.data.setW.pop();
          }
          return 1; /* 如果不做提子,其实就是判断有无死子,这样找到一块死子就可以直接返回了 */
        } else {
          /* 如果块没有气,则提子 */
          //console.log(step, ":", block);
          this.data.disablepos[0] = block[0][0];
          this.data.disablepos[1] = block[0][1];
          removecount += block.length;
          if (step % 2 == 0) {
            for (let i = 0;i<this.data.setW.length;i++) {
              for (let j = 0;j<block.length;j++) {
                if (this.data.setW[i][0] == block[j][0] && this.data.setW[i][1] == block[j][1]) {
                  //console.log("remove from white:", block[j]);
                  this.data.setW.splice(i, 1);
                  i--;
                  break;
                }
              }
            }
          } else {
            for (let i = 0;i<this.data.setB.length;i++) {
              for (let j = 0;j<block.length;j++) {
                if (this.data.setB[i][0] == block[j][0] && this.data.setB[i][1] == block[j][1]) {
                  this.data.setB.splice(i, 1);
                  //console.log("remove from black:", block[j]);
                  i--;
                  break;
                }
              }
            }
          }
        }
      }
    }
    /* 返回前恢复BW */
    if (step % 2 == 0) {
      this.data.setB.pop();
    } else {
      this.data.setW.pop();
    }
    return removecount;
  },

  checkBW(step, pos):boolean {
    /*检查是否有需要提的子, cur刚刚加了一个子,现在判断other中是否需要提子*/
    let other = [];
    let removecount = 0;
    /* pos是否在棋盘内 */
    if (pos[0] < 1 || pos[0] > 19 || pos[1] < 1 || pos[1] > 19) {
      console.log("pos invalid:", pos, step);
      return false;
    }
    /* pos是否为禁着点 */
    if (pos[0] == this.data.disablepos[0] && pos[1] == this.data.disablepos[1]) {
      console.log("disable pos:", this.data.disablepos);
      return false;
    }
    /* pos处是否已经有子 */
    if (!this.isPosBlank(pos[0], pos[1])) {
      console.log("pos is occupied ", pos, " B", this.data.setB, " W", this.data.setW);
      return false;
    }
    /*先判断是否造成对方提子 */
    removecount = this.checkRemoveBlock(step, pos, true);
    if (removecount == 0) {
      /* 如果没有导致对方提子,判断该子落下是否导致本方被提 */
      if (this.checkRemoveBlock(step, pos, false) > 0 ) {
         return false; /*如果本方有子被提,那就不能允许落子 */ 
      }
    }
    /* 如果落子导致对方提掉一子,则该子的位置禁止对方落子,这个在checkRemoveBlock已经记录了这个位置,
       如果没有提子或者提的子不只一颗,那就取消禁止对方落子 */
    if (removecount != 1) {
      this.data.disablepos[0] = 0;
      this.data.disablepos[1] = 0;
    }
    /* 没问题就执行落子 */
    if (step % 2 == 0) {
      this.data.setB.push(pos);
    } else {
      this.data.setW.push(pos);
    }
    return true;
  },

  updateBW():boolean{
    this.data.setB = [];
    this.data.setW = [];
    let i = 0;
    for (i = 0;i < this.data.cursteps;i++) {
      if (!this.checkBW(i, this.data.setA[i])) {
        this.setData({cursteps: i});
        return false;
      }
    }
    return true;
  },

  updateData() : boolean {
    const obj = this.data.qp.qpData[this.data.selectid];
    if (obj) {
      const alen = this.data.setA.length;
      const flagpos = obj.f + alen - obj.s.length;
      for (let i = 0;i<alen;i++) {
        if (i < flagpos)
          this.data.setA[i][2] = 0;
        else 
          this.data.setA[i][2] = i - flagpos + 1;
      }
    }
    const ret = this.updateBW();
    if (!ret) {
      this.data.setA.pop();
    }
    this.drawBoard();
    return ret;
  },

  updateSelect() : boolean {
    let displayData:[] = [];
    this.updateItem(displayData, 0, "", true, true);
    this.data.setB = [];
    this.data.setW = [];
    this.data.setA = [];
    const obj = this.data.qp.qpData[this.data.selectid];
    
    this.updateA(this.data.selectid);
    this.setData({totalsteps: this.data.setA.length});
    this.setData({cursteps: this.data.setA.length});
    this.setData({listData: displayData});
    this.setData({dispInfo: this.data.qp.getFullPath(obj.i) + ": " + obj.d + "(" +this.data.qp.countChildren(obj.i)+")"});
    return this.updateData();
  },

  getCanvasWidth() {
    wx.getSystemInfo({
      success: (res) => {
        const screenWidth = res.screenWidth;
        const canvasWidth = screenWidth * 0.95; // 假设画布宽度为屏幕宽度的90%
        this.setData({ canvasWidth });
        this.drawBoard();
      },
      fail: (err) => {
        console.error('获取系统信息失败', err);
        this.drawBoard(); // 使用默认宽度
      }
    });
  },

  qp_click: function(e) {
    const rect = wx.createSelectorQuery().select("#goBoard").boundingClientRect();
    rect.exec((res) => {
      const width = this.data.canvasWidth; 
      const gap = width / (this.data.size + 1);
      const left = res[0].left;
      const top = res[0].top;
      const x = Math.floor((e.detail.x - left) / gap);
      const y = Math.floor((e.detail.y - top) / gap);
      
      this.data.clickpos[0] = x;
      this.data.clickpos[1] = y;
      if ( (this.data.clickpos[0] < 0) || (this.data.clickpos[0] > 19) || (this.data.clickpos[1] < 0) || (this.data.clickpos[1] > 19)) {
        this.data.clickpos[0] = 0;
        this.data.clickpos[1] = 0;  
      } else {
        this.drawBoard();
      }
    });
  },

handleItemTap: function(e) {
  const item = e.currentTarget.dataset.item;
  let dataitem = this.data.qp.qpData[item.itemid];
  dataitem.t = 1 - dataitem.t;
  this.setData({selectid: dataitem.i});
  this.setData({curname: dataitem.n});
  this.data.clickpos[0] = 0;
  this.data.clickpos[1] = 0;  
  this.updateSelect();
},  

button_prev(): void {
  if (this.data.cursteps > 0) {
    this.data.clickpos = [0, 0];
    this.setData({cursteps: this.data.cursteps-1});
    this.updateData();
  } else {
    wx.showToast({
      title: '已经到最前面了',
      icon: 'success'
    }); 
  }
},
button_next(): void {
  if (this.data.cursteps < this.data.totalsteps) {
    this.data.clickpos = [0, 0];
    this.setData({cursteps: this.data.cursteps+1});
    this.updateData();
  } else {
    wx.showToast({
      title: '已经到最后了',
      icon: 'success'
    }); 
  }
},
button_addchild(): void {
  this.setData({isInputNameModalShow: true});
},
confirmNameInput(): void {
  this.setData({isInputNameModalShow: false});
  const newname = this.data.inputName.trim();
  if (newname == "") {
    wx.showToast({
      title: '节点名称不对',
      icon: 'success'
    }); 
    return ;
  }
  let newnode : QPNode = [];
  newnode.i = this.data.qp.idUsable();
  newnode.n = newname;
  newnode.p = this.data.selectid;
  newnode.d = newname;
  newnode.s = [];
  newnode.f = 0;
  newnode.y = [];
  newnode.t = 1;
  this.data.qp.qpData[newnode.i] = newnode;
  this.data.qp.qpData[newnode.p].t = 1;
  this.saveChanged();
  this.updateSelect();
},
cancelNameInput(): void {
  this.setData({isInputNameModalShow: false});
},
inputNameChange(e): void {
  this.setData({inputName: e.detail.value});
},
button_edit(): void {
  const item = this.data.qp.qpData[this.data.selectid];
  let itemy = "";
  if (!item)
    return ;
  for (let i = 0;i<item.y.length;i++) {
    itemy += item.y[i].n + "," + String(item.y[i].d[0]) + ", " + String(item.y[i].d[1]) + "\n" ;
  }
  this.setData({editName: item.n});
  this.setData({editDesc: item.d});
  this.setData({editFlags: itemy});
  this.setData({isEditModalShow: true});
},
confirmEditInput(): void {
  this.setData({isEditModalShow: false});
  const newname = this.data.editName.trim();
  if (newname == "") {
    wx.showToast({
      title: '节点名称不对',
      icon: 'success'
    }); 
    return ;
  }
  const lines = this.data.editFlags.split('\n');
  let nodey = [];
  for (let i = 0;i<lines.length;i++) {
    let parts = [];
    const line = lines[i].trim();
    let s = "";
    for (let j = 0; j<line.length;j++) {
      if ((line[j] == ',') || (line[j] == ',') || (line[j] == ' ') || (line[j] == '\t')) {
        s = s.trim();
        if (s.length > 0) {
          parts.push(s);
        }    
        s = "";
      } else {
        s += line[j];
      }
    }
    s = s.trim();
    if (s.length > 0) {
      parts.push(s);
    }
    if (parts.length >= 3) {
      let node = {
        n: parts[0],
        d: [parseInt(parts[1]), parseInt(parts[2])]
      };
      nodey.push(node);
      //console.log(i, ":", lines[i], "=>", parts[0], parts[1], parts[2], node);    
    }
  }

  /* 构造新的节点 */
  let item = this.data.qp.qpData[this.data.selectid];
  item.n = newname;
  item.d = this.data.editDesc;
  item.t = 1;
  item.y = nodey;
  this.saveChanged();
  this.updateSelect();
},
cancelEditInput(): void {
  this.setData({isEditModalShow: false});
},
editNameChange(e): void {
  this.setData({editName: e.detail.value});
},
editDescChange(e): void {
  this.setData({editDesc: e.detail.value});
},
editFlagsChange(e): void {
  this.setData({editFlags: e.detail.value});
},
button_remove(): void {
  const that = this;
  let item = that.data.qp.qpData[that.data.selectid];
  if (!item || item.i==0) {
    return ;
  }

  wx.showModal({
    title: '提示',
    content: '是否删除节点['+this.data.qp.getFullPath(item.i)+']及其子节点?',
    success: (res) => {
      if (res.confirm) {
        const pid = item.p;
        const dataitem = that.data.qp.qpData[pid];
        this.data.qp.removeChild(that.data.selectid);
        this.setData({selectid: dataitem.i});
        this.setData({dispInfo: this.data.qp.getFullPath(dataitem.i) + ": " + dataitem.d + "(" +this.data.qp.countChildren(dataitem.i)+")"});
        this.data.clickpos[0] = 0;
        this.data.clickpos[1] = 0;  
        this.saveChanged();
        this.updateSelect();        
      } else if (res.cancel) {
      }
    }
  });
},
do_append() {
  let item = this.data.qp.qpData[this.data.selectid];
  if (item.s.length == 0) {
    item.s.push([this.data.clickpos[0], this.data.clickpos[1]]);
  } else {
    item.s.push([this.data.clickpos[0], this.data.clickpos[1]]);
  }
   this.data.clickpos = [0, 0];   
   this.saveChanged();
   if (!this.updateSelect()) {
     item.s.pop();
     this.saveChanged();
     this.updateSelect();
     wx.showToast({
      title: '该位置不能落子',
      icon: 'success'
      }); 
    }
},
button_append(): void {
  if ( (this.data.clickpos[0] < 1) || (this.data.clickpos[0] > 19) || (this.data.clickpos[1] < 1) || (this.data.clickpos[1] > 19)) {
    wx.showToast({
      title: '请先选落子位置',
      icon: 'success'
    }); 
    return ;
  }
  if (this.data.qp.countChildren(this.data.selectid) > 0) {
    wx.showToast({
      title: '有子节点禁落子',
      icon: 'success'
    }); 
  } else {
    if (this.data.cursteps != this.data.totalsteps) {
      const that = this;
      wx.showModal({
        title: '提示',
        content: '此时落子将删除当前步之后已经存在的走子,你是否确认执行此操作?',
        success: (res) => {
          if (res.confirm) {
             let item = that.data.qp.qpData[that.data.selectid];
             const removesteps = that.data.totalsteps - that.data.cursteps;
             item.s.splice(item.s.length - removesteps, removesteps);
             that.do_append();
          } else if (res.cancel) {
          }
        }
      });
    } else {
      this.do_append();
    }
  }
},
button_setfirst(): void {
  const that = this;
  let taillen = this.data.totalsteps - this.data.cursteps;
  let item = that.data.qp.qpData[that.data.selectid];
  if (item && item.f != item.s.length - taillen - 1) {
    item.f = item.s.length - taillen - 1;
    this.saveChanged();
    this.updateSelect();
  }
},
genNode(id):String {
  let data = "";
  let i = 0;
  const value = this.data.qp.qpData[id];
  if (!value) return "";
  /* 先生成当前节点的输出字符串 */
  data += "\t{\n";
  data += '\t\t"i":'+String(value.i) + ',\n';
  data += '\t\t"p":'+String(value.p) + ',\n';
  data += '\t\t"f":'+String(value.f) + ',\n';
  data += '\t\t"n":"'+value.n + '",\n';
  data += '\t\t"d":"'+value.d + '",\n';
  data += '\t\t"s":[\n';
  for (i = 0;i<value.s.length;i++) {
    if (i % 5 == 0) {
      data += "\t\t\t";
    }
    data += "[" + String(value.s[i][0]) + ","+ String(value.s[i][1]) + "]";
    if (i != value.s.length-1) {
      data += ",";
    }
    if (i % 5 == 4) {
      data += "\n";
    }
  }
  if (i % 5 != 0) {
    data += "\n";
  }
  data += "\t\t],\n";
  data += '\t\t"t":1,\n';
  data += '\t\t"y":[\n';
  for (i = 0;i<value.y.length;i++) {
    data += '\t\t\t{"n":"' + value.y[i].n + '","d":['+ String(value.y[i].d[0])  + ','+ String(value.y[i].d[1])+ ']}';
    if (i != value.y.length-1) {
      data += ",";
    }
    data += "\n";
  }
  if (i % 5 != 0) {
    data += "\n";
  }
  data += "\t\t]\n\t}";
  /*再生成其每个子节点的字符串*/
  Object.entries(this.data.qp.qpData).forEach(([key, v]) => {
    if (v.p == id) {
      data += ",\n\n";
      data += this.genNode(v.i);
    }
  });
  return data;
},
button_copy(): void {
  let data = "[\n";
  data += this.genNode(this.data.selectid);
  data += "\n\n]\n\r";
  wx.setClipboardData({
    data: data,
    success: function (res) {
      wx.showToast({
        title: '复制成功',
        icon: 'success'
      }); 
    },
    fail: function(err) {
      wx.showToast({
        title: '复制失败',
        icon: 'fail'
      }); 
    }
  })  
},

addChildren(objarray, obj, pid): void {
  const objid = obj.i;
  const newid = this.data.qp.addChild(obj, pid);
  for (let i = 0;i<objarray.length;i++) {
    if (objarray[i].p == objid) {
      this.addChildren(objarray, objarray[i], newid);
    }
  }
},
button_paste(): void {
  wx.getClipboardData()
  .then((res) => {
    //console.log(res.data);
    const obj = JSON.parse(res.data);
    if (obj.length == 0)
      return ;
    //console.log(this.data.selectid, obj[0], obj);
    this.addChildren(obj, obj[0], this.data.selectid);
    this.saveChanged();
    this.updateSelect();
  })
  .catch((err) => {
    console.log(err.message);
    this.setData({dispInfo: "格式错误:" + err.message});
    wx.showToast({
      title: '格式错误',
      icon: 'fail'
    }); 
  })
},

saveChanged() {
  const data = JSON.stringify(this.data.qp.qpData);
  //return;
  wx.getFileSystemManager().writeFile({
      filePath: wx.env.USER_DATA_PATH + '/dsqp.txt',
      data: data,
      encoding: 'utf8'
  });
},

drawBoard: function () {
    const ctx = wx.createCanvasContext('goBoard');
    const strTitle = "ABCDEFGHJKLMNOPQRST";
    const width = this.data.canvasWidth; 
    const gap = width / (this.data.size + 1); // 计算每个网格之间的间距

    // 绘制背景颜色(可选)
    ctx.setFillStyle('#F0D39B');
    ctx.fillRect(10, 10, width, width);

    ctx.setFontSize(10);
    ctx.setFillStyle('black');
    ctx.setTextAlign('center'); // 文本对齐方式:'left', 'center', 'right'
    ctx.setTextBaseline('middle'); // 基线位置:'top', 'middle', 'bottom'

    // 绘制棋盘线
    for (let i = 1; i <= this.data.size; i++) {
      const y = gap * i;
      ctx.fillText(String(i), 17, y + gap / 2);
      ctx.fillText(String(i), 17 + gap * 19, y + gap / 2);
      ctx.fillText(String(i)/*strTitle.charAt(i-1)*/, y + gap / 2, 17);
      ctx.fillText(String(i)/*strTitle.charAt(i-1)*/, y + gap / 2, 17 + gap * 19);

      // 绘制横线
      ctx.moveTo(this.data.xstart + gap, this.data.ystart + y);
      ctx.lineTo(this.data.xstart + width - gap, this.data.ystart + y);

      // 绘制竖线
      ctx.moveTo(this.data.xstart + y, this.data.ystart + gap);
      ctx.lineTo(this.data.xstart + y, this.data.ystart + width - gap);
    }

    ctx.setStrokeStyle('black');
    ctx.stroke();

    const starPoints: [number, number][] = [
      [4, 4],
      [10, 4],
      [16, 4],
      [16, 10],
      [4, 16],
      [4, 10],
      [10, 16],
      [16, 16],
      [10, 10]
    ];

    for (const point of starPoints) {
      const x = gap * point[0];
      const y = gap * point[1];

      ctx.beginPath();
      ctx.arc(this.data.xstart + x, this.data.xstart + y, gap / 8, 0, Math.PI * 2);
      ctx.fillStyle = 'black';
      ctx.fill();
    }

    ctx.setFontSize(10);
    ctx.font = 'bold 10px Arial';
    ctx.setFillStyle('white');
    for (let j = 0; j < this.data.setB.length; j++) {
      const p = this.data.setB[j];
      const x = gap * p[0] - gap / 2;
      const y = gap * p[1] - gap / 2;
      ctx.drawImage('../../images/black.jpg',
                   0, 0, 64, 64, 
                   this.data.xstart + x, this.data.xstart + y, gap * 0.95, gap * 0.95);
      if (p[2] > 0) {
        ctx.fillText(String(p[2]), x + gap, y + gap);  
      }
    }

    ctx.setFillStyle('black');
    for (let j = 0; j < this.data.setW.length; j++) {
      const p = this.data.setW[j];
      const x = gap * p[0] - gap / 2;
      const y = gap * p[1] - gap / 2;
      ctx.drawImage('../../images/white.jpg',
                   0, 0, 64, 64, 
                   this.data.xstart + x, this.data.xstart + y, gap * 0.95, gap * 0.95);
      if (p[2] > 0) {
        ctx.fillText(String(p[2]), x + gap, y + gap);  
      }
    }

    ctx.setFontSize(16);
    ctx.font = 'bold 16px Arial';
    ctx.setFillStyle('black');
    ctx.setStrokeStyle("white");
    ctx.setTextAlign('center'); // 文本对齐方式:'left', 'center', 'right'
    ctx.setTextBaseline('middle'); // 基线位置:'top', 'middle', 'bottom'
    const sel = this.data.qp.qpData[this.data.selectid];
    if (sel) {
      for (let j = 0; j < sel.y.length; j++) {
        const p = sel.y[j];
        const x = gap * p.d[0] - gap / 2;
        const y = gap * p.d[1] - gap / 2;
        ctx.strokeText(p.n, x + gap, y + gap);     
        ctx.fillText(p.n, x + gap, y + gap); 
      }
    }

    if (this.data.disablepos[0] != 0) {
      const x = gap * this.data.disablepos[0] - gap / 2;
      const y = gap * this.data.disablepos[1] - gap / 2;
      ctx.setFillStyle("red");
      ctx.fillText("禁", x + gap, y + gap); 
    }

    if (this.data.clickpos[0] != 0) {
      const x = gap * this.data.clickpos[0] - gap / 2;
      const y = gap * this.data.clickpos[1] - gap / 2;
      if (this.data.cursteps % 2 == 0)
      {
        ctx.drawImage('../../images/black.jpg',
          0, 0, 64, 64, 
          this.data.xstart + x, this.data.xstart + y, gap * 0.95, gap * 0.95);
        ctx.setFillStyle("white");
      } else {
        ctx.drawImage('../../images/white.jpg',
          0, 0, 64, 64, 
          this.data.xstart + x, this.data.xstart + y, gap * 0.95, gap * 0.95);
        ctx.setFillStyle("black");
      }
      ctx.fillText("◇", x + gap, y + gap); 
    }

    ctx.draw(); // 渲染到canvas上
  }
});

board.wxml:


<canvas canvas-id="goBoard" id="goBoard" hidden="{{isInputNameModalShow || isEditModalShow}}" style="width:100%; height:100%;" bindtap="qp_click"> </canvas>
<view class="button_control">
<button class="button_class" bindtap="button_prev">前</button>
<button class="button_class"  bindtap="button_next">后</button>
<button class="button_class"  bindtap="button_addchild">增</button>
<button class="button_class"  bindtap="button_remove">删</button>
<button class="button_class"  bindtap="button_edit">改</button>
<button class="button_class"  bindtap="button_setfirst">①</button>
<button class="button_class"  bindtap="button_copy">拷</button>
<button class="button_class"  bindtap="button_paste">贴</button>
<button class="button_class"  bindtap="button_append">落</button>
</view>
<view class="step_info"> {{cursteps}}/{{totalsteps}}({{curname}})</view>
<scroll-view class="listview"
   scroll-y="true" scroll-x="true">
   <view wx:for="{{listData}}"
         wx:key="index"
         bindtap="handleItemTap"
         data-item="{{item}}"> 
         <view class ="list-item">
            <rich-text nodes="{{item.name}}"> </rich-text>
         </view>
    </view>
</scroll-view>
<view class="info" > <rich-text nodes="{{dispInfo}}"></rich-text> </view>

<view class="modal" wx:if="{{isInputNameModalShow}}">
  <view class="modal-content">
    <view class="modal-header">请输入子节点名称</view>
    <form bindsubmit="confirmNameInput">
      <view class="form-item">
        <label>父节点名称:{{curname}}</label>
        <label>子节点名称:</label>
        <input class="form-input" placeholder="请输入名称" bindinput="inputNameChange" value="{{inputName}}" />
      </view>
      <button formType="submit">确认</button>
      <button bindtap="cancelNameInput">取消</button>
    </form>
  </view>
</view>

<view class="modal" wx:if="{{isEditModalShow}}">
  <view class="modal-content">
    <view class="modal-header">节点</view>
    <form bindsubmit="confirmEditInput">
      <view class="form-item">
        <label>名称:</label>
        <input class="editname"  bindinput="editNameChange" value="{{editName}}" />
      </view>
      <view class="form-item">
        <label>描述:</label>
        <textarea class="editdesc" bindinput="editDescChange" value="{{editDesc}}" ></textarea>
      </view>
      <view class="form-item">
        <label>标签:</label>
        <textarea class="editflags" bindinput="editFlagsChange" value="{{editFlags}}" ></textarea>
      </view>
      <button formType="submit">确认</button>
      <button bindtap="cancelEditInput">取消</button>
    </form>
  </view>
</view>

board.wxss:

/* board.wxss */
page {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200vh;
}

canvas {
  background-color: #300575; /* 可选:设置棋盘背景颜色 */
}

.button_control {
  position: absolute; /* 使用绝对定位 */
  display: flex;
  justify-content: space-around;
  align-items: center;
  top: 51%;          /* 距离顶部的距离 */
  left: 2px;         /* 距离左侧的距离 */
  width: 98%;        
  height: 4%;       /* 按钮高度 */
  color: white;       /* 字体颜色 */
}
.button_class {
  height: 95%;
  width : 10%;
  background-color: blue; /* 背景颜色 */
  color: white;       /* 字体颜色 */
  margin: 2px;
  font-size: 24px;
  text-align: left;
  padding-left: 2%;
  padding-top: 2px;
  line-height: 24px;
}
.step_info {
  position: absolute; /* 使用绝对定位 */
  top: 56%;          /* 距离顶部的距离 */
  width : 90%;
  height: 4%;
  font-size:18px;
  background-color: blue; /* 背景颜色 */
  color: white;       /* 字体颜色 */
  margin: 2px;
  text-align: center;
}

.listview {
  position: absolute; /* 使用绝对定位 */
  top: 61%;          /* 距离顶部的距离 */
  left: 2px;         /* 距离左侧的距离 */
  width: 95%;        /* 按钮宽度 */
  height: 25%;       /* 按钮高度 */
  margin-left: 0px;
  color: white;       /* 字体颜色 */
  border: solid;    
  white-space: nowrap;
}

.list-item {
  display: flex;
  align-items: center;
  font-size: 18px;
  height : 18px;
  padding: 0px;
}

.info {
  position: absolute; /* 使用绝对定位 */
  top: 87%;          /* 距离顶部的距离 */
  left: 2px;         /* 距离左侧的距离 */
  width: 95%;        /* 按钮宽度 */
  height: 140px;       /* 按钮高度 */
  margin-left: 2px;
  font-size: 14px;
  color: white;       /* 字体颜色 */
}

.container {
  padding: 20px;
}
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background-color: white;
  padding: 20px;
  border-radius: 5px;
  width: 80%;
  max-width: 400px;
}

.modal-header {
  text-align: center;
  margin-bottom: 10px;
}

.form-item {
  margin-bottom: 10px;
}

.form-input {
  height: 40px;
  font-size : 16px;
}

.form-item label {
  display: block;
  margin-bottom: 5px;
}

.form-item input {
  width: 100%;
  padding: 5px;
  box-sizing: border-box;
}

.picker {
  border: 1px solid #ccc;
  padding: 5px;
}

.editname {
  height: 40px;
}
.editdesc {
  height: 100px;
}
.editflags {
  height: 200px;
}

wqqp_data.ts:

// 定义棋谱节点的数据结构
export type QPNode = {
  i: number; /*节点ID */
  p: number; /*父节点ID */
  f: number; /*①标志位置,相对于该节点起点 */
  n: string; /*节点名称 */
  d: string; /*节点描述 */
  s: [number, number][]; /*节点中落子位置 */
  t: number; /*节点是否展开 */
  y: {n:string; /*节点特别标志名称 */
      d:[number, number] /*节点特别标志位置 */
     }[];
};

type QPData = { [key: number]: QPNode }; /*棋谱是由若干节点构成的,使用它的ID号作为关键字组成一个映射 */

export class QiPuManager {
  private qpData: QPData;
  constructor() {
      this.qpData = {};
      /* 定式示例 */
      const nodes = [
      { /*根节点,节点ID=0,父节点号-1,这是规定死的 */
        i : 0,
        p : -1,
        f : 0,
        n : "围棋",
        d : '<br>基本功能:<br>前后:可以查看当前定式的前后落子<br>增:在当前选择的节点下增加一个子节点<br>删:删除当前节点以及它的子节点<br>改:修改当前节点的名称等<br>①:设置当前节点以当前落子位置为第一个序号起始点<br>拷:将当前节点及其子节点数据拷贝到剪裁板<br>贴:从剪裁板得到节点数据,作为子节点加到当前节点下面<br>落:当前选择的落子点加到当前选择节点的当前位置<br>点击棋盘:设置落子点位置', /*一个简单的帮助文本,选中根节点显示出来 */
        s : [],
        t : 1,
        y : [ 
            ]
      },
      {
          "i":1,
          "p":0,
          "f":0,
          "n":"占角定式", 
          "d":"占角的手法有八种,A位星位和B位小目是正手,C位三三也不亏,D位目外,E位高目,F位超高目,G位超高目外以及H位五五都有失均衡,角地过于空虚,该节点及子节点示例来自于【AI围棋定式大全】",
          "s":[
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[4,16]},
            {"n":"B","d":[3,16]},
            {"n":"C","d":[3,17]},
            {"n":"D","d":[3,15]},
            {"n":"E","d":[4,15]},
            {"n":"F","d":[4,14]},
            {"n":"G","d":[3,14]},
            {"n":"H","d":[5,15]}
      
          ]
        },
      
        {
          "i":2,
          "p":1,
          "f":0,
          "n":"星位",
          "d":"星位兼顾实地与势力的占角手法,A位小飞挂角,B位点三三都是正招,C位托角有强烈的方向性用以侵入黑棋右边,D位高挂则应视情况而定,意在防止对方靠压,配合己方外势A",
          "s":[
            [4,16]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[6,17]},
            {"n":"B","d":[3,17]},
            {"n":"C","d":[4,17]},
            {"n":"D","d":[6,16]}
      
          ]
        },
      
        {
          "i":3,
          "p":1,
          "f":0,
          "n":"小目",
          "d":"小目兼三四线之长,比较均衡,更重地。白方A位小飞挂比较古老, B位高挂也是正手,C位大飞和D位二间高挂稍显松软,E位碰一般是侵分下发。当然黑方在ABCD位防守也是大棋。",
          "s":[
            [3,16]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[5,17]},
            {"n":"B","d":[5,16]},
            {"n":"C","d":[6,17]},
            {"n":"D","d":[6,16]},
            {"n":"E","d":[4,16]}
          ]
        },
      
        {
          "i":4,
          "p":2,
          "f":0,
          "n":"小飞挂角",
          "d":"星位小飞挂角是正手,黑方应对方法很多,ABJ位守角,CDEFGH夹击, I位尖顶,K位靠压, LMNO看情况,P位是骗招,当然也可以脱先A-1",
          "s":[
            [6,17]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[3,14]},
            {"n":"B","d":[4,14]},
            {"n":"C","d":[8,17]},
            {"n":"D","d":[8,16]},
            {"n":"E","d":[9,17]},
            {"n":"F","d":[9,16]},
            {"n":"G","d":[10,17]},
            {"n":"H","d":[10,16]},
            {"n":"I","d":[5,17]},
            {"n":"J","d":[3,13]},
            {"n":"K","d":[6,16]},
            {"n":"L","d":[5,18]},
            {"n":"M","d":[4,13]},
            {"n":"N","d":[5,15]},
            {"n":"O","d":[7,16]},
            {"n":"P","d":[6,18]}
      
          ]
        },
      
        {
          "i":5,
          "p":4,
          "f":0,
          "n":"小飞守角",
          "d":"小飞守角是正手,此时A位托是最佳应对,B位飞角和C位拆边稍缓,D位点三三意图转身,E位靠有夹击的意味,F位大飞是骗招,右下角是白子时,也可以脱先A-1-1",
          "s":[
            [3,14]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[4,17]},
            {"n":"B","d":[4,18]},
            {"n":"C","d":[10,16]},
            {"n":"D","d":[3,17]},
            {"n":"E","d":[4,15]},
            {"n":"F","d":[3,18]}
      
          ]
        },
      
        {
          "i":6,
          "p":5,
          "f":0,
          "n":"托连扳",
          "d":"托连扳,白1,3托连扳有力,黑A位打吃普通,B位倒虎与C位立是新思路,D位连扳和E位粘也可行,F位二路打吃一般不大好A-1-1-1",
          "s":[
            [4,17],[3,17],[3,18]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[5,17]},
            {"n":"B","d":[2,16]},
            {"n":"C","d":[2,17]},
            {"n":"D","d":[2,18]},
            {"n":"E","d":[3,16]},
            {"n":"F","d":[4,18]}
      
          ]
        },
      
        {
          "i":7,
          "p":6,
          "f":0,
          "n":"挖粘",
          "d":"托连扳后黑1挖,白2粘,后续变化比较多",
          "s":[
            [5,17],[4,18],[5,16],[5,18]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":8,
          "p":7,
          "f":-4,
          "n":"压长",
          "d":"黑取势,白则得实地,两分A-1-1-1-1-1",
          "s":[
            [6,16],[7,17]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":9,
          "p":7,
          "f":0,
          "n":"左立",
          "d":"托连扳变招,黑1立重实地,白2贴强调黑气紧,征子有利时黑3犀利",
          "s":[
            [2,17],[6,16]
          ],
          "t": 1,
          "y":[
            {"n":"3","d":[7,18]}
      
          ]
        },
      
        {
          "i":10,
          "p":9,
          "f":-2,
          "n":"二路刺",
          "d":"托连扳变招,黑1立重实地,白2贴强调黑气紧,征子有利时黑3犀利",
          "s":[
            [7,18]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":11,
          "p":10,
          "f":-3,
          "n":"连",
          "d":"托连扳变招,黑1立重实地,白2贴强调黑气紧,征子有利时黑3犀利,白4,6则简明处理,后续黑棋有ABC的下法,双方皆可战A-1-1-1-1-2",
          "s":[
            [6,18],[10,17],[6,14]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[8,17]},
            {"n":"B","d":[13,17]},
            {"n":"C","d":[4,12]}
      
          ]
        },
      
        {
          "i":12,
          "p":14,
          "f":0,
          "n":"叫吃",
          "d":"叫吃",
          "s":[
            [2,16]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":13,
          "p":12,
          "f":0,
          "n":"跳冲",
          "d":"跳",
          "s":[
            [10,17],[7,17]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":14,
          "p":15,
          "f":0,
          "n":"断",
          "d":"断",
          "s":[
            [3,16]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":15,
          "p":10,
          "f":0,
          "n":"小尖",
          "d":"托",
          "s":[
            [8,17],[8,18]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":16,
          "p":12,
          "f":0,
          "n":"扳长",
          "d":"若黑子征子不利,白3长时黑棋只得脱先,后面白棋A位长价值极大,黑稍弱A-1-1-1-1-11",
          "s":[
            [9,18],[9,17],[10,18]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[6,15]}
          ]
        },
      
        {
          "i":17,
          "p":23,
          "f":0,
          "n":"单拐",
          "d":"左上是黑棋时,黑1单拐有力量,然后黑3以下冲出弃子取势A-1-1-1-1-10",
          "s":[
            [10,16],[11,17],[7,17],[8,16],[7,16],
            [5,15],[4,15],[7,15],[6,18],[6,15],
            [8,15],[9,15],[8,14],[9,14],[5,14],
            [7,14],[8,13],[6,13],[9,13],[10,15],
            [6,12],[7,13],[7,12],[5,13],[4,12],
            [6,19],[5,12],[8,19]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":18,
          "p":13,
          "f":-6,
          "n":"扳长",
          "d":"白1跳凶狠,白3断精巧,然后白5跳,白7先打,确保白9成立,将来黑棋可从A,B两点寻机,大致两分A-1-1-1-1-4",
          "s":[
            [4,15],[5,15],[8,16]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[9,18]},
            {"n":"B","d":[7,16]}
      
          ]
        },
      
        {
          "i":19,
          "p":23,
          "f":0,
          "n":"再长",
          "d":"长",
          "s":[
            [11,17],[11,18]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":20,
          "p":9,
          "f":0,
          "n":"三间跳",
          "d":"托连扳变招,黑1稍损,白2拆,A点就点不到了,若点A位,白棋可在B位挡A-1-1-1-1-3",
          "s":[
            [10,17],[8,17]
          ],
          "t": 1,
          "y":[
            {"n":"A","d":[7,18]},
            {"n":"B","d":[8,18]}
      
          ]
        },
      
        {
          "i":21,
          "p":13,
          "f":1,
          "n":"提子",
          "d":"黑1提子不好,白2打吃愉快,后面白6飞是好手,注意8和11必得其一,因此黑7可提子,白16逼住后,黑棋左下角未活净,可能陷入苦战A-1-1-1-1-6",
          "s":[
            [4,15],[3,15],[5,15],[3,16],[6,18],
            [9,17],[8,15],[8,16],[10,16],[9,15],
            [8,14],[10,18],[11,18],[11,19],[12,19],
            [10,19],[3,12]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":22,
          "p":19,
          "f":0,
          "n":"冲吃",
          "d":"如果如图一样上冲叫吃,此时形成转换,白4先顶细腻,白8是强手,白稍占上风A-1-1-1-1-9",
          "s":[
            [7,17],[5,15],[8,16],[3,15],[2,15],
            [4,15],[13,17],[2,13],[2,14],[3,13],
            [4,14],[8,15],[5,12],[3,10]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":23,
          "p":16,
          "f":0,
          "n":"长再扳",
          "d":"再扳",
          "s":[
            [10,17], [9,16]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":24,
          "p":19,
          "f":-7,
          "n":"再长吃两子",
          "d":"白1扳强手,黑棋如征子有利,黑4可压出,白5扳是好手,此时如果左上是白棋时,黑6,8连长为好,大致两分A-1-1-1-1-8",
          "s":[
            [12,17],[7,17],[12,18],[6,18]
          ],
          "t": 1,
          "y":[
          ]
        },
      
        {
          "i":25,
          "p":18,
          "f":0,
          "n":"断吃四子",
          "d":"黑1断吃四子,价值20目,不过这是后手官子,序盘阶段嫌缓,被白2,4先手成外势,得不偿失A-1-1-1-1-5",
          "s":[
            [6,18],[9,18],[2,18],[7,16],[3,19]
          ],
          "t": 1,
          "y":[
          ]
        },
        {
          "i":26,
          "p":15,
          "f":0,
          "n":"跳冲",
          "d":"跳冲",
          "s":[
            [10,17],[7,17]
          ],
          "t": 1,
          "y":[
          ]
        },
        {
          "i":27,
          "p":26,
          "f":0,
          "n":"误长",
          "d":"白先长次序错误,黑2冲时,白3断已经不是先手,黑6虎有力,白棋外围损失惨重。A-1-1-1-1-7",
          "s":[
            [8,16],[7,16],[3,16],[6,15],[2,16],
            [8,15]
          ],
          "t": 1,
          "y":[
          ]
        },
          {
            "i":28,
            "p":0,
            "f":0,
            "n":"柯洁执白胜一力辽",
            "d":"第10届应氏杯柯洁执白胜一力辽20240706,随便挑了一局棋作为完整对局的例子",
            "s":[
              [17,4],[4,4],[16,17],[3,16],[15,3],
              [17,15],[15,16],[17,12],[5,17],[7,17],
              [3,3],[3,4],[4,3],[5,4],[6,2],
              [6,16],[3,17],[2,17],[2,18],[2,16],
              [5,16],[5,15],[6,15],[4,15],[7,15],
              [6,18],[9,16],[5,18],[17,10],[15,13],
              [7,3],[4,8],[14,7],[15,8],[15,10],
              [14,8],[13,7],[13,8],[12,7],[17,8],
              [15,12],[14,12],[16,12],[16,13],[17,11],
              [18,12],[13,10],[17,5],[16,5],[16,9],
              [16,10],[18,4],[17,6],[18,5],[17,3],
              [16,6],[16,7],[15,6],[15,7],[17,7],
              [15,5],[14,6],[14,5],[13,6],[12,5],
              [18,6],[17,16],[14,15],[12,16],[7,5],
              [12,6],[18,9],[3,10],[2,3],[2,2],
              [3,9],[5,10],[18,15],[8,17],[8,18],
              [9,18],[8,16],[9,17],[13,15],[13,16],
              [2,10],[3,11],[6,9],[6,10],[7,10],
              [7,9],[5,9],[8,9],[7,11],[2,11],
              [9,10],[6,12],[14,11],[16,8],[10,8],
              [8,6],[12,8],[15,9],[17,9],[12,11],
              [11,9],[14,10],[8,8],[13,13],[14,13],
              [10,11],[11,12],[11,11],[8,5],[7,8],
              [7,7],[9,8],[9,9],[8,7],[9,6],
              [7,6],[9,7],[6,7],[9,11],[6,5],
              [6,4],[9,5],[10,5],[9,4],[7,4],
              [4,7],[3,7],[5,6],[4,6],[5,8],
              [5,7],[10,4],[6,8],[10,7],[8,8],
              [18,13],[17,13],[9,8],[10,12]
            ],
            "t":1,
            "y":[
            ]
          },
          {
            "i":29,
            "p":0,
            "f":0,
            "n":"手筋死活",
            "d":"手筋死活",
            "s":[
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":30,
            "p":29,
            "f":0,
            "n":"黄莺扑蝶",
            "d":"黄莺扑蝶",
            "s":[
              [4,16],[4,17],[5,16],[5,17],[6,17],
              [6,18],[7,17],[3,16],[3,17],[3,15],
              [4,15],[3,14],[4,14],[3,13],[4,13],
              [4,12],[2,16],[3,12],[2,17],[6,16],
              [8,16],[6,15],[16,16],[6,13]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":31,
            "p":30,
            "f":0,
            "n":"失败1",
            "d":"失败1",
            "s":[
              [4,18],[5,18],[3,18],[7,18]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":32,
            "p":30,
            "f":0,
            "n":"失败2",
            "d":"失败2",
            "s":[
              [5,18],[4,18],[3,18],[5,19],[3,19],
              [4,19]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":33,
            "p":30,
            "f":0,
            "n":"成功",
            "d":"以下就是紧气,始终保持白棋只有两口气就可以",
            "s":[
              [5,19]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":34,
            "p":33,
            "f":-1,
            "n":"成功1",
            "d":"成功1",
            "s":[
              [5,18],[7,18],[6,19],[7,19],[4,19],
              [3,18],[3,19],[4,18]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":35,
            "p":33,
            "f":-1,
            "n":"成功2",
            "d":"成功2",
            "s":[
              [3,18],[4,19]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":36,
            "p":35,
            "f":-3,
            "n":"成功21",
            "d":"成功21",
            "s":[
              [7,18],[5,18],[4,18],[2,18]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":37,
            "p":35,
            "f":-3,
            "n":"成功22",
            "d":"成功22",
            "s":[
              [2,18],[4,18],[5,18],[7,18]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":38,
            "p":35,
            "f":-3,
            "n":"成功23",
            "d":"成功23",
            "s":[
              [4,18],[2,18],[3,19],[2,19],[5,18],
              [7,18],[6,19],[7,19]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":39,
            "p":35,
            "f":-3,
            "n":"成功24",
            "d":"成功24",
            "s":[
              [3,19],[2,18],[5,18],[7,18],[4,18],
              [2,19],[6,19],[7,19]
            ],
            "t":1,
            "y":[
            ]
          },
        
          {
            "i":40,
            "p":35,
            "f":-3,
            "n":"成功25",
            "d":"成功25",
            "s":[
              [5,18],[7,18],[4,18],[2,18],[3,19],
              [2,19],[6,19],[7,19]
            ],
            "t":1,
            "y":[
            ]
          }   
    ];
    for (let i = 0;i<nodes.length;i++) {
      this.qpData[nodes[i].i] = nodes[i];
    }
  }

  getFullPath(itemid): String {
    if (itemid == 0)
      return "//围棋";
    else 
      return this.getFullPath(this.qpData[itemid].p)  + "/" + this.qpData[itemid].n;
  }

  removeChild(itemid) {
    if (!this.qpData[itemid])
      return ;
    Object.entries(this.qpData).forEach(([key, value]) => {
      if (value.p == itemid) {
        this.removeChild(value.i);
      }
    });
    delete this.qpData[itemid];
  }

  addChild(obj, pid):number {
    let item : QPNode = {
      "i": 0,
      "p": pid,
      "f": obj.f,
      "n": obj.n,
      "d": obj.d,
      "s": obj.s,
      "t": 1,
      "y": obj.y
    };
    item.i = this.idUsable();
    this.qpData[item.i] = item;
    return item.i;
  }

  countChildren(itemid): number {
    let childrencount = 0;
    Object.entries(this.qpData).forEach(([key, value]) => {
      if (value.p == itemid) {
        childrencount++;
      }
    });
    return childrencount;
  }

  idUsable() : number {
    for (let i = 1;i<10000;i++) {
      if (!this.qpData[i]) {
        return i;
      }
    }
    return 0;
  }

  isDescendant(itemid, descendid) : boolean {
    if (itemid == descendid)
      return true;
    do {
      const obj = this.qpData[itemid];
      if (!obj)
        return false;
      if (obj.p == descendid)
        return true;
      if (obj.p == -1)
        return false;
      itemid = obj.p;
    }
    while (true);
  }
} 

对DeepSeek感兴趣的可以看看前面发的文章:

  1. DeepSeek能进行逻辑推理吗?
  2. DeepSeek能运行程序吗?
  3. DeepSeek能下围棋吗?
  4. DeepSeek能下围棋吗?(续)
  5. DeepSeek真有智能吗?

http://www.kler.cn/a/577891.html

相关文章:

  • Helm安装chart包到k8s报错“不能重复使用名称,名称已被使用”
  • FPGA 的 LBC 总线详解
  • HttpServlet源码分析与Servlet开发最佳实践
  • 初阶数据结构(C语言实现)——4.1栈
  • QT---鼠标事件
  • 【网络】HTTP协议、HTTPS协议
  • 模拟调制技术详解
  • 顶点着色器和片段着色器
  • 无人机推流/RTMP视频推拉流:EasyDSS无法卸载软件的原因及解决方法
  • Android AudioFlinger(四)—— 揭开PlaybackThread面纱
  • react脚手架(creat-react-app)
  • 大数定律详解
  • 回归预测 | Matlab实现GWO-BP-Adaboost基于灰狼算法优化BP神经网络结合Adaboost思想的回归预测
  • 使用Dify+DeepSeek搭建私有知识库
  • STM32入门之I2C总线应用详解(附LM75A温度传感器实战) | 零基础入门STM32第四十九步
  • #UVM# 关于 config_db 机制中的“路径”和“层次”概念剖析
  • 点云数据处理--splat转3dtiles
  • 架构师面试(十一):消息收发
  • ajax之生成一个ajax的demo示例
  • 网络安全 api 网络安全 ast技术