【axios封装】万字长文,TypeScript实战,封装一个axios - 基础封装篇

目录

  • 前言
  • 版本
  • 环境变量配置
    • 引入的类型
      • 1、AxiosIntance: axios实例类型
      • 2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型
      • 3、AxiosRequestConfig: 请求体配置参数类型
      • 4、AxiosError: 错误对象类型
      • 5、AxiosResponse: 完整原始响应体类型
  • 目标效果
  • 开始封装
    • 骨架
    • 在拦截器封装之前
    • 全局请求拦截器
    • 全局响应拦截器
    • requst封装
    • CRUD
    • upload
  • 完整代码:
    • 类型文件
    • code配置
    • 封装的axios
  • 使用
  • 源码地址

前言

axios 是一个流行的网络请求库,简单易用。但实际上,我们开发时候经常会出于不同的需求对它进行各种程度的封装。

最近在制作自己的脚手架时,写了一个Vue3+ts+Vite项目模板,其中使用TypeScript对axios的基础请求功能进行了简单的封装,在这里梳理一下思路,也留作一个记录,为后续其他功能封装做准备。

希望这篇文章能够帮助到刚学习axios和ts的小伙伴们。同时,若文中存在一些错误或者设计不合理的地方,也欢迎大家指正。

版本

  • axios1.6.2
  • TypeScript5.3.2

环境变量配置

一般我们会使用环境变量来统一管理一些数据,比如网络请求的 baseURL 。这个项目模板中,我将文件上传的接口地址、token的key也配置在了环境变量里。

.env.development

# .env.production 和这个一样
# the APP baseURL
VITE_APP_BASE_URL = 'your_base_url'

# the token key
VITE_APP_TOKEN_KEY = 'your_token_key'

# the upload url
VITE_UPLOAD_URL = 'your_upload_url'

# app title
VITE_APP_TITLE = 'liushi_template'

环境变量类型声明文件 env.d.ts

/// <reference types="vite/client" />

export interface ImportMetaEnv {
    readonly VITE_APP_TITLE: string
    readonly VITE_APP_BASE_URL: string
    readonly VITE_APP_TOKEN_KEY?: string
    readonly VITE_UPLOAD_URL?: string
}

interface ImportMeta {
    readonly env: ImportMetaEnv
}

然后,我们使用 来封装 axios

先引入 axios, 以及必要的类型

import axios, 
    { AxiosInstance,
      InternalAxiosRequestConfig, 
      AxiosRequestConfig, 
      AxiosError, 
      AxiosResponse,
    } from 'axios';

在这里,我们引入了 axios,以及一些本次封装中会使用到的类型,

使用ts进行二次封装时,最好 ctrl+左键 看一下源码中对应的类型声明,这对我们有很大的帮助和指导作用。

引入的类型

1、AxiosIntance: axios实例类型

image.png

2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型

image.png

注意: 以前的版本下,请求拦截器的 use方法 第一个参数类型是 AxiosRequestConfig,但在高版本下,更改为了 InternalAxiosRequestConfig,如果发现使用 AxiosRequestConfig时报错, 请看一下自己版本下的相关类型声明。这里提供我的:

image.png

image.png

3、AxiosRequestConfig: 请求体配置参数类型

image.png

4、AxiosError: 错误对象类型

image.png

5、AxiosResponse: 完整原始响应体类型

image.png

从源码提供的类型可以很清晰地看到各参数或者类、方法中对应的参数、方法类型定义,这可以非常直观地为我们指明路线

目标效果

通过这次基础封装,我们想要的实现的效果是:

  • API的参数只填写接口和其他配置项、可以规定后端返回数据中 data 的类型
  • API直接返回后端返回的数据
  • 错误码由响应拦截器统一处理
  • 预留 扩展其他进阶功能的空间
  • nice的代码提示

开始封装

骨架

axios 和其中的类型在前面已经引入, 这里就先写一个骨架

class HttpRequest {
    service: AxiosInstance
    constructor(){
        // 设置一些默认配置项
        this.service = axios.create({
            baseURL: import.meta.env.VITE_APP_BASE_URL,
            timeout: 5 * 1000
        });
    }
}

