2024年8月

稀土领域生产一体化管控系统案例

目       录

1.      项目背景... 2

2.      实现目标... 2

3.      整体框架... 3

4.      集控中心改造... 4

5.      车间加装传感器及网络改造... 5

6.      边缘工艺模型及仿真... 5

7.      基础工业互联网平台... 6

8.      稀土生产一体化管控业务... 6

9.      掌上工厂... 6


注:本文档为案例宣传资料,省略涉密和方案细节内容。

1.   项目背景

稀土是国家的战略资源,国家对稀土产业发展实行统一规划,国家鼓励和支持稀土产业新技术、新工艺、新产品、新材料、新装备的研发和应用,持续提升稀土资源开发利用水平,推动稀土产业高端化、智能化、绿色化发展。

经过2个月的详细调研,**稀土目前缺少生产车间之间数据协同交互、缺少完整的生产工序数据协同监测和分析,无法及时了解生产过程和经营情况,更无法进行有效的生产过程优化。

本项目以工业互联网基础平台为基础,实现设备互联互通、异构数据集成,实现**稀土生产工艺核心业务数字化、信息化和智能化,使公司生产者、管理者和经营者随时掌握稀土生产、生产过程决策、执行反馈优化等。

本项目推动**稀土生产环节的数字化、信息化和智能化改造,从行业、战略、技术和市场等方案全面提升**稀土的竞争力,打造**稀土焙烧、水浸、萃取、碳沉和灼烧全工序生产智慧大脑。

2.   实现目标

基于**稀土生产现状及结合稀土行业智能化发展现状,通过高标准设计、高质量完成,向稀土冶炼全流程的智能制造发展看齐,提升**稀土生产和经营的综合竞争力。从原料到产成品整条生产线实现集中管控、生产运营协同、关键设备实时监测预警、产品吨氧化物能源消耗分析,提高产品质量稳定性、产量稳定性、设备运行稳定性。

最终实现生产工艺全流程数据透明化、生产工序间数据及人员协同、降低稀土工艺关键设备故障率、降低稀土产品综合能源消耗、生产过程数据积累及数据分析。

3.   整体框架

**稀土生产一体化管控系统建设本着“软硬兼顾,内外兼修”,一方面从硬件层面打破地理空间位置限制,实现产线各工序的集中操控,另一方面从生产工艺出发,打通产线数据流、物质流与能量流,从机理与大数据分析层面建立机理模型,构建单元工序、稀土产线智能应用,真正实现稀土产线的全流程协同智能管控,最终实现稀土智能中心建设。

**稀土生产一体化管控系统覆盖检化验、焙烧、水浸、转型、萃取、碳沉、灼烧和环保等连续生产全工序段,统一规划边缘智能模型和仿真、工业互联网基础平台、一体化生产运营监控、全厂智能决策、数字孪生、掌上工厂等。简单框架示意(
注:详细框架涉密,不作为公开宣传材料
),如下图:

数据框架包括车间及工序(焙烧、水浸、萃取、碳沉、灼烧、环保和机动)、平台端,主要保障数据传输的实时性、稳定性、唯一性和完整性。统一数据采集、统一数据传输方式及通信协议,统一数据处理、统一数据存储、统一数据建模及分析、统一数据接口和统一业务扩展。数据框架示意,如下图:

4.   集控中心改造

集控位置:**稀土萃取车间控制室。

现有中控室长15m*宽6m*高3m,中控屏幕安装在北侧墙面,有两块,分别为左侧安防视频模块和右侧为集控系统模块。设置有5个操作台,中控室西侧放置机柜和办公桌。

本项目将主屏幕更换为高2.24m、宽8.96m,显示面积20.07㎡大屏,布置在北侧墙面,作为集控系统显示端。

应用效果示意,如下图:

5.   车间加装传感器及网络改造

根据各车间生产工艺的要求,对车间内生产工序关键设备加装传感器,实现生产过程数字化监测和分析,例如**车间积液池加装流量计;回转窑加装温度探测器;调浆罐、中和罐、渣洗罐加装液位探测器和pH值探测器等。

对新加装的传感器和老旧传感器进行网络改造,加装网络转换模块,实现生产数字化。某车间网络改造示意,如下图:

6.   边缘工艺模型及仿真

(1)
焙烧工序仿真模型

气相(天然气和空气)由窑头进入向窑尾排气口运动,固相(干燥精矿)和液相(浓硫酸)由窑尾进料口进入焙烧窑向窑头出料口运动。在生产过程中,固相、液相和气相之间进行复杂的换热,窑内进行大量的化学反应,同时发生物料的输送、翻动、发泡等多种物理反应。综上,建立窑内气固液三相流动、传热模型,分析三相分布状态、运动行为及温度分布综合模型。(
注:省略模型图
)。

(2)
萃取物料平衡控制与分离效果模型

各级萃取槽中的各稀土元素分布发生异常波动时,会影响萃取分离效果和产品纯度指标。

