uniapp-原生地圖截屏返回base64-進行畫板編輯功能

一、場景

vue寫uniapp打包安卓包,實現原生地圖截屏(andirod同事做的)-畫板編輯功能

實現效果:

二、邏輯步驟簡略

1. 由 原生地圖nvue部分,回調返回 地圖截屏生成的base64 數據,

2. 通過 uni插件市場?image-tools 插件?base64ToPath方法,將base64數據 轉成文件路徑

3. 通過 uni -API-?uni.createCanvasContext() 、ctx.createPattern() 方法,將 圖片數據?創建繪圖對象

4. 通過 uni -?movable-area+movable-view 控制畫布縮放

5. 通過?canvas?@touchmove="touchmove"? @touchend="touchend"??@touchstart="touchstart" 等方法實現在畫布上繪制畫筆

6. 生成圖片及清空畫布

三、具體實現

1.??由 原生地圖nvue部分,回調返回 原生地圖截屏生成的base64 數據(andirod同事做的)

2.??image-tools 插件?base64ToPath?

image-tools - DCloud 插件市場

import { pathToBase64, base64ToPath } from '@/js_sdk/mmmm-image-tools/index.js'

3.通過 uni -API-?uni.createCanvasContext() 、ctx.createPattern() 方法

uni-app官網?API-?createPattern()