const httpRequest = new HttpRequest()
export default httpRequest;

在拦截器封装之前

为了封装出更加合理的拦截器,为以及进阶封装时为 axios 配置更加强大的功能,你需要首先了解一下 axios 从发送一个请求到接收响应并处理,最后呈现给用户的流程。这样,对各部分的封装会有一个更加合理的设计。

image.png

axios请求流程 - chatGPT绘制

全局请求拦截器

class HttpRequest {
    // ...
    constructor() {
        // ...
        this.service.interceptors.request.use(
            // ...
        );
    }
}

axios v1.6.2 中,根据上面的接口请求拦截器的 use方法 接受三个参数, 均是可传项

image.png

  • onFulfilled: 在请求发送前执行, 接受一个 config 对象并返回处理后的新 config对象,一般在里面配置token等

    这里要注意一点, 高版本 axios 将它的参数类型修改为了 InternalAxiosRequestConfig

  • onRejected: onFulfilled 执行发生错误后执行,接收错误对象,一般我们请求没发送出去出现报错时,执行的就是这一步

  • options:其他配置参数,接收两个参数, 均是可传项,以后的进阶功能封装里可能会使用到

    • synchronous: 是否同步
    • runWhen: 接收一个类型为InternalAxiosRequestConfigconfig 参数,返回一个 boolean。触发时机为每次请求触发拦截器之前,runWhen返回 true, 则执行作用在本次请求上的拦截器方法, 否则不执行

了解了三个参数之后,思路就清晰了,然后我们可以根据需求进行全局请求拦截器的封装

class HttpRequest {
    // ...
    constructor() {
        // ...
        this.service.interceptors.request.use(
            (config: InternalAxiosRequestConfig) => {
                /**
                 * set your config
                 */
                if (import.meta.env.VITE_APP_TOKEN_KEY && getToken()) {
                    // carry token
                    config.headers[import.meta.env.VITE_APP_TOKEN_KEY] = getToken()
                }
                return config
            },
            (error: AxiosError) => {
                console.log('requestError: ', error)
                return Promise.reject(error);
            },
            {
                synchronous: false,
                runWhen: ((config: InternalAxiosRequestConfig) => {
                    // do something

                    // if return true, axios will execution interceptor method
                    return true
                })
            }
        );
    }
}

全局响应拦截器

同样是三个参数,后两个和请求拦截器差不多,说第一个就行。

类型定义如下:

image.png

第一个参数同样是 onFulfilled,在返回响应结果之前执行,我们需要在这里面取出后端返回的数据,同时还要进行状态码处理。

从类型定义上可以看到,参数类型是一个泛型接口, 第一个泛型 T 用来定义后端返回数据的类型

先定义一下和后端约定好的返回数据格式:

我一般做项目时候约定的是这种,可以根据实际情况进行修改

./types/index.ts

export interface ResponseModel<T = any> {
    success: boolean;
    message: string | null;
    code: number | string;
    data: T;
}

因为里面定义了 code,所以还需要配置一份和后端约定好的 code 表,来对返回的 code 进行分类处理

./codeConfig.ts

// set code cofig
export enum CodeConfig {
    success = 200,
    notFound = 404,
    noPermission = 403
}

其实axios本身也提供了一份 HttpStatusCode

image.png
但最好根据项目组实际情况维护一份和后端约定好的 code

然后就可以开始封装响应拦截器了。要注意返回的类型

import { CodeConfig } from './codeConfig.ts'
import { ResponseModel } from './types/index.ts'
class HttpRequest {
    // ...
    constructor() {
        // ...
        this.service.interceptors.response.use(
            (response: AxiosResponse<ResponseModel>): AxiosResponse['data'] => {
                const { data } = response
                const { code } = data
                if (code) {
                    if (code != HttpCodeConfig.success) {
                        switch (code) {
                            case HttpCodeConfig.notFound:
                                // the method to handle this code
                                break;
                            case HttpCodeConfig.noPermission:
                                // the method to handle this code
                                break;
                            default:
                                break;
                        }
                        return Promise.reject(data.message)
                    } else {
                        return data
                    }
                } else {
                    return Promise.reject('Error! code missing!')
                }
            },
            (error: AxiosError) => {
                return Promise.reject(error);
            }
        );
    }
}