实时监测有机相(包括皂化液)、料液以及各产品出口的流量、取样结果等参数,构建物料平衡和分离效果模型,调整水浸液、有机和渣洗液的流比,从而实现萃取工艺优化控制,提高产品产量和质量。(
注:省略模型图
)。

7.   基础工业互联网平台

工业互联网是本项目的基础平台,为本项目提供开发业务的工具,同时将来可以扩展其他业务功能。

工业互联网平台作为工业智能化发展的核心载体,平台体系为数据采集、数据建模、数据预警、数据视图、数据报表、数据追溯、数据体检和算法分析等提供支撑。

工业互联网平台向下连接设备、向上支撑应用开发,通过数据汇聚、数据应用和分析,逐步建立稀土冶炼制造能力和工业知识的标准化,构建稀土智能制造核心能力。

8.   稀土生产一体化管控业务

第一期主要实现业务的功能模块包括:综合决策大屏、生产工艺协同监测、关键设备管理、能源监测、能源预警、能源分析、化验室管理、报表管理等核心业务功能。决策大屏效果示意,如下图:

9.   掌上工厂

系统展现层通过集控室大屏、PC端和移动端多重展现。其中移动端搭配掌上移动应用,进度汇报自动关联生产数据,一键查看生产订单所有执行过程,实时追踪、掌握生产进度及时调整、安排生产执行,能源、原辅料消耗与生产进度。


物联网&大数据技术 QQ群:54256083

物联网&大数据项目 QQ群:727664080

QQ:504547114

微信:wxzz0151

博客:https://www.cnblogs.com/lsjwq

微信公众号:iNeuOS



在FastAPI中,你可以使用PEP 593中的
Annotated
类型来添加元数据到类型提示中。这个功能非常有用,因为它允许你在类型提示中添加更多的上下文信息,例如描述、默认值或其他自定义元数据。

FastAPI支持
Annotated
类型,这使得你可以为路径操作函数的参数提供额外的元数据,例如依赖项、查询参数的描述、别名等。

FastAPI介绍

FastAPI 是一个用于构建 API 的现代、快速(高性能)web 框架,基于 Python 类型提示。它的主要特点包括自动生成 OpenAPI 和 JSON Schema 文档、快速代码编写、简洁的代码结构、高效的性能等。FastAPI 使用 Starlette 作为 Web 框架的核心,并使用 Pydantic 进行数据验证。

FastAPI 的主要特点

  1. 快速


    • FastAPI 的性能非常接近于 NodeJS 和 Go 等速度较快的语言,并且比其他基于 Python 的框架如 Flask 和 Django 快得多。
  2. 简洁


    • 通过类型提示和依赖注入,代码简洁易读。
    • 开发者可以更少的代码实现更多的功能。
  3. 自动文档生成


    • FastAPI 自动生成符合 OpenAPI 规范的文档,这些文档可以通过内置的 Swagger UI 和 ReDoc UI 查看。
    • 自动生成 JSON Schema。
  4. 数据验证


    • 基于 Pydantic,FastAPI 提供了强大的数据验证功能。
    • 支持复杂的数据验证和数据解析。
  5. 类型提示


    • 充分利用 Python 的类型提示,帮助开发者编写和维护代码。
  6. 依赖注入


    • FastAPI 提供了一个简单但功能强大的依赖注入系统,可以方便地管理依赖项。

FastAPI 还支持以下功能:

  • 文件上传
  • 安全性(OAuth2、JWT 等)
  • 后台任务
  • 流媒体响应
  • GraphQL
  • SQL(通过 SQLAlchemy 等)
  • 数据库事务
  • 后台任务

安装 FastAPI 和 Uvicorn

pip install fastapi
pip install
"uvicorn[standard]"

FastAPI 是一个非常现代化和高效的框架,非常适合用于构建高性能的 API。其自动文档生成、数据验证和依赖注入等特性,使得开发者能够更快、更安全地编写代码,并提供出色的用户体验。

FastAPI项目的参数设计,这些您可以在
路径操作函数
参数或使用
Annotated
的依赖函数中使用的特殊函数,用于从请求中获取数据。

它包括

  • Query()
  • Path()
  • Body()
  • Cookie()
  • Header()
  • Form()
  • File()

您可以直接从
fastapi
导入它们

from fastapi import Body, Cookie, File, Form, Header, Path, Query

1、Query参数-查询参数

Query参数是指我们在URL中带有的查询参数如url/items?q=123&b=234 的类型格式。

假设我们要创建一个API,其中的查询参数需要带有描述和默认值:

from fastapi importFastAPI, Queryfrom typing importAnnotated

app
=FastAPI()

@app.get(
"/items/")
async
defread_items(
q: Annotated[str, Query(description
="Query string", min_length=3, max_length=50)] = "default"):return {"q": q}

在这个例子中:

  1. 我们导入了FastAPI和
    Query
    类,以及
    Annotated
    类型。
  2. 我们创建了一个FastAPI应用实例。
  3. 我们定义了一个路径操作函数
    read_items
    ,它有一个查询参数
    q
  4. 我们使用
    Annotated
    类型为查询参数
    q
    添加了元数据,这些元数据包括描述、最小长度和最大长度等。
  5. Annotated
    的第一个参数是类型提示,第二个参数是与此类型相关的元数据。

