chrom扩展开发配合百度图像文字识别实现自动登录(后端.net core web api)

news/2024/5/19 21:08:14 标签: .netcore, 前端

好久没做浏览器插件开发了,因为公司堡垒机,每次登录都要输入账号密码和验证码。太浪费时间了,就想着做一个右键菜单形式的扩展。

实现思路也很简单,在这里做下记录,方便下次开发参考。

一,先来了解下chrome扩展开发,必备文件。

manifest.json也叫清单文件。
先简单看下配置:

 //需要做C#兼职  或者浏览器插件兼职的请加我QQ:3388486286
{
    "manifest_version": 2,
    "name": "堡垒机自动化",
    "version": "1.0",
    "description": "右键菜单跳转到堡垒机页面,并自动登录",
    "permissions": 
    [
        "tabs", 
        "contextMenus",
        "webRequest",
        "webRequestBlocking",
        "https://127.0.0.4/*",
        "http://127.0.0.4/*"
    ],
    "background": {
      "scripts": ["background.js"]
    },
    "content_scripts": [
        {
          "matches": [
           "https://127.0.0.4/*",
           "http://127.0.0.4/*"
        ],
          "js": ["JS/log.js"]
        }
      ],
    "web_accessible_resources": [
        "JS/config.json"
      ],
    "icons": {
      "16": "Images/icon.png",
      "48": "Images/icon.png",
      "128": "Images/icon.png"
    }
  }
  

上述配置基本包含了插件开发的常用配置。现在简单介绍下每个配置的作用:

1.manifest_version :也就是版本的意思,你需要使用的插件版本,不过现在已经是第三版了,因为之前用过2开发,2和3的语法有差别,估本版本还是用到老版本的语法。新版本没时间去研究,反正都能用。

2.name :插件名称(自定义)

3.version : 当前插件的版本(自定义)

4.description : 插件描述

5.permissions :权限(你需要使用哪些API就需要对应的开通权限,也叫注册服务吧)

 "tabs", (该权限可以打开新的页面)
"contextMenus",(该权限可以右键创建新的菜单)
"webRequest",(该权限可以监听网络请求)
"webRequestBlocking",(该权限可以监听网络请求并且修改请求)
"https://127.0.0.4/*",(表示插件需要访问以 HTTPS 协议开头、IP 地址为 127.0.0.4 的所有页面或资源。这意味着插件将被允许在浏览器中与这些特定的 IP 地址和相关页面进行通信,无论是通过 HTTP 还是 HTTPS 访问。)
"http://127.0.0.4/*"(表示插件需要访问以 HTTP 协议开头、IP 地址为 127.0.0.4 的所有页面或资源。)
添加对应的地址作用:这意味着插件将被允许在浏览器中与这些特定的 IP 地址和相关页面进行通信,无论是通过 HTTP 还是 HTTPS 访问。
  1. "background":
    { "scripts": ["background.js"] }该脚本会一直运行监听。

具体含义如下:

"background":表示指定插件的后台脚本。
"scripts":表示指定后台脚本文件的路径。
"background.js":表示插件的后台页面脚本文件名为 "background.js"。
通过将脚本文件名添加到 "scripts" 数组中,你可以告诉浏览器插件在加载时要运行哪个脚本文件作为后台页面。这个后台页面脚本通常用于处理插件的核心功能、与浏览器 API 进行交互以及响应来自其他插件部分(如内容脚本或浏览器操作栏)的事件。

7.content_scripts

 //content_scripts  为业务JS注入,就是你要操作前端页面元素的JS  其中matches表示你JS要注入到哪些页面地址。
 "content_scripts": [
        {
          "matches": [
           "https://127.0.0.4/*",
           "http://127.0.0.4/*"
        ],
          "js": ["JS/log.js"]   //需要注入的Js文件
        }
      ]

8.web_accessible_resources:这里面往往放些静态全局的配置文件。
9.icons:图标文件。

二 、先说下background.js的思路。

1.先注册右键菜单,这样你右键浏览器,就可以看到自己的扩展程序了。