在这个响应拦截器里,我们先通过解构赋值拿出了后端返回的响应数据 data, 然后提取出了里面约定好的 code,如果 code 是约定的表示一切成功的值,那么把响应数据返回, 否则根据 code 的不同值进行相应的处理。比如 把message里信息用 MessageBox 显示、登录过期清空token强制登出、无权限警告、重新请求等等

requst封装

重新封装 axios.request() 方法,传入一个config, 以后的进阶版本中,可能会修改传参,并在这个封装的 request() 中添加更多高级功能。但是在基础版本里,这一步看上去似乎有些冗余。

import { ResponseModel } from './types/index.ts'
class HttpRequest {
    // ...
    constructor(){/**/}
    request<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        /**
         * TODO: execute other methods according to config
         */
        return new Promise((resolve, reject) => {
            try {
                this.service.request<ResponseModel<T>>(config)
                    .then((res: AxiosResponse['data']) => {
                        resolve(res as ResponseModel<T>);
                    })
                    .catch((err) => {
                        reject(err)
                    })
            } catch (err) {
                return Promise.reject(err)
            }
        })
    }
}

CRUD

调用我们已经封装好的 request() 来封装 crud 请求,而不是直接调用 axios 自带的, 原因上面已经说了

import { ResponseModel } from './types/index.ts'
class HttpRequest {
    // ...
   
    get<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'GET', ...config })
    }
    post<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'POST', ...config })
    }
    put<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'PUT', ...config })
    }
    delete<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'DELETE', ...config })
    }
}

upload

文件上传封装,一般是表单形式上传,它有特定的 Content-Type 和数据格式,需要单独拿出来封装

先定义需要传入的数据类型 —— 和后端约定好的 name, 以及上传的文件数据 —— 本地临时路径或者Blob。在这里我是设置的上传文件的接口唯一,所以希望把接口url配置在环境变量里,在文件上传接口中不允许用户在接口的配置项参数里修改url,于是新定义了一个 UploadFileItemModel 类型, 不允许用户在 options 里再传入 urldata

若有多个文件上传接口url, 可以根据实际情况进行修改

./types/index.ts

export interface UploadFileItemModel {
    name: string,
    value: string | Blob
}

export type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'> 

一般来说,文件上传完成后,后端返回的响应数据中的data是被上传文件的访问url,所以这里泛型 T 设置的默认值是 string

import { UploadFileItemModel } from './types/index.ts'
class HttpRequest {
    // ...
    upload<T = string>(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise<ResponseModel<T>> | null {
        if (!import.meta.env.VITE_UPLOAD_URL) return null

        let fd = new FormData()
        fd.append(fileItem.name, fileItem.value)
        let configCopy: UploadRequestConfig
        if (!config) {
            configCopy = {
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            }
        } else {
            config.headers!['Content-Type'] = 'multipart/form-data'
            configCopy = config
        }
        return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy })
    }

完整代码:

类型文件

./types/index.ts

import { AxiosRequestConfig } from 'axios'
export interface ResponseModel<T = any> {
    success: boolean;
    message: string | null;
    code: number | string;
    data: T;
}

export interface UploadFileItemModel {
    name: string,
    value: string | Blob
}


/**
 * customize your uploadRequestConfig
 */
export type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'> 

code配置

./codeConfig.ts

// set code cofig
export enum CodeConfig {
    success = 200,
    notFound = 404,
    noPermission = 403
}

封装的axios

./axios.ts

import axios, 
    { AxiosInstance,
      InternalAxiosRequestConfig, 
      AxiosRequestConfig, 
      AxiosError, 
      AxiosResponse,
    } from 'axios';
import { CodeConfig } from './CodeConfig';
import { ResponseModel, UploadFileItemModel, UploadRequestConfig } from './types/index'
import { getToken } from '../token/index'