Annotated
类型允许你将额外的元数据与类型提示关联,这在创建API时特别有用,因为它可以帮助生成更丰富的API文档并确保参数验证。

下面是一个更复杂的例子,展示了如何使用
Annotated
类型与依赖项结合:

from fastapi importDependsdefcommon_parameters(
q: Annotated[str, Query(description
="Query string", min_length=3, max_length=50)] = "default"):return {"q": q}

@app.get(
"/items/")
async
defread_items(params: Annotated[dict, Depends(common_parameters)]):return params

在这个例子中:

  1. 我们定义了一个依赖函数
    common_parameters
    ,它返回一个包含查询参数
    q
    的字典。
  2. 我们使用
    Annotated
    类型和
    Depends
    将这个依赖项注入到路径操作函数
    read_items
    中。
  3. read_items
    函数返回了从依赖函数中获取的参数字典。

这种方法不仅简化了路径操作函数的参数定义,还使得代码更具可读性和可维护性。

2、Path参数-路径参数

路径参数通常用于从 URL 路径中提取信息。例如,如果你有一个获取用户信息的路径
/users/{user_id}
,你可以这样定义路径参数:

from fastapi importFastAPIfrom fastapi.params importPathfrom typing importAnnotated

app
=FastAPI()

@app.get(
"/users/{user_id}")
async
def read_user(user_id: Annotated[int, Path(..., title="The ID of the user to get")]):return {"user_id": user_id}

在这个示例中,
Annotated[int, Path(..., title="The ID of the user to get")]
表示
user_id
是一个整数,并且它是从路径中提取的参数。此外,我们还为这个参数添加了一个标题,用于生成 API 文档。

3、Body参数-请求体参数

求体参数用于处理复杂的数据结构,例如 JSON 请求体。你可以使用 Pydantic 模型来定义请求体的结构,并使用
Annotated
来进一步注解这些参数。例如:

from fastapi importFastAPIfrom pydantic importBaseModelfrom typing importAnnotated

app
=FastAPI()classUser(BaseModel):
name: str
age: int

@app.post(
"/users/")
async
def create_user(user: Annotated[User, Body(..., title="The user to create")]):return {"user": user}

在这个示例中,
Annotated[User, Body(..., title="The user to create")]
表示
user
参数是一个
User
模型实例,并且它来自请求体。我们同样为这个参数添加了一个标题。

有时候我们可以结合路径参数和请求体参数进行使用,如下例子:

from fastapi importFastAPI, Path, Bodyfrom pydantic importBaseModelfrom typing importAnnotated

app
=FastAPI()classUser(BaseModel):
name: str
age: int

@app.put(
"/users/{user_id}")
async
defupdate_user(
user_id: Annotated[int, Path(..., title
="The ID of the user to update")],
user: Annotated[User, Body(..., title
="The new user data")]
):
return {"user_id": user_id, "user": user}

在这个综合示例中,路径参数
user_id
和请求体参数
user
都使用了
Annotated
进行注解,以明确它们的来源和意图,同时为生成的 API 文档提供了更多的上下文信息。

复杂的请求体通常包括嵌套的结构,可以使用 Pydantic 模型来定义。例如:

from fastapi importFastAPIfrom pydantic importBaseModelfrom typing importList, Annotated

app
=FastAPI()classAddress(BaseModel):
street: str
city: str
state: str
zip: str
classUser(BaseModel):
name: str
age: int
addresses: List[Address]

@app.post(
"/users/")
async
def create_user(user: Annotated[User, Body(..., title="The user to create")]):return {"user": user}

在这个例子中,
User
模型包含一个嵌套的
Address
列表,这样你就可以在请求体中处理复杂的嵌套数据结构。

一个结合路径参数、查询参数和请求体参数的复杂示例:

from fastapi importFastAPI, Path, Query, Bodyfrom pydantic importBaseModelfrom typing importAnnotated

app
=FastAPI()classItem(BaseModel):
name: str
description: str
| None =None
price: float
tax: float
| None =None

@app.put(
"/items/{item_id}")
async
defupdate_item(
item_id: Annotated[int, Path(..., title
="The ID of the item to update")],
q: Annotated[str
| None, Query(None, max_length=50, title="Query string")],
item: Annotated[Item, Body(..., title
="The item to update")]
):
result
= {"item_id": item_id, "item": item}ifq:
result.update({
"q": q})return result

在这个综合示例中,我们使用了路径参数
item_id
、查询参数
q
和请求体参数
item
,并通过
Annotated
对这些参数进行注解,明确它们的来源和约束。

应用上面的处理方案,我们在项目中应用FastApi构建文档如下所示。

在发出
求救信
后,很多园友出手相救,非常感谢大家的支持!

在求救的同时,我们也在想办法奋力自救,会采取一系列的救园行动,这一次一定要把园子救下来,因为没有下一次了。

今天发布的救园行动是赞助商计划,想找10家企业赞助园子,并成为园子的创始赞助商。