chrome.contextMenus.create({
    title:"堡垒机自动化",
    onclick:function(){
        console.log('准备跳转..')
        //跳转指定页面
        chrome.tabs.create({url:'https://127.0.0.4/index.php/login'},(tab)=>{
            console.log('跳转成功..')
            console.log(tab)
            //执行业务代码 注入JS
            chrome.tabs.executeScript(tab.id,{ file: 'JS/content_script.js'})
        })
  
    }

})

其中executeScript也是注入JS的一种方式,这种方式比较灵活,这种注入方式也可以和页面元素进行交互。

2.跳转到指定页面后,我们需要输入账号和密码,这块业务逻辑就写在content_script.js


var credentials = {};

// 读取配置文件并存储到全局对象
async function getConfig() {
  try {
    const response = await fetch(chrome.runtime.getURL('JS/config.json'));
    credentials = await response.json();
    console.log(credentials); // 打印全局对象
    // 调用填充函数
    fillCredentials();
  } catch (error) {
    console.error('Error reading config file:', error);
  }
} 

// 在页面上填充账号和密码
function fillCredentials() {
    document.querySelector("#pwd_username").value = credentials.username;
    document.querySelector("#pwd_pwd").value = credentials.password;
    //GetAccessToken();
  }

这里我们调用下getConfig()方法,进行配置文件读取,然后赋值给全局变量存储。,然后调用fillCredentials()进行账号密码赋值。

3.验证码识别,并赋值登录。
我们验证码就是一个4位字母图片,这块逻辑我是直接调用百度API实现的,本来想着自己后端实现,但是用了下第三方库,要么响应时间太久了,要么就是识别不准确。百度API直接帮你解决了这2个麻烦。但是要收费,我只用了100次的免费体验。
现在说下具体实现思路吧,本来想着直接前端发送请求识别,但是浏览器有同源策略,跨域了…这个最终还是要用到后端服务,于是就用了后端API去做图片识别功能了。

3.1先获取验证码图片的url地址。
这里我们在background.js中添加网络监听,因为图片url有固定格式,很好匹配。具体代码如下:

// 声明一个变量用于保存监听器
let requestListener;
// 启动网络请求监听器

    requestListener = function(details) {

      // 检查请求地址是否以指定的 URL 开头
      if (details.url.startsWith('https://127.0.0.4/captcha/')) {

        // 提取图片地址
        const imageUrl = details.url;
  
        // 输出图片地址
        console.log('图片地址:', imageUrl);
  
        // 在这里可以进行进一步的处理
        sendImageURLToContentScript(imageUrl);
  
  
      }
    }
  
    chrome.webRequest.onBeforeRequest.addListener(
      requestListener,
      { urls: ['https://127.0.0.4/captcha/*'] }, // 监听以指定 URL 开头的请求
      ['blocking']
    );

通过 chrome.webRequest.onBeforeRequest.addListener开启网络监听,当匹配到指定url开头的信息后,就代表该url是图片url,然后把imageUrl传给content_script.js
这里声明下:
background.js是不能直接和操作的页面通信的,background.js主要操作浏览器提供的API。实际操作Dom元素需要通过业务JS去操作。这里的业务JS就是content_script.js
那么background.jscontent_script.js如何通信呢,看代码:

 // 在background.js中获取到图片地址后,延迟1秒发送消息给业务 JS
  function sendImageURLToContentScript(imageUrl) {
    chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
      if (tabs.length > 0) {
        const tabId = tabs[0].id;
  
        setTimeout(function() {
          chrome.tabs.sendMessage(tabId, { imageUrl: imageUrl }, function(response) {
            // 在收到业务 JS 的响应后进行处理(可选)
            console.log('收到业务 JS 的响应:', response);
          });
        }, 500); // 延迟1秒后发送消息
      }
    });
  }

通过 chrome.tabs.sendMessage 发送消息。

// 业务 JS 接收消息,并进行处理
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    // 接收到来自 background.js 的消息
    if (request.imageUrl) {
      const imageUrl = request.imageUrl;
      // 在这里进行业务逻辑处理,使用获取到的图片地址
      console.log('收到来自 background.js 的图片地址:', imageUrl);
      GetCode(imageUrl);
      //GetBase64(imageUrl);
      // 可以通过 sendResponse 向 background.js 发送响应消息(可选)
      sendResponse({ message: '已收到图片地址' });
    }
  });

