小程序绘制海报
html canvas api
https://blog.csdn.net/web220507/article/details/124925691
https://www.jianshu.com/p/b869580ff48b
小程序绘制海报
https://zhuanlan.zhihu.com/p/603629548?utm_id=0
https://codeleading.com/article/38663680885/
绘制图片
// 图片对象
const image = canvas.createImage()
// 图片加载完成回调
image.onload = () => {
// 将图片绘制到 canvas 上
ctx.drawImage(image,50,50,50,50, 0, 0,100,200)
}
// 设置图片src
image.src = 'https://img0.baidu.com/it/u=1302187753,1022585962&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1699376400&t=8e1590a6a32b9a6746e054af95e34fb8'
http://doofuu.com/article/4156180.html&wd=&eqid=91a2ebf500004a4b0000000664658446
什么是离屏canvas https://blog.csdn.net/zdluoa/article/details/80598326
离屏canvas分享海报: https://blog.csdn.net/qq_41261218/article/details/133958064
生成二维码的插件 weapp-qrcode
https://gitcode.com/mirrors/yingye/weapp-qrcode/overview?utm_source=csdn_github_accelerator
https://blog.csdn.net/qq_56835359/article/details/131443331
小程序新版canvas
Canvas-2d
https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html
weapp-qrcode-canvas-2d 新的canvas生成二维码
https://developers.weixin.qq.com/community/develop/article/doc/000e88e73a415835ed9b46d7956013
https://gitee.com/WeiDoctor/weapp-qrcode-canvas-2d
//---1--- 引入生成二维码的插件
import drawQrcode from 'weapp-qrcode-canvas-2d'
//---2--- 使用插件生成二维码
// 创建离屏 2D canvas 实例
const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 150})
// 使用插件生成二维码
drawQrcode({
canvas: canvas,
canvasId: 'myQrcode',
width: 260,
padding: 30,
background: '#ffffff',
foreground: '#000000',
text: 'abc',
})
//--- 将二维码 canvas 专为二维码图片地址,本可以使用canvasToTempFilePath 但是 在真机测试的时候 报错 所以改为下面的方式
let base64 = canvas.toDataURL();
const time = new Date().getTime();
const imgPath = wx.env.USER_DATA_PATH + "/poster" + time + "share" + ".png";
//如果图片字符串不含要清空的前缀,可以不执行下行代码.
const imageData = base64.replace(/^data:image\/\w+;base64,/, "");
const fs = wx.getFileSystemManager();
fs.writeFileSync(imgPath, imageData, "base64");
fs.close()
// 二维码图片的临时地址
this.setData({
qrcodeUrl:imgPath
})
wxml-to-canvas
使用规则
https://developers.weixin.qq.com/community/develop/article/doc/00040e7a6140f83f904e8468251813
https://developers.weixin.qq.com/community/develop/article/doc/000204eb9f80386c1f80bf25966413
https://developers.weixin.qq.com/community/develop/article/doc/000ac686c5c5506f18b87ee825b013
canvasToTempFilePath在iOS 真机调试时报错 fail invalid viewId
https://developers.weixin.qq.com/community/develop/article/doc/000ce88f738e781b23609445961c13
demo 使用wxml-to-canvas 绘制canvas
生成二维码的插件
npm i weapp-qrcode-canvas-2d
使用wxml-to-canvas
npm install --save wxml-to-canvas
{
"usingComponents": {
"wxml-to-canvas": "wxml-to-canvas",
}
}
//生成二维码的插件
import drawQrcode from 'weapp-qrcode-canvas-2d'
//wxml-to-canvas的 绘制canvas的样式
const style = {
container: {
textAlign:'center',
width: 300,
height: 475,
//------ 可以使用flex布局
// flexDirection: 'row',
// justifyContent: 'space-around',
// backgroundColor: '#ccc',
// alignItems: 'center',
},
imgbg:{//背景图片 图片和文字都要设置宽高值
width: 300,
height: 475,
position:'absolute',
top:0,
left:0
},
contentBox:{
backgroundColor:'#ff0',
height:100,
width:200,
borderWidth:5
},
titleText:{//图片和文字都要设置宽高值
height:100,
width:200,
color:'#000',
fontSize:20,
textAlign: 'left',
verticalAlign: 'middle',
},
imgBox:{
position:'absolute',
top:0,
left:0,
height:90,
width:190,
},
shareText:{//图片和文字都要设置宽高值
height:90,
width:190,
color:"#ff0" ,
fontSize:30
},
qrcode:{ //图片和文字都要设置宽高值
width:200,
height:200
}
}
Page({
data: {
isShowMasker:false,//是否显示遮罩
qrcodeUrl:'',//二维码图片地址
},
renderToCanvas() {
this.widget = this.selectComponent('.widget')
// 创建离屏 2D canvas 实例
const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 150})
// 使用插件生成二维码
drawQrcode({
canvas: canvas,
canvasId: 'myQrcode',
width: 260,
padding: 30,
background: '#ffffff',
foreground: '#000000',
text: 'abc',
})
//--- 将二维码 canvas 专为二维码图片地址,本可以使用canvasToTempFilePath 但是 在真机测试的时候 报错 所以改为下面的方式
let base64 = canvas.toDataURL();
const time = new Date().getTime();
const imgPath = wx.env.USER_DATA_PATH + "/poster" + time + "share" + ".png";
//如果图片字符串不含要清空的前缀,可以不执行下行代码.
const imageData = base64.replace(/^data:image\/\w+;base64,/, "");
const fs = wx.getFileSystemManager();
fs.writeFileSync(imgPath, imageData, "base64");
fs.close()
// 二维码图片的临时地址
this.setData({
qrcodeUrl:imgPath
})
// --------canvas的模版
const wxml = `
<view class="container" >
<image class="imgbg" src="https://picnew13.photophoto.cn/20190310/daqichuangyijihetuxingxiacuxiaohaibao-33122220_1.jpg"></image>
<view class="content-box">
<image class="img-box" src="https://img0.baidu.com/it/u=925843206,3288141497&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=769"></image>
<text class="title-text">你的好友:66 邀请您来观看</text>
</view>
<text class="share-text">分享二维码</text>
<image class="qrcode" src="${this.data.qrcodeUrl}"></image>
</view>
`
// 渲染canvas
const p1 = this.widget.renderToCanvas({ wxml, style })
p1.then((res) => {
// console.log('container****', res)
this.container = res
}).catch(err=>{
console.log('---errhaha',err)
})
},
// 保存图片
extraImage() {
const p2 = this.widget.canvasToTempFilePath()
p2.then(res => {
console.log('=======',res)
// 保存图片
wx.saveImageToPhotosAlbum({
filePath:res.tempFilePath,
success(res) {
wx.showToast({
title: '保存成功',
})
}
})
this.setData({
qrcodeUrl: res.tempFilePath,
width: this.container.layoutBox.width,
height: this.container.layoutBox.height
})
})
},
// 点击 分享海报
changeShowMasker(){
// 显示遮罩
this.setData({
isShowMasker:true
})
setTimeout(()=>{
// 渲染海报
this.renderToCanvas();
},300)
}
})
小程序绘制canvas
绘制canvas 封装好的函数 引入调用即可
//--------- ./uCanvas.js
/**
* 绘制矩形 zw
* 参数:cxt、x坐标、y坐标、宽度、高度、圆角、颜色
*/
function fillRoundRect(cxt, x, y, width, height, radius, fillColor) {
cxt.beginPath();
//圆的直径必然要小于矩形的宽高
if (2 * radius > width || 2 * radius > height) {
return false;
}
cxt.save();
// //绘制圆角矩形的各个边
drawRoundRectPath(x,y,cxt, width, height, radius);
cxt.fillStyle = fillColor || '#000'; //若是给定了值就用给定的值否则给予默认值
cxt.fill();
cxt.restore();
}
// 绘制矩形各个边过程 在fillRoundRect函数中使用了
function drawRoundRectPath(x,y,cxt, width, height, radius) {
cxt.save()
cxt.beginPath(0);
//从右下角顺时针绘制,弧度从0到1/2PI
cxt.arc(x+width - radius, y+height - radius, radius, 0, Math.PI / 2);
//矩形下边线
cxt.lineTo(x+radius, y+height);
//左下角圆弧,弧度从1/2PI到PI
cxt.arc(x+radius, y+height - radius, radius, Math.PI / 2, Math.PI);
//矩形左边线
cxt.lineTo(x+0, y+radius);
//左上角圆弧,弧度从PI到3/2PI
cxt.arc(x+radius, y+radius, radius, Math.PI, (Math.PI * 3) / 2);
//上边线
cxt.lineTo(x+width - radius, y+0);
//右上角圆弧
cxt.arc(x+width - radius, y+radius, radius, (Math.PI * 3) / 2, Math.PI * 2);
//右边线
cxt.lineTo(x+width, y+height - radius);
cxt.closePath();
}
/** 绘制矩形边框 zw
* 参数:cxt、x坐标、y坐标、宽度、高度、圆角、border宽度,border颜色
*/
function roundRectBorder(cxt, x, y, w, h, r, borderWidth = 0,fillColor) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
cxt.beginPath();
cxt.moveTo(x + r, y);
cxt.arcTo(x + w, y, x + w, y + h, r);
cxt.arcTo(x + w, y + h, x, y + h, r);
cxt.arcTo(x, y + h, x, y, r);
cxt.arcTo(x, y, x + w, y, r);
cxt.closePath();
cxt.lineWidth = borderWidth
cxt.strokeStyle=fillColor || '#000'
console.log('borderWidth',borderWidth,fillColor)
cxt.stroke()
}
//加载图片 这个好像在其他函数中没有使用
function getImageInfo(image) {
return new Promise((req, rj) => {
// #ifndef APP-PLUS
wx.getImageInfo({
src: image,
success: function (res) {
req(res)
},
});
// #endif
// #ifdef APP-PLUS
// if (wx.getSystemInfoSync().platform == 'ios') {
// uni.downloadFile({
// url: image,
// success: (res) => {
// res.path = res.tempFilePath
// req(res)
// },
// })
// } else {
// wx.getImageInfo({
// src: image,
// success: function (res) {
// req(res)
// },
// });
// }
// #endif
})
}
/**zw
* 绘制圆形头像 可以webp格式的图片 放在前面会不显示其他绘制过的图形,推荐在调用的时候 放到最后
* 也可以使用drawImgCover 方法绘制头像
* x,y为头像的位置
* 参数:canvas,cxt、图标路径path、x坐标、y坐标、宽度、高度
*/
function drawCircular(canvas,ctx, url, x, y, width, height) {
//画圆形头像
var avatarurl_width = width;
var avatarurl_heigth = height;
var avatarurl_x = x;
var avatarurl_y = y;
ctx.save();
ctx.beginPath();
ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2,
false);
ctx.clip();
// 图片对象
const image = canvas.createImage()
// 图片加载完成回调
image.onload = () => {
ctx.drawImage(image,avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth);
ctx.restore();
}
// 设置图片src
image.src = url
}
/* zw
* 使用场景: 可以用为背景图片 头像 可以设置圆角 圆形
* 绘制图片cover
* canvas
* t:cxt;
* src 图片地址 不能使用webp图片
* s:x坐标
* o:y坐标
* i:绘制图片宽度
* a:绘制图片高度
* radius 圆角
* isTop 图片是否在最上面
*/
function drawImgCover(canvas,t, src, s, o, i, a, radius = 0,isTop) {
let e; // * e:图片属性(通过getImageInfo获取)
//获取图片信息
wx.getImageInfo({
src:src,
success:(res)=>{
e=res;
//创建img
let img = canvas.createImage();
img.src = src;
img.onload = () => {
// 图片为异步 绘制的,后画,所以设置 层叠关系
if(isTop){
console.log('**t*',isTop)
t.globalCompositeOperation = 'source-over'
}else{
console.log('**f*',isTop)
t.globalCompositeOperation = 'destination-over'
}
t.save();
//绘制圆角矩形的各个边
if( radius !== 0){
roundRectBorder(t, s, o, i, a, radius)
t.clip();
}
if (e.width / e.height >= i / a) {
var r = e.height,
n = Math.ceil(i / a * r);
t.drawImage(img, (e.width - n) / 2, 0, n, e.height, s, o, i, a)
} else {
var c = e.width,
l = Math.ceil(a / i * c);
t.drawImage(img, 0, (e.height - l) / 2, e.width, l, s, o, i, a)
}
t.restore();
};
},
fail:(err)=>{
console.log('图片信息失败',err)
}
})
}
/* zw
* 文本自定义换行---已适配英文不截断换行(2022.7.22更新)
* family = " 'PingFang SC',tahoma,arial,'helvetica neue','hiragino sans gb','microsoft yahei',sans-serif";
* var options = {
font:"22px" + family, 字体大小 字体系列
color:'red'
ctx:ctx,
word:"文字", 文字
maxWidth:750, 最大宽度
maxLine:2, 最大行数
x:100, x坐标
y:100, y坐标
l_h:40, 行高
textCenter:false, 是否居中
cvsW:cvsW, 画布总宽--用于计算文字居中
}
* callback 自定义函数
*/
function dealWords(options, callback) {
options.ctx.font = options.font; //设置字体
options.ctx.fillStyle = options.color;
callback && callback();
var allRow = getLineNum(options.ctx, options.word, options.maxWidth)
var allRow = Math.ceil(options.ctx.measureText(options.word).width / options.maxWidth);
//实际总共能分多少行--默认多加一行,预留有英文截断
var count = allRow >= options.maxLine ? options.maxLine : allRow; //实际能分多少行与设置的最大显示行数比,谁小就用谁做循环次数
var endPos = 0; //当前字符串的截断点
for (var j = 0; j < count; j++) {
if (endPos >= options.word.length) {
break;
}
var nowStr = options.word.slice(endPos); //当前剩余的字符串
var rowWid = 0; //每一行当前宽度
if (options.ctx.measureText(nowStr).width > options.maxWidth) { //如果当前的字符串宽度大于最大宽度,然后开始截取
for (var m = 0; m < nowStr.length; m++) {
rowWid += options.ctx.measureText(nowStr[m]).width; //当前字符串总宽度
if (rowWid > options.maxWidth) {
endPos += m; //下次截断点
var isBlock = false //是否被截断了
var num = 1 //前几位数是空'',判断从这个点换行
// 判断最后一个是否字母--判断是否截断了英文
var jy = /^[a-zA-Z]*$/
if (jy.test(nowStr[m - 1])) {
// console.log(m - 1, nowStr[m - 1], '最后一个是字母')
for (var n = 1; n < (m - 1); n++) {
if (!jy.test(nowStr[m - 1 - n])) {
console.log(m - 1 - n, nowStr[m - 1 - n], num, endPos, '这个位置是空字符')
endPos = endPos - num
isBlock = true
break;
}
num++
}
}
if (j === options.maxLine - 1) { //如果是最后一行
options.ctx.fillText(nowStr.slice(0, m - 1) + '...', options.textCenter ? (options.cvsW -
options.ctx.measureText(nowStr.slice(0, m - 1)).width) / 2 : options.x, options.y +
(j + 1) * options.l_h); //(j+1)*18这是每一行的高度
} else {
options.ctx.fillText(nowStr.slice(0, isBlock ? (m - num) : m), options.textCenter ? (options
.cvsW - options.ctx.measureText(nowStr.slice(0, isBlock ? (m - num) : m)).width) /
2 : options.x, options.y + (j + 1) * options.l_h);
}
break;
}
}
} else { //如果当前的字符串宽度小于最大宽度就直接输出
options.ctx.fillText(nowStr.slice(0), options.textCenter ? (options.cvsW - options.ctx.measureText(nowStr
.slice(0)).width) / 2 : options.x, options.y + (j + 1) * options.l_h);
}
}
}
/* zw 在dealWords 函数中使用了
* 计算文字有几行--针对英文换行不截断的计算行数
* ctx:cxt;
* str:文案内容
* maxWidth:文字最大宽度
*/
function getLineNum(ctx, str, maxWidth) {
var allRow = Math.ceil(ctx.measureText(str).width / maxWidth) + 1; //实际总共能分多少行--默认多加一行,预留有英文截断
var endPos = 0; //当前字符串的截断点
var lineNum = 0 //行数
for (var j = 0; j < allRow; j++) {
var nowStr = str.slice(endPos); //当前剩余的字符串
var rowWid = 0; //每一行当前宽度
if (ctx.measureText(nowStr).width > maxWidth) { //如果当前的字符串宽度大于最大宽度,然后开始截取
for (var m = 0; m < nowStr.length; m++) {
rowWid += ctx.measureText(nowStr[m]).width; //当前字符串总宽度
if (rowWid > maxWidth) {
endPos += m; //下次截断点
var isBlock = false //是否被截断了
var num = 1 //前几位数是空'',判断从这个点换行
// 判断最后一个是否字母--判断是否截断了英文
var jy = /^[a-zA-Z]*$/
if (jy.test(nowStr[m - 1])) {
console.log(m - 1, nowStr[m - 1], '最后一个是字母')
for (var n = 1; n < (m - 1); n++) {
if (!jy.test(nowStr[m - 1 - n])) {
console.log(m - 1 - n, nowStr[m - 1 - n], num, endPos, '这个位置是空字符')
endPos = endPos - num
isBlock = true
break;
}
num++
}
}
break;
}
}
} else {
lineNum = j + 1
console.log(nowStr.slice(0), lineNum, j, '最后一行')
break;
}
}
console.log(lineNum, '总行数')
return lineNum
}
/*zw
* 绘制圆角按钮
* ctx:createCanvasContext
* color:背景颜色;
* x:x坐标
* y:y坐标
* width:宽
* height:高
* radius:圆角
* text:文字
* fontColor:文字颜色
* textAlign: left/center/right
* fontStyle:字体的样式 默认样式为: 'normal bold 42px sans-serif'
*/
function drawButton(ctx, color, x, y, width, height, radius, text, fontColor, textAlign,fontStyle) {
//分为4条直线4个圆角绘制
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.arc(x + width - radius, y + radius, radius, Math.PI * 3 / 2, Math.PI * 2);
ctx.lineTo(x + width, y + height - radius);
ctx.arc(x + width - radius, y + height - radius, radius, Math.PI, Math.PI / 2);
ctx.lineTo(x + radius, y + height);
ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI);
ctx.lineTo(x, y + radius);
ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 3 / 2);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.fillStyle = fontColor;
ctx.font = fontStyle?fontStyle:'normal bold 42px sans-serif';
ctx.textAlign=textAlign;
ctx.textBaseline = "middle";
ctx.fillText(text, x + width / 2, y + height / 2);
}
export default {
fillRoundRect: fillRoundRect, //绘制矩形
roundRectBorder: roundRectBorder, //绘制矩形+边框
getImageInfo: getImageInfo, //加载图片
drawCircular: drawCircular, //绘制圆形头像
drawImgCover: drawImgCover, //绘制图片cover
dealWords: dealWords, //文本自定义换行
drawButton: drawButton, //绘制圆角按钮
getLineNum: getLineNum, //计算内容行数
}
//---- drawCanvas/drawCanvas.wxml
<view>
<button type="primary" bind:tap="changeShowMasker">分享给好友</button>
</view>
<!-- 遮罩 -->
<view class="masker" wx:if="{{isShowMasker}}">
<!-- 分享海报 -->
<canvas type="2d" id="myCanvas" width="300" height="400" style="border: 2rpx solid;"></canvas>
<!-- 保存图片 -->
<view>
<button type="primary" bind:tap="extraImage">保存图片</button>
</view>
</view>
//--- drawCanvas/drawCanvas.wxss
.masker{
width: 100%;
height: 100%;
position: fixed;
left: 0;
top:0;
background-color: rgba(0,0,0,.5);
}
#myCanvas{
width: 600rpx;
height: 950rpx;
margin: 30rpx auto;
background-color: #f5efef;
}
//----- drawCanvas/drawCanvas.js
//生成二维码的插件
import drawQrcode from 'weapp-qrcode-canvas-2d'
import uCanvas from './uCanvas.js'
Page({
data: {
isShowMasker:false,//是否显示遮罩
qrcodeUrl:'',//二维码图片地址
},
renderToCanvas() {
//=================1=== 创建二维码
// 创建离屏 2D canvas 实例
const canvas = wx.createOffscreenCanvas({type: '2d', width: 300, height: 150})
// 使用插件生成二维码
drawQrcode({
canvas: canvas,
canvasId: 'myQrcode',
width: 260,
padding: 30,
background: '#ffffff',
foreground: '#000000',
text: 'abc',
})
//--- 将二维码 canvas 专为二维码图片地址,本可以使用canvasToTempFilePath 但是 在真机测试的时候 报错 所以改为下面的方式
let base64 = canvas.toDataURL();
const time = new Date().getTime();
const imgPath = wx.env.USER_DATA_PATH + "/poster" + time + "share" + ".png";
//如果图片字符串不含要清空的前缀,可以不执行下行代码.
const imageData = base64.replace(/^data:image\/\w+;base64,/, "");
const fs = wx.getFileSystemManager();
fs.writeFileSync(imgPath, imageData, "base64");
fs.close()
// 二维码图片的临时地址
this.setData({
qrcodeUrl:imgPath
})
//===============2==== 绘制canvas
const query = wx.createSelectorQuery()
query.select('#myCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
this.canvas=canvas;
const ctx = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = 600 * dpr
canvas.height = 950 * dpr
ctx.scale(dpr, dpr)
//=======2.2==调用封装好的绘制canvas的函数 调用哪些方法视情况而定
// **************
uCanvas.drawImgCover(canvas,ctx,this.data.qrcodeUrl,200,500,200,200,100,true)
uCanvas.drawImgCover(canvas,ctx,'/images/haibao.jpeg',0,0,600,950,50,false)
uCanvas.roundRectBorder(ctx,200,0,100,100,50,2,'blue')
uCanvas.drawButton(ctx,'green',0,0,100,100,10,'提交','blue','center')
uCanvas.dealWords({
font:"30px PingFang SC",
color:'green',
ctx:ctx,
word:"文8的粉sdfv十分广泛发色 阿瑟费的v啊但是阿斯顿 复古v是对方感受到风是电饭锅阿斯顿",
maxWidth:600,
maxLine:3,
x:0,
y:0,
l_h:40,
textCenter:false,
cvsW:600,
})
uCanvas.fillRoundRect(ctx,200,200,200,200,10,'red')
uCanvas.fillRoundRect(ctx,0,700,100,100,0,'rgba(0,0,0,0.6)')
// 绘制圆形图像 推荐放在最后
uCanvas.drawCircular(canvas,ctx,'/images/1122.webp',200,300,500,500)
// **************
})
},
// 保存图片
extraImage() {
//把当前画布指定区域的内容导出生成指定大小的图片
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 600,
height: 950,
destWidth: 600,
destHeight: 950,
canvas: this.canvas,
success(res) {
// console.log(res.tempFilePath)
// 保存图片
wx.saveImageToPhotosAlbum({
filePath:res.tempFilePath,
success(res) {
// 保存图片成功后提示信息
wx.showToast({
title: '保存成功',
})
}
})
}
})
},
// 点击 分享海报
changeShowMasker(){
// 显示遮罩
this.setData({
isShowMasker:true
})
// 创建二维码 绘制canvas
this.renderToCanvas();
}
})
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)