有人可能会笑话这个救园计划是一个幼稚的想法,没有直接的收益,哪个企业会愿意花钱支持一个困境中的技术社区。

但我们还是想试一试,万一找到呢,哪怕只一家,也会帮助加快救园,也会让大家见证的确有这样非同寻常的企业。

赞助商计划这样的,赞助费用没有高低之分,都统一为5w/年,赞助商排名按赞助款到账时间先后。

目前的赞助商权益:

  • 在赞助商展示页面显示企业信息,包含公司logo与名称、官网地址、一行文字的公司或者主打产品简介
  • 博客园网站主导航会添加赞助商页面链接
  • 博客园网站首页底部会显示赞助商的友情链接
  • 博客园官方博客主页会置顶一篇博文列出所有赞助商

如果大家想到更好的赞助商权益,欢迎提出建议。

如果您的企业有意向赞助困境中的园子,欢迎加企业微信与我们联系,加好友时请备注【赞助商】。

一旦收到赞助款,我们会首先在这篇博文中列出赞助商公司名称。

赞助费用可以转账至园子的公司账号:

  • 户名:杭州语源科技有限公司
  • 账号:33050110838700000223
  • 开户行:中国建设银行股份有限公司杭州云栖小镇支行
  • SWIFT Code: PCBCCNBJZJX
  • 转账用途:填写“赞助博客园”

开篇

最近在做一个图片截图的功能。
因为工作时间很紧张,
当时是使用的是一个截图插件。
周末两天无所事事,来写一个简单版本的截图功能。
因为写的比较简单,如果写的不好,求大佬轻一点喷

读取图片并获取图片的宽度和高度思路

首先读取文件我们使用input中类型是file。
我们需要对读取的对象进行限制,必须是图片类型。
这个可以使用这个属性:accept="image/*" 来实现
但是这个属性不可靠,最好还是通过正则来判断。
我们要获取图片的宽和高,需要创建FileReader对象。
使用reader.readAsDataURL(file)异步读取文件内容,
并将其编码为一个Data URL(数据URL)
当文件读取完成之后,会触发reader.onload事件。
这个时候我们还要创建一个图片对象。
等待这个图片读取完成后,通过 img.width, img.height返回图片的宽和高。
下面我们就来简单实现一下

读取图片并获取图片的宽度

<div>
  <input  type="file" id="file" accept="image/*" />
</div>
<script>
  // 获取文件节点
  let fileNode = document.getElementById("file")
  // 给文件节点注册事件
  fileNode.addEventListener("change", readFile)
  // 读取文件,然后返回宽度和高度
  function readFile(e){
    let file = e.target.files[0]
    getImageWH(file, function(width, height) {  
      console.log('Width:', width, 'Height:', height);  
    }); 
  }
  // 返回文件(图片的宽和高)
  function getImageWH(file, callback) {  
    // 创建一个FileReader实例  
    const reader = new FileReader();  
    // 当文件读取完成时触发  
    reader.onload = function(e) {  
      // e 这个对象中包含这个图片相关的属性
      console.log('e这个对象', e)
      // 创建一个新的Image对象  
      const img = new Image();  
      // 设置Image的src为读取到的文件内容  
      img.src = e.target.result;  
      // 当图片加载时触发  
      img.onload = function() {  
        // 调用回调函数,并传入图片的宽高  
        callback(img.width, img.height);  
      };
    };
    // 开始读取文件内容,以DataURL的形式 
    // reader.onload 方法的执行需要调用下面这个 reader.readAsDataURL
    reader.readAsDataURL(file);  
  } 
  </script>

将图片的宽高赋值给canvas

我们在获取图片的宽和高之后然后赋值给canvas。
并且将canvas给显示出来就行。
这一步比较简单
<style>
  .canvas-box{
    border: 1px solid red;
    display: none;
  }
</style>
<canvas id="canvas-node" class="canvas-box"></canvas>
// 获取canvas节点
let canvasNode = document.getElementById("canvas-node")

// 读取文件
function readFile(e){
  let file = e.target.files[0]
  getImageWH(file, function(width, height) {  
    // 将宽度和高度传给canvasSetWH函数,显示在页面上
    canvasSetWH(canvasNode,width, height)
  }); 
}

function canvasSetWH(canvasNode,width, height){
  canvasNode.width = width
  canvasNode.height = height
  canvasNode.style.display = "block"
}

将图片内容在canvas中显示出来

想要将图片绘制出来,此时我们需要借助drawImage这个API。
这个API有三种形式的传参

第1种:drawImage(image, x, y)
image: 绘制的图像源
x, y:  图像在画布上的起始坐标(x,y), 图像将以原始尺寸绘制

第2种:drawImage(image, x, y, width, height)
image: 绘制的图像源
x, y:  图像在画布上的起始坐标(x,y)
width, height(可选):绘制到画布上的图像的宽度和高度