通过chrome.runtime.onMessage.addListener接收消息
这样就实现了background.jscontent_script.js的通信。
至于为什么要延迟500毫秒再发送,是因为我们点击右键后,content_script.js才注入到页面,而再这时候,background.js已经再运行了。所有延迟会再发送消息,这样可以避免content_script.js那边接收不到消息。

3.2业务JS发送imageUrl给后端api进行图像验证码识别

 function GetCode(imageUrl)
  {
    fetch(credentials.getcode_url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(imageUrl)
      })
      .then(response => response.text())
      .then(result => {
        let data = JSON.parse(result);

        // 获取 words_result 字段的值  
        let wordsResult = data.words_result;
        
        // 获取数组中第一个元素的 words 字段的值
        let words = wordsResult[0].words;
        console.log(words);  // 输出: "code"

        //赋值code
        document.querySelector("#pwd_captcha").value = words;

        //点击登录
       document.querySelector("#sign-box > form.form-vertical.login-content.active > div.submit-row > button").click();

      })
      .catch(error => {
        console.error("请求出错:", error);
      });

  }

好了这样就实现了验证码识别,然后赋值登录操作呢。

三、给出所有相关代码

config.json:

{
    "username": "username",
    "password": "password",
    "client_id":"FrktGAjFVjGv9SSA6S3",
    "client_secret":"IFL6FbU6tuFrPoCjaYnvvRrCGd",
    "token_url":"https://aip.baidubce.com/oauth/2.0/token",
    "getcode_url":"http://localhost:5270/api/CodeIdentity"
}

background.js


// 声明一个变量用于保存监听器
let requestListener;
// 启动网络请求监听器

    requestListener = function(details) {

      // 检查请求地址是否以指定的 URL 开头
      if (details.url.startsWith('https://127.0.0.4/captcha/')) {

        // 提取图片地址
        const imageUrl = details.url;
  
        // 输出图片地址
        console.log('图片地址:', imageUrl);
  
        // 在这里可以进行进一步的处理
        sendImageURLToContentScript(imageUrl);
  
  
      }
    }
  
    chrome.webRequest.onBeforeRequest.addListener(
      requestListener,
      { urls: ['https://127.0.0.4/captcha/*'] }, // 监听以指定 URL 开头的请求
      ['blocking']
    );

  
  // 在background.js中获取到图片地址后,延迟1秒发送消息给业务 JS
  function sendImageURLToContentScript(imageUrl) {
    chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
      if (tabs.length > 0) {
        const tabId = tabs[0].id;
  
        setTimeout(function() {
          chrome.tabs.sendMessage(tabId, { imageUrl: imageUrl }, function(response) {
            // 在收到业务 JS 的响应后进行处理(可选)
            console.log('收到业务 JS 的响应:', response);
          });
        }, 500); // 延迟1秒后发送消息
      }
    });
  }
  
  

chrome.contextMenus.create({
    title:"堡垒机自动化",
    onclick:function(){
        console.log('准备跳转..')
        //跳转指定页面
        chrome.tabs.create({url:'https://127.0.0.4/index.php/login'},(tab)=>{
            console.log('跳转成功..')
            console.log(tab)
            //执行业务代码 注入JS
            chrome.tabs.executeScript(tab.id,{ file: 'JS/content_script.js'})
        })
  
    }

})


// // 启动网络请求监听器
// chrome.webRequest.onBeforeRequest.addListener(
//     function(details) {
//       // 检查请求地址是否以指定的 URL 开头
//       if (details.url.startsWith('https://127.0.0.4/captcha/')) {
//         // 提取图片地址
//         const imageUrl = details.url;
  
//         // 输出图片地址
//         console.log('图片地址:', imageUrl);
  
//         // 在这里可以进行进一步的处理
//         sendImageURLToContentScript(imageUrl);
//       }
//     },
//     { urls: ['https://127.0.0.4/captcha/*'] }, // 监听以指定 URL 开头的请求
//     ['blocking']
//   );



// 在background.js中获取到图片地址后,延迟1秒发送消息给业务 JS
// function sendImageURLToContentScript(imageUrl) {
//     chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
//       if (tabs.length > 0) {
//         const tabId = tabs[0].id;
  