initC() {const that = this// 創建繪圖對象this.ctx = uni.createCanvasContext('mycanvas', this);// 在canvas設置背景 - 入參 僅支持包內路徑和臨時路徑const pattern = this.ctx.createPattern(this.imageUrl, 'repeat-x')this.ctx.fillStyle = patternthis.ctx.setStrokeStyle('red')this.ctx.fillRect(0, 0, this.dWidth, this.dHeight)this.ctx.draw()// 方法二  在畫布上插入圖片// this.img = new Image();// this.img.src = this.imageUrl;// this.img.onload = () => {//   console.log('this.img', that.img.width)//   that.ctx.drawImage(that.img, 0, 0, this.dWidth, this.dHeight)//   // that.ctx.draw()// }},

4. 通過 uni -?movable-area+movable-view 控制畫布縮放

<movable-area :scale-area="true" :style="{'width':windowWidth+'px','height':windowHeight+'px','backgroundColor':'#ddd','overflow':'hidden'}"><movable-view direction="all":inertia="false":out-of-bounds="false":scale-min="0.001":scale-max="4"   :scale="true":disabled="movableDisabled":scale-value="scaleValue"class="pr":style="{'width':widths+'px','height':heights+'px'}"@scale="scaleChange"><canvasid="mycanvas"canvas-id="mycanvas":style="{'width':widths+'px','height':heights+'px'}"@touchmove="touchmove"@touchend="touchend"@touchstart="touchstart"></canvas></movable-view></movable-area>

5.通過?canvas?@touchmove="touchmove"? 等方法實現在畫布上繪制畫筆

touchstart(e) {let startX = e.changedTouches[0].xlet startY = e.changedTouches[0].yif (this.scaleValue > 1) {startX = e.changedTouches[0].x / this.scaleValue;startY = e.changedTouches[0].y / this.scaleValue;} else {startX = e.changedTouches[0].x * this.scaleValue;startY = e.changedTouches[0].y * this.scaleValue;}console.log('touchstart()-x', e.changedTouches[0].x, 'scaleValue', this.scaleValue, 'startX', startX)let startPoint = { X: startX, Y: startY };this.points.push(startPoint);// 每次觸摸開始,開啟新的路徑this.ctx.beginPath();},touchmove(e) {if (this.isEdit) {let moveX = e.changedTouches[0].xlet moveY = e.changedTouches[0].yif (this.scaleValue > 1) {moveX = e.changedTouches[0].x / this.scaleValue;moveY = e.changedTouches[0].y / this.scaleValue;} else {moveX = e.changedTouches[0].x * this.scaleValue;moveY = e.changedTouches[0].y * this.scaleValue;}console.log('touchmove()-x', e.changedTouches[0].x, 'scaleValue', this.scaleValue, 'moveX', moveX)let movePoint = { X: moveX, Y: moveY };this.points.push(movePoint); // 存點let len = this.points.length;if (len >= 2) {this.draw(); // 繪制路徑}}},touchend() {this.points = [];},draw() {let point1 = this.points[0];let point2 = this.points[1];this.points.shift();this.ctx.moveTo(point1.X, point1.Y);this.ctx.lineTo(point2.X, point2.Y);this.ctx.stroke();this.ctx.draw(true);},

6.生成圖片及清空畫布

clear() {let that = this;this.scaleValue = 1this.isEdit = falsethis.movableDisabled = falseuni.getSystemInfo({success: function(res) {let canvasw = res.windowWidth;let canvash = res.windowHeight;that.ctx.clearRect(0, 0, canvasw, canvash);const pattern = that.ctx.createPattern(that.imageUrl, 'repeat-x')that.ctx.fillStyle = patternthat.dWidth = 285that.dHeight = 200that.ctx.setStrokeStyle('red')that.ctx.fillRect(0, 0, that.dWidth, that.dHeight)that.ctx.draw()// that.ctx.draw(true);}});},finish() {let that = this;uni.canvasToTempFilePath({canvasId: 'mycanvas',success: function(res) {// 這里的res.tempFilePath就是生成的簽字圖片// console.log('tempFilePath', res.tempFilePath);that.tempFilePath = res.tempFilePaththat.$emit('onImgUrl', that.tempFilePath) // 向父級組件傳值}});},

utils:

// 是否是 base64數據
export function isBase64Two(str) {try {return btoa(atob(str)) === str;} catch (err) {return false;}
}
export function isBase64(str) {// 正則表達式匹配B4-64編碼格式const regex = /^[a-zA-Z0-9+\/]+={0,2}$/;return regex.test(str);
}
// 校驗內容是否包含base64格式的圖片
export function isBase64Three(str){let imgReg = RegExp(/data:image\/.*;base64,/)const res = imgReg.test(str)return res
}

四、總結

以下完整代碼 DrawingBoard.vue:

<template><view class="canvas-frame"><view class="icon-frame"><uni-icons :class="{ 'is-edit': isEdit }" type="compose" size="18" class="icon-item mr10" @click="createCanvas">編輯</uni-icons><uni-iconstype="plus"size="18" class="icon-item mr10"title="放大"@click="plusImageScalex"></uni-icons><uni-iconstype="minus"size="18" class="icon-item"title="縮小"@click="minusImageScalex"></uni-icons></view><view class="button-frame"><button size="mini" class="mr10" @click="clear">清空</button><button size="mini" @click="finish">確定</button></view><!-- style="border: 1rpx solid #ccc;width: 570rpx; height: 400rpx;" --><!-- <canvasid="mycanvas"canvas-id="mycanvas":style="{'width':widths+'px','height':heights+'px'}"@touchmove="touchmove"@touchend="touchend"@touchstart="touchstart"></canvas> --><movable-area :scale-area="true" :style="{'width':windowWidth+'px','height':windowHeight+'px','backgroundColor':'#ddd','overflow':'hidden'}"><movable-view direction="all":inertia="false":out-of-bounds="false":scale-min="0.001":scale-max="4"   :scale="true":disabled="movableDisabled":scale-value="scaleValue"class="pr":style="{'width':widths+'px','height':heights+'px'}"@scale="scaleChange"><canvasid="mycanvas"canvas-id="mycanvas":style="{'width':widths+'px','height':heights+'px'}"@touchmove="touchmove"@touchend="touchend"@touchstart="touchstart"></canvas></movable-view></movable-area></view>
</template><script>
// import { fabric } from 'fabric';
// import { fabric } from '@/utils/fabric.min.js';
// import { Database64ToFile } from '@/utils/index';
import { pathToBase64, base64ToPath } from '@/js_sdk/mmmm-image-tools/index.js'
import { isBase64 } from '@/utils/index.js';
// isBase64 方法判斷 原生端返回到的數據格式是否正確
export default {props: {// 更新 原始地圖畫布mapImageUrl: {type: String,default: '',}},data() {return {canvasEle: null,isEdit: false,imageContainer: null,scaleValue: 1,ctx: '', // 繪圖圖像points: [], // 路徑點集合tempFilePath: '', // 簽名圖片imageUrl: require('@/static/res/imgs/all/fushanhou-area.jpg'), // 本地圖片畫布資源img: null,dWidth: 285,dHeight: 200,widths: 285,heights: 200,windowWidth: 285,windowHeight: 200,movableDisabled: false,};},mounted() {this.initC()},watch: {mapImageUrl(newV, oldV) {const that = thisconsole.log('watch()-mapImageUrl-newV,監聽數據變化-newV', newV? '有值': '無值')if (!['',undefined,null].includes(newV)) {console.log('watch()-mapImageUrl-isBase64(newV)', isBase64(newV))// const base64Image = 'data:image/png;base64,/9j/4AAQSkZJRgA...'; // that.base64ToTempFilePath(newV ,(tempFilePath) => {//   console.log('轉換成功,臨時地址為:', tempFilePath)//   that.imageUrl = tempFilePath //   // 會在canvas中調用//   that.initC()// }, // () =>{//   console.log('fail轉換失敗')// });const base64 = 'data:image/png;base64,' + newV;base64ToPath(base64).then((tempFilePath) => {console.log('轉換成功,臨時地址為:', tempFilePath)that.imageUrl = tempFilePaththat.initC()})}},},methods: {initC() {const that = this// 創建繪圖對象this.ctx = uni.createCanvasContext('mycanvas', this);// 在canvas設置背景 - 入參 僅支持包內路徑和臨時路徑const pattern = this.ctx.createPattern(this.imageUrl, 'repeat-x')this.ctx.fillStyle = patternthis.ctx.setStrokeStyle('red')this.ctx.fillRect(0, 0, this.dWidth, this.dHeight)this.ctx.draw()// 方法二  在畫布上插入圖片// this.img = new Image();// this.img.src = this.imageUrl;// this.img.onload = () => {//   console.log('this.img', that.img.width)//   that.ctx.drawImage(that.img, 0, 0, this.dWidth, this.dHeight)//   // that.ctx.draw()// }},createCanvas() {this.isEdit = !this.isEditif (this.isEdit) {this.movableDisabled = true// 設置畫筆樣式this.ctx.lineWidth = 2;this.ctx.lineCap = 'round';this.ctx.lineJoin = 'round';} else {this.movableDisabled = false}},touchstart(e) {let startX = e.changedTouches[0].xlet startY = e.changedTouches[0].yif (this.scaleValue > 1) {startX = e.changedTouches[0].x / this.scaleValue;startY = e.changedTouches[0].y / this.scaleValue;} else {startX = e.changedTouches[0].x * this.scaleValue;startY = e.changedTouches[0].y * this.scaleValue;}console.log('touchstart()-x', e.changedTouches[0].x, 'scaleValue', this.scaleValue, 'startX', startX)let startPoint = { X: startX, Y: startY };this.points.push(startPoint);// 每次觸摸開始,開啟新的路徑this.ctx.beginPath();},touchmove(e) {if (this.isEdit) {let moveX = e.changedTouches[0].xlet moveY = e.changedTouches[0].yif (this.scaleValue > 1) {moveX = e.changedTouches[0].x / this.scaleValue;moveY = e.changedTouches[0].y / this.scaleValue;} else {moveX = e.changedTouches[0].x * this.scaleValue;moveY = e.changedTouches[0].y * this.scaleValue;}console.log('touchmove()-x', e.changedTouches[0].x, 'scaleValue', this.scaleValue, 'moveX', moveX)let movePoint = { X: moveX, Y: moveY };this.points.push(movePoint); // 存點let len = this.points.length;if (len >= 2) {this.draw(); // 繪制路徑}}},touchend() {this.points = [];},draw() {let point1 = this.points[0];let point2 = this.points[1];this.points.shift();this.ctx.moveTo(point1.X, point1.Y);this.ctx.lineTo(point2.X, point2.Y);this.ctx.stroke();this.ctx.draw(true);},clear() {let that = this;this.scaleValue = 1this.isEdit = falsethis.movableDisabled = falseuni.getSystemInfo({success: function(res) {let canvasw = res.windowWidth;let canvash = res.windowHeight;that.ctx.clearRect(0, 0, canvasw, canvash);const pattern = that.ctx.createPattern(that.imageUrl, 'repeat-x')that.ctx.fillStyle = patternthat.dWidth = 285that.dHeight = 200that.ctx.setStrokeStyle('red')that.ctx.fillRect(0, 0, that.dWidth, that.dHeight)that.ctx.draw()// that.ctx.draw(true);}});},finish() {let that = this;uni.canvasToTempFilePath({canvasId: 'mycanvas',success: function(res) {// 這里的res.tempFilePath就是生成的簽字圖片// console.log('tempFilePath', res.tempFilePath);that.tempFilePath = res.tempFilePaththat.$emit('onImgUrl', that.tempFilePath)}});},plusImageScalex() {const num = this.scaleValue + 0.4this.scaleValue = Math.floor(num * 100) / 100;// this.setImageScale(this.scaleValue);},minusImageScalex() {const num = this.scaleValue + 0.4this.scaleValue = - (Math.floor(num * 100) / 100);// this.setImageScale(-this.scaleValue);},// 設置圖片縮放setImageScale(scale) {const that = thisconsole.log('this.ctx.', this.ctx.dWidth, scale)// const value = this.imageContainer.scaleX + scale;// const zoom = Number(value.toFixed(2));// // 設置圖片的縮放比例和位置// this.imageContainer.set({//   scaleX: zoom,//   scaleY: zoom,// });// this.canvasEle.renderAll();// that.ctx.fillRect(0, 0, 285, 200)// that.ctx.draw()const pattern = that.ctx.createPattern(that.imageUrl, 'repeat-x')that.ctx.fillStyle = patternconst w = that.dWidth * scale const h = that.dHeight * scaleconsole.log('this.ctx.',w, h)that.ctx.fillRect(0, 0, w, h)that.ctx.draw()},//點擊事件 判斷縮放比例 touchstart(e) {let x = e.touches[0].xlet y = e.touches[0].y// this.node.forEach(item => {//   if (x > item.x * this.scale && x < (item.x + item.w) * this.scale//       && y > item.y * this.scale && y < (item.y + item.h) * this.scale) {//       //在范圍內,根據標記定義節點類型//       // this.lookDetial(item)//   }// }) },//s縮放比例scaleChange(e) {this.scaleValue = e.detail.scale},// 將base64圖片轉換為臨時地址base64ToTempFilePath(base64Data, success, fail) {const fs = uni.getFileSystemManager()const fileName = 'temp_image_' + Date.now() + '.png' // 自定義文件名,可根據需要修改const USER_DATA_PATH = 'ttfile://user' // uni.env.USER_DATA_PATHconst filePath = USER_DATA_PATH + '/' + fileNameconst buffer = uni.base64ToArrayBuffer(base64Data)fs.writeFile({filePath,data: buffer,encoding: 'binary',success() {success && success(filePath)},fail() { fail && fail()}});},// base64轉化成本地文件路徑parseBlob(base64, success) {const arr = base64.split(',');console.log('parseBlob()-arr:', arr)const mime = arr[0].match(/:(.*?);/)[1];const bstr = atob(arr[1]);const n = bstr.length;const u8arr = new Uint8Array(n);for(let i = 0; i < n; i++) {u8arr[i] = bstr.charCodeAt(i);}// const url = URL || webkitURL;let a = new Blob([u8arr], {type: mime});const file = new File([a], 'test.png', {type: 'image/png'});console.log('parseBlob()-file', file);success && success(file)},}
};
</script><style lang="scss" scoped>
.pr{position: relative;
}
.canvas-frame {position: relative;width: 570rpx;// overflow: hidden;.icon-frame {position: absolute;top: 20rpx;right: 40rpx;z-index: 2;}.blockS{background: transparent;width: 570rpx; height: 400rpx;position: absolute;top: 0;left: 0;z-index: 1;}.icon-item {// font-size: 36rpx;// padding: 12rpx;// border-radius: 8rpx;// margin-right: 16rpx;// border: 1rpx solid #ccc;// background-color: #fff;&:hover {// background-color: #f1f1f1;}&:active {opacity: 0.8;}}.is-edit {color: #007EF3 !important;}.button-frame {position: absolute;bottom: 10rpx;right: 40rpx;z-index: 2;}#canvasElement {cursor: pointer;}
}
</style>

由于hbuildex-真機調試-打印很費勁,需要來回構建打包,從而找問題找了好久,其中因為 原生地圖截屏返回的是純base64的數據,未帶 data:image\/.*;base64,然后找了半天的問題,需要一步步的推導和確認有沒有錯,錯在那,花費了很多時間和精力;

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/36236.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/36236.shtml
英文地址,請注明出處:http://en.pswp.cn/news/36236.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

《圖解HTTP》——HTTP協議詳解

一、HTTP協議概述 HTTP是一個屬于應用層的面向對象協議&#xff0c;由于其簡捷、快速的方式&#xff0c;適用于分布式超媒體信息系統。它于1990年提出&#xff0c;經過幾年的使用與發展&#xff0c;得到不斷地完善和擴展。目前在WWW中使用的是HTTP/1.0的第六版&#xff0c;HTTP…

muduo 29 異步日志

目錄 Muduo雙緩沖異步日志模型: 異步日志實現: 為什么要實現非阻塞的日志

SQL 語句解析過程詳解

SQL 語句解析過程詳解&#xff1a; 1&#xff0e;輸入SQL語句 2&#xff0e;詞法分析------flex 使用詞法分析器&#xff08;由Flex生成&#xff09;將 SQL 語句分解為一個個單詞&#xff0c;這些單詞被稱為“標記“。標記包括關鍵字、標識符、運算符、分隔符等。 2.1 flex 原…

【CSS 布局】水平垂直方向居中

【CSS 布局】水平垂直方向居中 單行元素 <div class"container"><div class"item"></div> </div>方式一&#xff1a;relative 和 absolute .container {position: relative;height: 400px;border: 1px solid #ccc;.item {posit…

20個互聯網用戶Python數據分析項目

這篇文章給大家整理了20個互聯網用戶數據分析的項目。所有收錄的項目&#xff0c;進行了嚴格的篩選&#xff0c;標準有二&#xff1a; 1.有解說性文字&#xff0c;大家能知道每一步在干嘛&#xff0c;新手友好 2.數據集公開&#xff0c;保證大家可以在原文的基礎上自行探索 更…

[保研/考研機試] KY96 Fibonacci 上海交通大學復試上機題 C++實現

題目鏈接&#xff1a; KY96 Fibonacci https://www.nowcoder.com/share/jump/437195121692000803047 描述 The Fibonacci Numbers{0,1,1,2,3,5,8,13,21,34,55...} are defined by the recurrence: F00 F11 FnFn-1Fn-2,n>2 Write a program to calculate the Fibon…

【STM32】FreeRTOS互斥量學習

互斥量&#xff08;Mutex&#xff09; 互斥量又稱互斥信號量&#xff08;本質也是一種信號量&#xff0c;不具備傳遞數據功能&#xff09;&#xff0c;是一種特殊的二值信號量&#xff0c;它和信號量不同的是&#xff0c;它支持互斥量所有權、遞歸訪問以及防止優先級翻轉的特性…

人文景區有必要做VR云游嗎?如何滿足游客出行需求?

VR云游在旅游行業中的應用正在快速增長&#xff0c;為游客帶來沉浸式體驗的同時&#xff0c;也為文旅景區提供了新的營銷方式。很多人說VR全景展示是虛假的&#xff0c;比不上真實的景區觸感&#xff0c;人文景區真的有必要做VR云游嗎&#xff1f;我的答案是很有必要。 如果你認…

【跟小嘉學 Rust 編程】十二、構建一個命令行程序

系列文章目錄 【跟小嘉學 Rust 編程】一、Rust 編程基礎 【跟小嘉學 Rust 編程】二、Rust 包管理工具使用 【跟小嘉學 Rust 編程】三、Rust 的基本程序概念 【跟小嘉學 Rust 編程】四、理解 Rust 的所有權概念 【跟小嘉學 Rust 編程】五、使用結構體關聯結構化數據 【跟小嘉學…

【Pytroch】基于K鄰近算法的數據分類預測(Excel可直接替換數據)

【Pytroch】基于K鄰近算法的數據分類預測&#xff08;Excel可直接替換數據&#xff09; 1.模型原理2.數學公式3.文件結構4.Excel數據5.下載地址6.完整代碼7.運行結果 1.模型原理 K最近鄰&#xff08;K-Nearest Neighbors&#xff0c;簡稱KNN&#xff09;是一種簡單但常用的機器…

Redis基礎學習

目錄 第一章、Redis數據庫的下載和安裝1.1&#xff09;nosql數據庫和 Redis 介紹1.2&#xff09;Windows中下載安裝Redis數據庫1.3&#xff09;Linux中安裝Redis數據庫1.4&#xff09;Linux中啟動redis1.5&#xff09;Linux中關閉redis 第二章、三種Redis客戶端連接Redis數據庫…

安全遠控如何設置?揭秘ToDesk、TeamViewer 、向日葵安全遠程防御大招

寫在前面一、遠程控制&#xff1a;安全性不可忽略二、遠控軟件安全設置實測? ToDesk? TeamViewer? 向日葵 三、遠控安全的亮點功能四、個人總結與建議 寫在前面 說到遠程辦公&#xff0c;相信大家都不陌生。遠程工作是員工在家中或者其他非辦公場所上班的一種工作模式&…

傳輸層協議

傳輸層協議 再談端口號端口號范圍劃分認識知名端口號兩個問題netstatpidof UDP協議UDP協議端格式UDP的特點面向數據報UDP的緩沖區UDP使用注意事項基于UDP的應用層協議 TCP協議TCP協議段格式確認應答(ACK)機制超時重傳機制連接管理機制理解 CLOSE_WAIT 狀態理解TIME_WAIT狀態解決…

修改el-select和el-input樣式;修改element-plus的下拉框el-select樣式

修改el-select樣式 .select_box{// 默認placeholder:deep .el-input__inner::placeholder {font-size: 14px;font-weight: 500;color: #3E534F;}// 默認框狀態樣式更改:deep .el-input__wrapper {height: 42px;background-color: rgba(0,0,0,0)!important;box-shadow: 0 0 0 …

OptaPlanner筆記6 N皇后

N 個皇后 問題描述 將n個皇后放在n大小的棋盤上&#xff0c;沒有兩個皇后可以互相攻擊。 最常見的 n 個皇后謎題是八個皇后謎題&#xff0c;n 8&#xff1a; 約束&#xff1a; 使用 n 列和 n 行的棋盤。在棋盤上放置n個皇后。沒有兩個女王可以互相攻擊。女王可以攻擊同一水…

如何做好一名網絡工程師?具體實踐?

預防問題 – 資格與認證 在安裝線纜或升級網絡時測試線纜是預防問題的有效方式。對已安裝布線進行測試的方法有兩種。 資格測試確定布線是否有資格執行某些操作 — 換言之&#xff0c;支持特定網絡速度或應用。盡管“通過”認證測試也表明按標準支持某一網絡速度或應用的能力…

Redux - Redux在React函數式組件中的基本使用

文章目錄 一&#xff0c;簡介二&#xff0c;安裝三&#xff0c;三大核心概念Store、Action、Reducer3.1 Store3.2 Reducer3.3 Action 四&#xff0c;開始函數式組件中使用4.1&#xff0c;引入store4.1&#xff0c;store.getState()方法4.3&#xff0c;store.dispatch()方法4.4&…

cookie和session的區別及原理

Cookie概念 在瀏覽某些 網站 時,這些網站會把 一些數據存在 客戶端 , 用于使用網站 等跟蹤用戶,實現用戶自定義 功能. 是否設置過期時間: 如果不設置 過期時間,則表示這個 Cookie生命周期為 瀏覽器會話期間 , 只要關閉瀏覽器,cookie就消失了. 這個生命期為瀏覽會話期的cookie,就…

其他行業跳槽轉入計算機領域簡單看法

其他行業跳槽轉入計算機領域簡單看法 本人選擇從以下幾個方向談談自己的想法和觀點。 如何規劃才能實現轉碼 自我評估和目標設定&#xff1a;首先&#xff0c;你需要評估自己的技能和興趣&#xff0c;確定你希望在計算機領域從事的具體職位或領域。這有助于你更好地規劃學習路…

深入了解 Rancher Desktop 設置

Rancher Desktop 設置的全面概述 Rancher Desktop 擁有方便、強大的功能&#xff0c;是最佳的開發者工具之一&#xff0c;也是在本地構建和部署 Kubernetes 的最快捷方式。 本文將介紹 Rancher Desktop 的功能和特性&#xff0c;以及 Rancher Desktop 作為容器管理平臺和本地…