第3种: drawImage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight)
image: 绘制的图像源
sx, sy: 图像源中矩形区域的起始坐标
swidth, sheight:图像源中矩形区域的宽度和高度,即要绘制的图像部分
dx, dy:绘制到画布上的起始坐标
dwidth, dheight:绘制到画布上的矩形区域的宽度和高度,允许对绘制的图像进行缩放

也就是说:我们这里绘制可以使用第1种方法和第2种方法。
图像源在getImageWH 这个方法中返回来。
// 返回文件(图片的宽和高和图像源)
function getImageWH(file, callback) {  
  // ....其他代码.....
  // 当文件读取完成时触发  
  reader.onload = function(e) {  
    // ....其他代码.....
    // 当图片加载时触发  
    img.onload = function() {  
      // 调用回调函数,返回图像源,图片的宽度,高度
      callback(img,img.width, img.height);  
    };
  };
  // 开始读取文件内容,以DataURL的形式 
  // reader.onload 方法的执行需要调用下面这个 reader.readAsDataURL
  reader.readAsDataURL(file);  
} 

// 获取canvas节点
let canvasNode = document.getElementById("canvas-node")
// 创建上下文
let ctx = canvasNode.getContext("2d")

function readFile(e){
  let file = e.target.files[0]
  getImageWH(file, function(img, width, height) {  
    // 将宽度和高度传给canvasSetWH函数,显示在页面上
    canvasSetWH(canvasNode,width, height)
    // 将图片绘制出来
    ctx.drawImage(img, 0, 0,width, height );
  }); 
}

绘制蒙层

绘制蒙层这一步相对比较简单。
我们只需要在图片上绘制一个跟图片大小一样的蒙层即可。
可以借助fillStyle来填充颜色。fillRect绘制矩形。
下面我们简单实现一下
// 调用绘制蒙层的方法(在绘制图片完成后调用这个函数)
drawMask(0,0,width, height);

//绘制蒙层
function drawMask(x, y, width, height, opactity) {
  ctx.fillStyle = "rgba(0,0,0,0.5)";
  ctx.fillRect(x, y, width, height);
}

绘制截图区域

我们需要给canvas绑定鼠标按下事件。
在鼠标按下的时候记录上当前鼠标的坐标信息(x,y)
在鼠标按下的时候还要注册移动事件和抬起事件。
在鼠标移动的时候计算出蒙层的位置信息(rectEndX,rectEndY)
然后计算出截图区域的位置信息
最后还需要鼠标抬起的时候要移除移动事件和抬起事件
下面我们来简单实现一下
.... 其他代码.....
// 图像源
let img = new Image();
// 注册事件用于得到鼠标按下时的偏移量
canvasNode.addEventListener("mousedown", mousedownInCanvasHandler)
let currentPoint = {}
// 鼠标按下
function mousedownInCanvasHandler(e){
  currentPoint= { x: e.offsetX, y: e.offsetY }
  // 按下鼠标的时候我们需要注册移动事件和抬起事件
  canvasNode.addEventListener('mousemove', mousemoveInCanvasHandler)
  canvasNode.addEventListener('mouseup', mouseupInCanvasHandler)
}

// 绘制矩形
function mousemoveInCanvasHandler(e){
  let rectEndX = e.offsetX
  let rectEndY = e.offsetY
  // 得到矩形的宽度和高度
  let rectWidth = rectEndX - currentPoint.x
  let rectHeight = rectEndY - currentPoint.y
  let {width, height} = canvasNode
  ctx.clearRect(0, 0, width, height)
  // 绘制蒙层
  drawMask(0,0,width, height);
  drawScreenShot(width, height,rectWidth, rectHeight)
}
// 绘制截图
function drawScreenShot( canvasWidth, canvasHeight,rectWidth,rectHeight){
  // 在原图形之外画出一个矩形
  ctx.globalCompositeOperation = "destination-out";
  ctx.fillStyle='#000'
  ctx.fillRect(currentPoint.x, currentPoint.y,rectWidth,rectHeight)
  ctx.globalCompositeOperation ='destination-over'
  // 绘制截图区域的矩形
  ctx.drawImage(img, 0, 0,canvasWidth, canvasHeight,0,0,canvasWidth, canvasHeight );
}
// 鼠标抬起的时候要移除移动事件和抬起事件
function mouseupInCanvasHandler(e){
  canvasNode.removeEventListener('mousemove', mousemoveInCanvasHandler)
  canvasNode.removeEventListener('mouseup', mouseupInCanvasHandler)
}
.... 其他代码.....

把截图的区域显示出来

我们只需要在截图完成后(鼠标抬起时)
得到截图区域的信息ctx.getImageData()
然后把截图区域的信息写入一个新的画布即可。
在绘制前先清空画布
<style>
  .canvas-box,.canvas2-box{
    display: none;
  }
</style>
<body>
  <!-- 文件读取 -->
   <div>
     <input  type="file" id="file" accept="image/*" />
   </div>
   <canvas id="canvas-node" class="canvas-box"></canvas>
   <!-- 截图区域的图像显示在下面这个新的画布上 -->
   <div class="canvas2-box">
    <canvas id="canvas2"></canvas>
   </div>