//         setTimeout(function() {
//           chrome.tabs.sendMessage(tabId, { imageUrl: imageUrl }, function(response) {
//             // 在收到业务 JS 的响应后进行处理(可选)
//             console.log('收到业务 JS 的响应:', response);
//                  // 移除监听器
//         chrome.webRequest.onBeforeRequest.removeListener(requestListener);
//           });
//         }, 100); // 延迟1秒后发送消息
//       }
//     });
//   }
  
  // 开始监听网络请求
//startRequestListener();
  

content_script.js

// 业务 JS 接收消息,并进行处理
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    // 接收到来自 background.js 的消息
    if (request.imageUrl) {
      const imageUrl = request.imageUrl;
      // 在这里进行业务逻辑处理,使用获取到的图片地址
      console.log('收到来自 background.js 的图片地址:', imageUrl);
      GetCode(imageUrl);
      //GetBase64(imageUrl);
      // 可以通过 sendResponse 向 background.js 发送响应消息(可选)
      sendResponse({ message: '已收到图片地址' });
    }
  });



var credentials = {};

// 读取配置文件并存储到全局对象
async function getConfig() {
  try {
    const response = await fetch(chrome.runtime.getURL('JS/config.json'));
    credentials = await response.json();
    console.log(credentials); // 打印全局对象
    // 调用填充函数
    fillCredentials();
  } catch (error) {
    console.error('Error reading config file:', error);
  }
} 

// 在页面上填充账号和密码
function fillCredentials() {
    document.querySelector("#pwd_username").value = credentials.username;
    document.querySelector("#pwd_pwd").value = credentials.password;
    //GetAccessToken();
  }

  /**
 * 使用 AK,SK 生成鉴权签名(Access Token)
 * @returns 鉴权签名信息(Access Token)
 */
//  function GetAccessToken() {
//     const url = credentials.token_url;
//     const data = {
//       grant_type: 'client_credentials',
//       client_id: credentials.client_id,
//       client_secret: credentials.client_secret
//     };
  
//     const requestOptions = {
//       method: 'POST',
//       mode: 'cors',
//       headers: { 'Content-Type': 'application/json' },
//       body: JSON.stringify(data)
//     };
  
//     return fetch(url, requestOptions)
//       .then(response => {
//         if (!response.ok) {
//           throw new Error('Network response was not ok');
//         }
//         return response.json();
//       })
//       .then(data => {
//         console.log(data);
//         console.log(data.access_token);
//         return data.access_token;
//       })
//       .catch(error => {
//         console.error(error);
//       });
//   }


  function GetCode(imageUrl)
  {
    fetch(credentials.getcode_url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(imageUrl)
      })
      .then(response => response.text())
      .then(result => {
        let data = JSON.parse(result);

        // 获取 words_result 字段的值  
        let wordsResult = data.words_result;
        
        // 获取数组中第一个元素的 words 字段的值
        let words = wordsResult[0].words;
        console.log(words);  // 输出: "code"

        //赋值code
        document.querySelector("#pwd_captcha").value = words;

        //点击登录
       // document.querySelector("#sign-box > form.form-vertical.login-content.active > div.submit-row > button").click();

      })
      .catch(error => {
        console.error("请求出错:", error);
      });

  }




  //图片转base64
//   function getFileContentAsBase64(path) {
//     return new Promise((resolve, reject) => {
//       fetch(path)
//         .then(response => response.arrayBuffer())
//         .then(buffer => {
//           const bytes = new Uint8Array(buffer);
//           let binary = '';
//           for (let i = 0; i < bytes.byteLength; i++) {
//             binary += String.fromCharCode(bytes[i]);
//           }
//           const base64 = btoa(binary);
//           resolve(base64);
//         })
//         .catch(error => reject(error));
//     });
//   }
  

// function GetBase64(captchaUrl)
// {
// // 使用fetch函数获取验证码图片的二进制数据
// fetch(captchaUrl)
//   .then(response => response.blob())
//   .then(blob => {
//     // 创建一个FileReader对象来读取blob数据
//     const reader = new FileReader();
//     reader.onloadend = function() {
//       // 读取完成后,将二进制数据转换为Base64编码
//       const base64 = reader.result.split(',')[1];
      