class HttpRequest {
    service: AxiosInstance

    constructor() {
        this.service = axios.create({
            baseURL: import.meta.env.VITE_APP_BASE_URL,
            timeout: 5 * 1000
        });

        this.service.interceptors.request.use(
            (config: InternalAxiosRequestConfig) => {
                /**
                 * set your config
                 */
                if (import.meta.env.VITE_APP_TOKEN_KEY && getToken()) {
                    config.headers[import.meta.env.VITE_APP_TOKEN_KEY] = getToken()
                }
                return config
            },
            (error: AxiosError) => {
                console.log('requestError: ', error)
                return Promise.reject(error);
            },
            {
                synchronous: false
                runWhen: ((config: InternalAxiosRequestConfig) => {
                    // do something

                    // if return true, axios will execution interceptor method
                    return true
                })
            }
        );

        this.service.interceptors.response.use(
            (response: AxiosResponse<ResponseModel>): AxiosResponse['data'] => {
                const { data } = response
                const { code } = data
                if (code) {
                    if (code != HttpCodeConfig.success) {
                        switch (code) {
                            case HttpCodeConfig.notFound:
                                // the method to handle this code
                                break;
                            case HttpCodeConfig.noPermission:
                                // the method to handle this code
                                break;
                            default:
                                break;
                        }
                        return Promise.reject(data.message)
                    } else {
                        return data
                    }
                } else {
                    return Promise.reject('Error! code missing!')
                }
            },
            (error: any) => {
                return Promise.reject(error);
            }
        );
    }

    request<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        /**
         * TODO: execute other methods according to config
         */
        return new Promise((resolve, reject) => {
            try {
                this.service.request<ResponseModel<T>>(config)
                    .then((res: AxiosResponse['data']) => {
                        resolve(res as ResponseModel<T>);
                    })
                    .catch((err) => {
                        reject(err)
                    })
            } catch (err) {
                return Promise.reject(err)
            }
        })
    }

    get<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'GET', ...config })
    }
    post<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'POST', ...config })
    }
    put<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'PUT', ...config })
    }
    delete<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
        return this.request({ method: 'DELETE', ...config })
    }
    upload<T = string>(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise<ResponseModel<T>> | null {
        if (!import.meta.env.VITE_UPLOAD_URL) return null

        let fd = new FormData()
        fd.append(fileItem.name, fileItem.value)
        let configCopy: UploadRequestConfig
        if (!config) {
            configCopy = {
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            }
        } else {
            config.headers!['Content-Type'] = 'multipart/form-data'
            configCopy = config
        }
        return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy })
    }
}

const httpRequest = new HttpRequest()
export default httpRequest;

使用

历史上的今天开放API做个测试: https://api.vvhan.com/api/hotlist?type=history

拆分一下:

  • baseURL: ‘https://api.vvhan.com/api’
  • 接口url: ‘/hotlist?type=history’

把baseURL配置到环境变量里:

VITE_APP_BASE_URL = 'https://api.vvhan.com/api'

根据接口文档修改 ResponseModel, 因为这个接口的响应数据里没有code那些, 所以封装里的code相关逻辑就先注释了, 直接返回原始响应体中的 data

export interface ResponseModel<T> {
    data: T
    subtitle: string
    success: boolean
    title: string
    update_time: string
}

/src/api/types/hello.ts:定义后端返回给这个接口的数据中, data 的类型

export interface exampleModel {
    index: number
    title: string
    desc: string
    url: string
    mobilUrl: string
}

/src/api/example/index.ts:封装请求接口,使用 enum 枚举类型统一管理接口地址

import request from '@/utils/axios/axios'
import { exampleModel } from '../types/hello'

enum API {
    example = '/hotlist?type=history'
}

export const exampleAPI = () => {
    return request.get<exampleModel[]>({ url: API.example })
}

试一试:

<script setup lang="ts">
import HelloWorld from "../../components/HelloWorld.vue";
import { exampleAPI } from "@/api/hello";
exampleAPI().then((res) => {
    console.log('getData: ', res)
    const title = res.title
    const { data } = res
    console.log('list: ', data)
});
</script>