</body>
// 将截图区域的数据保存下来
screenshotData= [currentPoint.x, currentPoint.y, rectWidth, rectHeight]

// 鼠标抬起的时候要移除移动事件和抬起事件
function mouseupInCanvasHandler(e){
  canvasNode.removeEventListener('mousemove', mousemoveInCanvasHandler)
  canvasNode.removeEventListener('mouseup', mouseupInCanvasHandler)
  drawScreenShotImg(screenshotData)
}

// 绘制一个截图区域的信息在另外一个画布上,并且将他显示出来
function drawScreenShotImg(screenshotData){
  // screenshotData是截图的开始和结束坐标
  let drawData = ctx.getImageData(...screenshotData)
  canvasSetWH(canvas2Box,screenshotData[2],screenshotData[3])
  // 先清空画布,注意清空的大小,否者会造成叠加(清除不干净)
  ctx2.clearRect(0,0, currentPoint.x, currentPoint.y)
  // 将截图区域绘制到canvas2上
  ctx2.putImageData(drawData,0,0)
}

将截图区域的部分下载下来

将canvas下载下来时,需要借助
语法:canvas.toDataURL(picType, encoderOptions)
参数:
picType:表示的是图片的格式。默认为 image/png。
encoderOptions:从 0 到 1 的区间内选择图片的质量。
  如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。

获取图片的类型我们可以通过(file.type)来知道
let file = e.target.files[0]
// 得到图片的类型,等会下载的时候需要
fileType = file.type
<button id="downBtn">down</button>
// 注册下载事件
downBtn.addEventListener('click',()=>{
  let {width, height} = canvas2
  // toDataURL的第一个参数:图片格式,默认为 image/png,
  // 第2个参数:可以从 0 到 1 的区间内选择图片的质量。
  let imgURL = canvas2.toDataURL( fileType, 1);
  let link = document.createElement('a');
  link.download = "截图图片";
  link.href = imgURL;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
})

截图功能全部代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas实现截图功能</title>
  <style>
    .canvas-box,.canvas2-box{
      display: none;
    }
  </style>
</head>
<body>
  <!-- 文件读取 -->
   <div>
     <input  type="file" id="file" accept="image/*" />
   </div>
   <canvas id="canvas-node" class="canvas-box"></canvas>
   <button id="downBtn">down</button>
   <div class="canvas2-box">
    <canvas id="canvas2"></canvas>
   </div>