//       // 调用getFileContentAsBase64方法进行后续处理
//       getFileContentAsBase64(base64)
//         .then(result => {
//           console.log("base64:",result);
//         })
//         .catch(error => {
//           console.error(error);
//         });
//     };
//     reader.readAsDataURL(blob);
//   })
//   .catch(error => {
//     console.error(error);
//   });

// }



//识别验证码

// 获取配置并存储到全局对象
getConfig();






后端代码:

using CodeIdentify.Servers;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IBaiDuCodeIdentity, BaiDuCodeIdentityRepostoty>();
builder.Services.AddCors(options => {
    options.AddPolicy("AllowAll", builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});

var app = builder.Build();

app.UseCors("AllowAll");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

namespace CodeIdentify.Servers
{
    public interface IBaiDuCodeIdentity
    {
        Task<string> GetAccessTokenAsync();

        Task<string> GetCodeAsync(string base64);

        Task DownloadImageAsync(string url , string saveDirectory);

        string GetFileContentAsBase64(string path);

        void DeleteImage(string path);
    }
}

using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using RestSharp;
using System.Net;

namespace CodeIdentify.Servers
{
    public class BaiDuCodeIdentityRepostoty : IBaiDuCodeIdentity
    {
        public void DeleteImage(string path)
        {
          
                try
                {
                    // 获取指定文件夹中的所有图片文件
                    string[] imageFiles = Directory.GetFiles(path, "*.jpg");

                    foreach (string file in imageFiles)
                    {
                        // 删除文件
                        File.Delete(file);

                        Console.WriteLine($"已删除文件: {file}");
                    }

                    Console.WriteLine("所有图片文件已删除。");
                }
                catch (Exception ex)
                {
                    throw new Exception(ex.Message);
                    Console.WriteLine($"删除图片文件时出错:{ex.Message}");
                }
            

        }

        public async Task DownloadImageAsync(string imageUrl, string saveDirectory)
        {
            // 创建自定义的 HttpClientHandler,并禁用证书验证
            var handler = new HttpClientHandler()
            {
                ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
            };

            // 创建 HttpClient 实例,并使用自定义的 HttpClientHandler
            using (HttpClient httpClient = new HttpClient(handler))
            {
                try
                {
                    // 发送 GET 请求并获取响应消息
                    HttpResponseMessage response = await httpClient.GetAsync(imageUrl);
                    response.EnsureSuccessStatusCode();

                    // 从响应消息中获取图片内容
                    byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();

                    // 创建文件保存路径
                    Directory.CreateDirectory(saveDirectory);
                    string savePath = Path.Combine(saveDirectory, "image.jpg"); // 要保存的文件名

                    // 将图片内容保存到本地文件
                    File.WriteAllBytes(savePath, imageBytes);

                    Console.WriteLine("图片下载完成。");
                }
                catch (Exception ex)
                {
                    throw new  Exception($"图片下载失败:{ex.Message}");
                    Console.WriteLine($"图片下载失败:{ex.Message}");
                }
            }
        }


        public async Task<string> GetAccessTokenAsync()
        {
            try
            {
                var client = new RestClient($"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=FrktGAjFVjSSA6S3Tcs0f&client_secret=IFL6FbU6rPoCjaSiKjMLYnvvRrCGd");
                var request = new RestRequest(String.Empty, Method.Post);
                request.AddHeader("Content-Type", "application/json");
                request.AddHeader("Accept", "application/json");
                var body = @"";
                request.AddParameter("application/json", body, ParameterType.RequestBody);
                var response = await client.ExecuteAsync(request);
                var result = JsonConvert.DeserializeObject<dynamic>(response.Content??"");
                Console.WriteLine(result?.access_token.ToString());
                return result?.access_token.ToString()??"";
            }
            catch (Exception e)
            {

                throw new Exception($"可能次数用完了:{e.Message}");
            }

        }

        public async Task<string> GetCodeAsync(string base64)
        {
            var token = await GetAccessTokenAsync();
            var client = new RestClient($"https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token={token}");
            var request = new RestRequest(String.Empty, Method.Post);
            request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
            request.AddHeader("Accept", "application/json");
            // image 可以通过 GetFileBase64Content('C:\fakepath\captcha.png') 方法获取
            request.AddParameter("image", base64);
            var  response = await client.ExecuteAsync(request);
            Console.WriteLine(response.Content);
            return response.Content??"";
        }

        public string GetFileContentAsBase64(string path)
        {
            using (FileStream filestream = new FileStream(path, FileMode.Open))
            {
                byte[] arr = new byte[filestream.Length];
                filestream.Read(arr, 0, (int)filestream.Length);
                string base64 = Convert.ToBase64String(arr);
                return base64;
            }
        }
    }
}

using CodeIdentify.Servers;
using Microsoft.AspNetCore.Mvc;

namespace CodeIdentify.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CodeIdentityController : ControllerBase
    {
        private readonly IBaiDuCodeIdentity _baiDuCodeIdentity;
        public CodeIdentityController(IBaiDuCodeIdentity baiDuCodeIdentity) 
        {
            _baiDuCodeIdentity = baiDuCodeIdentity;
        }
        [HttpPost]
        public async Task<IActionResult> GetCode([FromBody] string url) 
        {
            string path = "Images\\image.jpg";
            string deletepath = "Images";
            await _baiDuCodeIdentity.DownloadImageAsync(url, "Images");
            string code = await _baiDuCodeIdentity.GetCodeAsync(_baiDuCodeIdentity.GetFileContentAsBase64(path));
            if(string.IsNullOrWhiteSpace(code)) return BadRequest("空字符串,识别有误");
            _baiDuCodeIdentity.DeleteImage(deletepath);
            return Ok(code);
         

        } 
    }
}