<template>
  <div>
    <HelloWorld msg="Vite + Vue + Tailwindcss + TypeScript" />
  </div>
</template>

提示很舒服

image.png

image.png

控制台打印的数据:

image.png

源码地址

v3-ts-tailwind-template中的axios封装文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/198626.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#文件流FileStream类

目录 一、文件流类 1.FileStream类的常用属性 2.FileStream类的常用方法 3.使用FileStream类操作文件 二、文本文件的写入与读取 1.StreamWriter类 2.StreamReader类 3.示例及源码 三、二进制文件的写入与读取 1.BinaryWriter类 2.BinaryReader类 3.示例源码 数据流…

【数据结构/C++】栈和队列_链栈

链头 栈顶。 #include<iostream> using namespace std; // 链栈 typedef int ElemType; typedef struct Linknode {ElemType data;struct Linknode *next; } *LiStack; // 初始化 void InitLiStack(LiStack &S) {S (LiStack)malloc(sizeof(struct Linknode));S->…

Shell条件变量练习

1.算数运算命令有哪几种&#xff1f; (1) "(( ))"用于整数运算的常用运算符&#xff0c;效率很高 [rootshell scripts]# echo $((24*5**2/8)) #(( ))2452814 14 (2) "$[ ] "用于整数运算 [rootshell scripts]# echo $[24*5**2/8] #[ ]也可以运…

技巧-PyTorch中num_works的作用和实验测试

简介 在 PyTorch 中&#xff0c;num_workers 是 DataLoader 中的一个参数&#xff0c;用于控制数据加载的并发线程数。它允许您在数据加载过程中使用多个线程&#xff0c;以提高数据加载的效率。 具体来说&#xff0c;num_workers 参数指定了 DataLoader 在加载数据时将创建的…

京东大数据(京东运营数据采集):2023年10月京东牛奶乳品行业品牌销售排行榜

鲸参谋监测的京东平台10月份牛奶乳品市场销售数据已出炉&#xff01; 10月份&#xff0c;牛奶乳品整体销售上涨。鲸参谋数据显示&#xff0c;今年10月&#xff0c;京东平台上牛奶乳品的销量将近1700万&#xff0c;同比增长1%&#xff1b;销售额将近17亿&#xff0c;同比增长约5…

React Native 更换淘宝镜像提升包下载速度

React Native 更换淘宝镜像提升包下载速度 每次运行项目的时候都是卡在包下载的命令上&#xff0c;每次一等就要 1h20m 极度崩溃&#xff0c;那是因maven镜像源为Google导致无法正常下载。 那么我们就可以切换maven镜像源&#xff0c;方法如下&#xff1a; 找到项目下的**/an…

09. 智慧商城——订单结算、订单管理

01. 订单结算台 所谓的 “立即结算”&#xff0c;本质就是跳转到订单结算台&#xff0c;并且跳转的同时&#xff0c;需要携带上对应的订单参数。 而具体需要哪些参数&#xff0c;就需要基于 【订单结算台】 的需求来定。 (1) 静态布局 准备静态页面 <template><di…

<JavaDS> 二叉树遍历各种遍历方式的代码实现 -- 前序、中序、后序、层序遍历

目录 有以下二叉树&#xff1a; 一、递归 1.1 前序遍历-递归 1.2 中序遍历-递归 1.3 后序遍历-递归 二、递归--使用链表 2.1 前序遍历-递归-返回链表 2.2 中序遍历-递归-返回链表 2.3 后序遍历-递归-返回链表 三、迭代--使用栈 3.1 前序遍历-迭代-使用栈 3.2 中序遍…

Unity中Shader的BRDF解析(三)

文章目录 前言一、BRDF中的镜面反射项二、分别解析每一个参数1、D、G函数&#xff1a;speclarTerm2、其他中间步骤3、光照颜色4、F函数&#xff08;菲涅尔函数&#xff09; &#xff1a;FresnelTermIBL在下篇文章中继续解析 三、最终代码.cginc文件:Shader文件&#xff1a; 前言…