</body>
<script>
  // 获取canvas节点
  let canvasNode = document.getElementById("canvas-node")
  // 创建上下文
  let ctx = canvasNode.getContext("2d")
  let downBtn =  document.getElementById("downBtn")
  
  let canvas2Box = document.querySelector(".canvas2-box")
  let canvas2 = document.getElementById("canvas2")
  let ctx2 = canvas2.getContext("2d")
  // 获取文件节点
  let fileNode = document.getElementById("file")
  // 给文件节点注册事件
  fileNode.addEventListener("change", readFile)
  // 图像源
  let img = new Image();
  // 截图区域的数据
  let screenshotData = []
  let fileType = "" // 文件的类型,下载的时候需要
  // 注册事件用于得到鼠标按下时的偏移量
  canvasNode.addEventListener("mousedown", mousedownInCanvasHandler)
  let currentPoint = {}

  // 注册下载事件
  downBtn.addEventListener('click',()=>{
    let {width, height} = canvas2
    // format:表示的是图片的类型  "image/png"
    // toDataURL的第一个参数:图片格式,默认为 image/png,
    // 第2个参数:可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
    let imgURL = canvas2.toDataURL( fileType, 1);
    let link = document.createElement('a');
    link.download = "截图图片";
    link.href = imgURL;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  })
  // 鼠标按下
  function mousedownInCanvasHandler(e){
    currentPoint= { x: e.offsetX, y: e.offsetY }
    // 按下鼠标的时候我们需要注册移动事件和抬起事件
    canvasNode.addEventListener('mousemove', mousemoveInCanvasHandler)
    canvasNode.addEventListener('mouseup', mouseupInCanvasHandler)
  }
  
  // 绘制矩形
  function mousemoveInCanvasHandler(e){
    let rectEndX = e.offsetX
    let rectEndY = e.offsetY
    // 得到矩形的宽度和高度
    let rectWidth = rectEndX - currentPoint.x
    let rectHeight = rectEndY - currentPoint.y
    let {width, height} = canvasNode
    // 将截图区域的数据保存下来
    screenshotData= [currentPoint.x, currentPoint.y, rectWidth, rectHeight]
    ctx.clearRect(0, 0, width, height)
    // 绘制蒙层
    drawMask(0,0,width, height);
    drawScreenShot(width, height,rectWidth, rectHeight)
  }
  // 绘制截图
  function drawScreenShot( canvasWidth, canvasHeight,rectWidth,rectHeight){
    // 在原图形之外画出一个矩形
    ctx.globalCompositeOperation = "destination-out";
    ctx.fillStyle='#000'
    ctx.fillRect(currentPoint.x, currentPoint.y,rectWidth,rectHeight)
    ctx.globalCompositeOperation ='destination-over'
    // 绘制截图区域的矩形
    ctx.drawImage(img, 0, 0,canvasWidth, canvasHeight,0,0,canvasWidth, canvasHeight );
  }

  
  // 鼠标抬起的时候要移除移动事件和抬起事件
  function mouseupInCanvasHandler(e){
    canvasNode.removeEventListener('mousemove', mousemoveInCanvasHandler)
    canvasNode.removeEventListener('mouseup', mouseupInCanvasHandler)
    drawScreenShotImg(screenshotData)
  }

  // 绘制一个截图区域的信息在另外一个画布上,并且将他显示出来
  function drawScreenShotImg(screenshotData){
    // screenshotData是截图的开始和结束坐标
    let drawData = ctx.getImageData(...screenshotData)
    canvasSetWH(canvas2Box,screenshotData[2],screenshotData[3])
    // 先清空画布,注意清空的大小,否者会造成叠加(清除不干净)
    ctx2.clearRect(0,0, currentPoint.x, currentPoint.y)
    // 将截图区域绘制到canvas2上
    ctx2.putImageData(drawData,0,0)
  }


  // 读取文件
  function readFile(e){
    let file = e.target.files[0]
    // 得到图片的类型,等会下载的时候需要
    console.log('file.type', file.type)
    fileType = file.type
    getImageWH(file, function(width, height) {  
      // 将宽度和高度传给canvasSetWH函数,显示在页面上
      canvasSetWH(canvasNode,width, height)
      ctx.drawImage(img, 0, 0,width, height );
      // 调用绘制蒙层的方法
      drawMask(0,0,width, height);
    });
  }


  // 返回文件(图片的宽和高)
  function getImageWH(file, callback) {  
    // 创建一个FileReader实例  
    const reader = new FileReader();  
    // 当文件读取完成时触发  
    reader.onload = function(e) {  
      // e 这个对象中包含这个图片相关的属性
      console.log('e这个对象', e)
      // 创建一个新的Image对象  
        
      // 设置Image的src为读取到的文件内容  
      img.src = e.target.result;  
      // 当图片加载时触发  
      img.onload = function() {  
        // 调用回调函数,返回图像源,图片的宽度,高度
        callback(img.width, img.height);  
      };
    };
    // 开始读取文件内容,以DataURL的形式 
    // reader.onload 方法的执行需要调用下面这个 reader.readAsDataURL
    reader.readAsDataURL(file);  
  } 

  function canvasSetWH(canvasNode,width, height){
    canvasNode.width = width
    canvasNode.height = height
    canvasNode.style.display = "block"
  }

  // 绘制蒙层
  function drawMask(x, y, width, height, opactity) {
    ctx.fillStyle = "rgba(0,0,0,0.5)";
    ctx.fillRect(x, y, width, height);
  }
</script>
</html>

尾声

终于写完了,这周过得很充实。。。
如果觉得写的不错,求未来的老板们点个赞,感谢!

大家好,我是码农先森。

在日常的开发过程中经常会遇到对时间的处理,比如将时间戳进行格式化、获取昨天或上周或上个月的时间、基于当前时间进行加减等场景的使用。在 PHP 语言中有一个针对时间处理的原生函数 strtotime,大家都知道这个函数只能满足基本的使用,如果遇到比较复杂的场景,则处理起来会比较繁琐。PHP 作为世界上最好的语言,在这个问题面前自然会有大佬出来造轮子,由此就出现了 nesbot/carbon 扩展包。其实在 Go 语言中也有同样功能的第三方库 carbon,我看过介绍文档后,惊呼到简直和 PHP 的扩展包长的一摸一样,看样子这位 Go 大佬也是从 PHP 转过去的。这样的好事直接降低了我们的学习成本,让我们更加容易上手。

在 php_carbon 目录下中使用 composer 命令安装 carbon 包。