http://www.niftyadmin.cn/n/4966624.html

相关文章

C# .aspx网页获取RFID读卡器HTTP协议提交的访问文件Request获得卡号、机号,Response回应驱动读卡器显示响声

本示例使用的设备&#xff1a;RFID网络WIFI无线TCP/UDP/HTTP可编程二次开发读卡器POE供电语音-淘宝网 (taobao.com) 服务端代码&#xff1a; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.…

Mybatis-plus中操作JSON字段

1.实体类上要加上自动映射 TableName(value "school", autoResultMap true)2.json字段上加上json处理器 TableField(value "cover_url", typeHandler JacksonTypeHandler.class)private List<String> cover_url;参考博客 http://www.dedeyun.co…

jenkins Linux如何修改jenkins 默认的工作空间workspace

由于jenkins默认存放数据的目录是/var/lib/jenkins&#xff0c;一般这个var目录的磁盘空间很小的&#xff0c;就几十G,所以需要修改jenkins的默认工作空间workspace 环境 jenkins使用yum安装的 centos 7 正题 1 查看jenkins安装路径 [rootlocalhost jenkins_old_data]# rpm…

学习ts(七)泛型

定义 泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型&#xff0c;在实例化时作为参数指明这些类型。在ts中&#xff0c;定义函数、接口或类的时候&#xff0c;不预先定义好具体的类型&#xff0c;而在使用的时候在指定类型的一种特性。 例子&#xff…

HTML 网页中 自定义图像单击或鼠标悬停时放大

HTML 网页中 自定义图像单击或鼠标悬停时放大 一&#xff1a;在悬停时更改 HTML 图像的大小 例子中&#xff0c;使用 CSS 样式&#xff1b;来设置每个图像元素的高宽 200px&#xff1b;以及 10px 边距&#xff0c;以便在图像周围留出空间。 使用 CSS 的 :hover 属性来添加悬停效…

【实战】十一、看板页面及任务组页面开发(四) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十六)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…

2023年京东婴童纸尿裤行业数据分析(京东数据运营)

当前&#xff0c;面对出生率下降、消费疲软等各种大环境不确定性&#xff0c;不仅是线下母婴店深陷于“生意难”的境地&#xff0c;线上消费同样受影响颇深&#xff0c;婴童纸尿裤类目便是如此。下面结合鲸参谋平台的数据&#xff0c;从行业大盘、品牌端等方面来看一下婴童纸尿…

.NETCORE中关于swagger的分组

有些时候我们的项目接口过多&#xff0c;就希望对应的swagger能够执行分组&#xff0c;网络上的几乎是千篇一律的分组方法&#xff0c;会累死&#xff01; 这里提供一个更加高效的分组方法&#xff0c;比如你可以说哪些模块分到哪个组&#xff0c;哪些权限分到哪个组&#xff…