Unity工具脚本-检测资源文件夹是否有预制件是指定层级

效果&#xff1a; 先在菜单栏里面找到Tools/CheckPrefabLayers打开窗口 代码&#xff1a; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine;public class CheckPrefabLayers : EditorWindow {public in…

直线(蓝桥杯)

直线 题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 在平面直角坐标系中&#xff0c;两点可以确定一条直线。如果有多点在一条直线上&#xff0c; 那么这些点中任意两点确定的直线是同一条。 给定平面上 2 3 个…

(Linux2.6内核)进程调度队列与切换

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 我们首先来了解几个概念 1. 进程在CPU上运行的时候&#xff0c;一定要运行完才行吗&#xff1f;答案是否定的&#xff0c;我们大部分的操作系统&#xff0c;主流就是分时操作系统&#xff0c;即基于时间片进程轮转执行的。 …

初次尝试http OAuth2验证的请求

第一次对接OAuth2验证的接口&#xff0c; 莫不着门道&#xff0c;后面获取token成功后&#xff0c;发现其实不难&#xff0c; 用postman举例&#xff1a; 其实挺简单。用客户端id秘钥 获取token---》后面的请求带上token 1,在head中增加 Authorization头 内容格式如上图&…

JAVA文件IO, File类, 字符流,字节流

文章目录 文件IO1. File2. IO流2.1 字符流2.1.1 Reader2.1.2 Writer 2.2 字节流2.2.1 InputStream2.2.2 FileInputStream2.2.3 利用Scanner进行字符读取2.2.4 OutputStream 文件IO I: Input, 从硬盘往内存读数据 O: Output, 从内存往硬盘输出数据 1. File Java 中通过 java…

服务器如何做好入侵防护

不管是企业还是个人&#xff0c;网上业务都需要依赖于服务器&#xff0c;服务器一旦被黑客入侵&#xff0c;企业会面临很多安全风险&#xff0c;比如业务被中断、数据被窃取、被加密勒索、服务器不稳定等影响。入侵防护&#xff0c;主机安全也是目前网络安全防护的一个重点。关…

通义千问 Qwen-7B-Chat-Int4 模型本地化部署

如需在本地或离线环境下运行本项目&#xff0c;需要首先将项目所需的模型下载至本地&#xff0c;通常开源 LLM 与 Embedding 模型可以从 HuggingFace 下载。 以本项目中默认使用的 LLM 模型 THUDM/ChatGLM2-6B 与 Embedding 模型 moka-ai/m3e-base 为例&#xff1a; 下载模型…

基于YOLO模型建筑工地个人防护设备目标检测

使用安全装备可以保护他们免受建筑工地的意外事故。据统计&#xff0c;每年有数以万计的工人在建筑工地受到严重伤害&#xff0c;造成终生困难。然而&#xff0c;通过自我监控来确保工人穿戴个人防护装备非常重要。在这方面&#xff0c;需要一个准确和快速的系统来检测工人是否…

怎么在电脑上做工作计划?

对于职场人士来说&#xff0c;想要提高工作效率&#xff0c;提前做好工作计划是非常有必要的。我们可以将所有的任务和工作计划都记录下来&#xff0c;并通过设定提醒时间来提醒自己&#xff0c;可以帮助我们更有效地管理时间&#xff0c;从而不会错过重要的工作和任务。 而在…

视觉测量基础

1. 相机模型 1.1 坐标系转换原理 世界坐标系(world Coords):点在真实世界中的位置&#xff0c;描述相机位置。 相机坐标系(Cameras Coords):以相机光学系统中心&#xff08;镜头中心&#xff09;为原点&#xff0c;建立相机坐标系。 图像物理坐标系(Film Coords):经过小孔成…

【Python 训练营】N_12 打印菱形图案

题目 打印菱形图案 分析 先把图形分成两部分来看待&#xff0c;前四行一个规律&#xff0c;后三行一个规律&#xff0c;利用双重for循环&#xff0c;第一层控制行&#xff0c;第二层控制列。 答案 # 方法一 for i in range(4):block **(2*i1)print({:^7}.format(block))…
最新文章