[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/php_carbon
[manongsen@root php_carbon]$ composer require nesbot/carbon

在 go_carbon 目录下中使用
go mod init
初始化 Go 项目,并且使用
go get
安装 carbon 库 。

[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/go_carbon
[manongsen@root go_carbon]$ go mod init go_carbon
[manongsen@root go_carbon]$ go get github.com/golang-module/carbon/v2

这是初始化完成后的项目结构。

[manongsen@root php_to_go]$ tree -L 2
.
├── README.md
├── go_carbon
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── php_carbon
    ├── composer.json
    ├── composer.lock
    ├── index.php
    └── vendor

我们先在 PHP 脚本代码的首行通过函数 date_default_timezone_set 设置时区,再使用 require 加载依赖包。从下面示例代码中可以看出 Carbon 工具的用法还是比较便捷,比如使用 subDays 和 addDays 函数可以加减天数、使用 diffForHumans 函数能以符合人阅读习惯的方式输出等。此外,还有一些例子中没有展示的用法,例如获取当前的季节、获取本周开始结束时间、时间差的计算、判断当前的月份、获取当前是本周的第几天等等,从官方提供的文档来看 Carbon 工具用法还是相当的丰富,几乎可以满足任意业务场景的使用。

<?php
date_default_timezone_set("Asia/Shanghai");

require 'vendor/autoload.php';

use Carbon\Carbon;

// 获取当前时间
$now = Carbon::now();
print_r("当前时间 " . $now->toDateTimeString() . "\n");

// 获取今天时间
$today = Carbon::today();
print_r("今天时间 " . $today->toDateString() . "\n");

// 获取昨天时间
$yesterday = Carbon::yesterday();
print_r("昨天时间 " . $yesterday->toDateString() . "\n");

// 获取明天时间
$tomorrow = Carbon::tomorrow();
print_r("明天时间 " . $tomorrow->toDateString() . "\n");

// 3 天前时间
$threeDaysAgo = Carbon::today()->subDays(3);
print_r("3天前时间 " . $threeDaysAgo->toDateString(). "\n");

// 7 天前时间
$sevenDaysAgo = Carbon::today()->subDays(7);
print_r("7天前时间 " . $sevenDaysAgo->toDateString(). "\n");

// 符合人阅读习惯的时间输出
Carbon::setLocale('zh');

// 1个小时前
$anHourAgo = Carbon::now()->subHours(1)->diffForHumans();
print_r($anHourAgo. "\n");

// 1天前
$oneDayAgo = Carbon::now()->subDays(1)->diffForHumans();
print_r($oneDayAgo. "\n");           

// 1个月前
$oneMonthAgo = Carbon::now()->subMonth()->diffForHumans();
print_r($oneMonthAgo. "\n"); 

执行
php index.php
命令便可输出上述示例代码的结果。

[manongsen@root php_carbon]$ php index.php 
当前时间 2024-06-27 22:23:11
今天时间 2024-06-27
昨天时间 2024-06-26
明天时间 2024-06-28
3天前时间 2024-06-24
7天前时间 2024-06-20
1小时前
1天前
1个月前

同样在 Go 中的使用方式与 PHP 类似,但包的导入是用 import 关键词。如果原来对 PHP 的 Carbon 工具使用很熟练的话,那么转到 Go 来也会很容易理解,说是零门槛的学习成本也不为过。

package main

import (
	"fmt"

	"gitee.com/golang-module/carbon/v2"
)

func main() {
	// 当前时间
	now := carbon.Now().String()
	fmt.Printf("当前时间 %v\n", now)

	// 获取今天时间
	today := carbon.Now().ToDateString()
	fmt.Printf("今天时间 %v\n", today)

	// 获取昨天时间
	yesterday := carbon.Yesterday().ToDateString()
	fmt.Printf("昨天时间 %v\n", yesterday)

	// 获取明天时间
	tomorrow := carbon.Tomorrow().ToDateString()
	fmt.Printf("明天时间 %v\n", tomorrow)

	// 3天前时间
	threeDaysAgo := carbon.Now().SubDays(3).ToDateString()
	fmt.Printf("3天前时间 %v\n", threeDaysAgo)

	// 7天前时间
	sevenDaysAgo := carbon.Now().SubDays(7).ToDateString()
	fmt.Printf("7天前时间 %v\n", sevenDaysAgo)

	// 对人类友好的可读格式时间格式
	lang := carbon.NewLanguage()
	lang.SetLocale("zh-CN")
	c := carbon.SetLanguage(lang)
	if c.Error != nil {
		// 错误处理
		fmt.Printf("err: %v\n", c.Error)
		return
	}

	// 1个小时前
	anHourAgo := c.Now().SubHours(1).DiffForHumans()
	fmt.Printf("%v\n", anHourAgo)

	// 1天前
	oneDayAgo := c.Now().SubDays(1).DiffForHumans()
	fmt.Printf("%v\n", oneDayAgo)

	// 1个月前
	oneMonthAgo := c.Now().SubMonths(1).DiffForHumans()
	fmt.Printf("%v\n", oneMonthAgo)
}

执行
go run main.go
命令便可输出上述示例代码的结果。从输出的结果来看,与 PHP 输出是一样的并无差异。

[manongsen@root go_carbon]$ go run main.go 
当前时间 2024-06-27 21:25:29
今天时间 2024-06-27
昨天时间 2024-06-26
明天时间 2024-06-28
3天前时间 2024-06-24
7天前时间 2024-06-20
1 小时前
1 天前
1 个月前

时间处理无论是哪一门编程语言都是绕不过的一个话题,我们经常做 PHP 开发的程序员,要想比较顺利的学习 Go 语言,最好的方式就是能找到同类型的第三方库,说白了就是找到熟悉且好用的轮子,因为人的心里总是对自己熟悉的东西感到兴奋,对不熟的东西感到恐惧。因此熟悉的东西可以减低我们的学习难度,提振我们学习的自信心。从上面的两个代码示例来看,Go 语言的学习难度也不过如此,同时也打破了很多人说 Go 很难学的说法。最后为了方便大家学习 Carbon 时间处理工具,我把 PHP 中的
nesbot/carbon
和 Go 中的
golang-module/carbon/v2
官方文档附上。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。