konva实现canvas画图基础版本

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>Konva 可疑区域编辑器(支持右键删除)</title>

<style>

body { margin: 0; padding: 20px; font-family: sans-serif; }

#container {

border: 1px solid #ccc;

position: relative;

overflow: auto;

max-width: 100vw;

max-height: 90vh;

}

#controls {

margin-top: 10px;

display: flex;

gap: 10px;

align-items: center;

flex-wrap: wrap;

}

button {

padding: 8px 16px;

}

/* .form-popup, #deleteMenu {

position: fixed;

background: white;

padding: 12px;

box-shadow: 0 4px 12px rgba(0,0,0,0.3);

z-index: 1000;

display: none;

} */

.form-popup label { display: block; margin: 8px 0 4px; }

.form-popup select, .form-popup button,

#deleteMenu button {

width: 100%; padding: 6px; margin: 4px 0;

}

.icon-btn {

width: 40px;

height: 40px;

display: flex;

align-items: center;

justify-content: center;

font-size: 18px;

cursor: pointer;

border: 1px solid #ccc;

background: white;

}

.icon-btn.active {

background: #007bff;

color: white;

}

.form-popup {

position: fixed;

background: white;

padding: 12px;

box-shadow: 0 4px 12px rgba(0,0,0,0.3);

z-index: 1000;

display: none;

left: 50%;

top: 50%;

transform: translate(-50%, -50%);

min-width: 250px;

}

#deleteMenu {

position: fixed;

background: white;

padding: 12px;

box-shadow: 0 4px 12px rgba(0,0,0,0.3);

z-index: 1000;

display: none;

min-width: 120px;

}

</style>

</head>

<body>

<div id="container"></div>

<div id="controls">

<button id="editBtn">进入编辑模式</button>

<button id="undoBtn">撤销 (Ctrl+Z)</button>

<button id="zoomInBtn">放大</button>

<button id="zoomOutBtn">缩小</button>

<div id="zoomDisplay">100%</div>

<div class="icon-btn" id="handBtn" title="拖拽模式">✋</div>

</div>

<!-- 新增区域表单 -->

<div class="form-popup" id="popup">

<label>操作类型:</label>

<select id="opType" disabled>

<option value="add">新增</option>

</select>

<label>瑕疵类型:</label>

<select id="defectType">

<option value="scratch">划痕</option>

<option value="stain">污渍</option>

<option value="dent">凹陷</option>

<option value="irregular">不规则瑕疵</option>

</select>

<button onclick="saveRegion()">确定</button>

<button onclick="cancelRegion()">取消</button>

</div>

<!-- 右键删除菜单 -->

<div id="deleteMenu">

<button onclick="deleteSelectedRegion()">删除区域</button>

</div>

<script src="https://unpkg.com/konva@9/konva.min.js"></script>

<script>

// ========== 模拟后端数据 ==========

const imageUrl = 'https://picsum.photos/1024/768';

// 数据转换函数

function convertPointsFormat(points) {

if (Array.isArray(points) && points.length > 0) {

// 判断是对象数组还是数值数组

if (typeof points[0] === 'object' && points[0] !== null) {

// 对象数组格式 [{x:100,y:200}, ...] 转换为 [100, 200, ...]

return points.flatMap(point => [point.x, point.y]);

} else {

// 已经是一维数组格式,直接返回

return points;

}

}

return [];

}

// 处理后端数据的示例

function processBackendData(backendData) {

return backendData.map(item => ({

id: item.id,

points: convertPointsFormat(item.points),

defectType: item.defectType

}));

}

function generateIrregularShape(cx, cy, outerR = 60, innerR = 25, spikes = 5) {

const points = [];

for (let i = 0; i < spikes * 2; i++) {

const radius = i % 2 === 0 ? outerR : innerR;

const angle = Math.PI / 2 + (i * Math.PI) / spikes;

const x = cx + radius * Math.cos(angle) + (Math.random() - 0.5) * 10;

const y = cy + radius * Math.sin(angle) + (Math.random() - 0.5) * 10;

points.push(x, y);

}

return points;

}

const backendResponse = [

{ id: 1, points: [100,100, 200,100, 200,200, 100,200], defectType: 'scratch' },

{ id: 2, points: [300,150, 350,120, 400,150, 375,200, 325,200], defectType: 'stain' },

{ id: 3, points: generateIrregularShape(600, 200), defectType: 'irregular' },

];

const suspiciousRegions = processBackendData(backendResponse);

console.log("后端返回的数据",suspiciousRegions)

// ========== 全局变量 ==========

let stage, layer;

let imageObj = new Image();

let imgWidth = 0, imgHeight = 0; // 始终为原始图片尺寸

let isEditing = false;

let isDragModeActive = false;

let regions = [];

let currentDrawingPoints = [];

let drawingLine = null;

let drawingAnchors = [];

let history = [];

let tempNewRegion = null;

let scale = 1;

const minScale = 0.1;

const maxScale = 5;

let offsetX = 0;

let offsetY = 0;

let isDraggingAnchor = false;

let currentSelectedRegion = null; // 用于右键删除

// ========== 历史管理 ==========

function addToHistory(operation) {

history.push(operation);

if (history.length > 50) history.shift();

}

document.addEventListener('keydown', (e) => {

if ((e.ctrlKey || e.metaKey) && e.key === 'z') {

e.preventDefault();

undoLastOperation();

}

if (e.key === 'Escape' && isEditing) {

if (currentDrawingPoints.length > 0) {

resetDrawingState();

}

}

});

function undoLastOperation() {

if (currentDrawingPoints.length > 0) {

resetDrawingState();

return;

}

if (history.length === 0) {

alert('没有可撤销的操作');

return;

}

const operation = history.pop();

switch (operation.type) {

case 'addRegion':

const region = createRegion(operation.points, operation.regionId, operation.defectType);

regions.push(region);

layer.batchDraw();

break;

case 'modifyRegion':

const regionToRestore = regions.find(r => r.id === operation.regionId);

if (regionToRestore) {

const scaledOldPoints = operation.oldPoints.map(p => p * scale);

regionToRestore.shape.points(scaledOldPoints);

regionToRestore.points = [...operation.oldPoints];

for (let i = 0; i < regionToRestore.anchors.length; i++) {

const anchor = regionToRestore.anchors[i];

const pointIndex = i * 2;

anchor.x(scaledOldPoints[pointIndex]);

anchor.y(scaledOldPoints[pointIndex + 1]);

}

layer.batchDraw();

}

break;

}

}

// ========== 工具函数 ==========

function createAnchor(x, y, line, index, region, isReadOnly = false) {

const anchor = new Konva.Circle({

x: x,

y: y,

radius: isReadOnly ? 5 : 8,

fill: isReadOnly ? '#ff6600' : '#ff0000',

stroke: '#ffffff',

strokeWidth: isReadOnly ? 1 : 2,

draggable: !isReadOnly,

visible: true,

dragBoundFunc: isReadOnly ? undefined : function (pos) {

return {

x: Math.max(0, Math.min(imgWidth * scale, pos.x)),

y: Math.max(0, Math.min(imgHeight * scale, pos.y)),

};

}

});

if (!isReadOnly) {

let dragStartPoints = null;

anchor.on('dragstart', () => {

isDraggingAnchor = true;

dragStartPoints = line.points().map(p => p / scale);

});

anchor.on('dragmove', function () {

const currentPoints = line.points().slice();

currentPoints[index] = this.x();

currentPoints[index + 1] = this.y();

line.points(currentPoints);

const updatedOriginalPoints = currentPoints.map(p => p / scale);

region.points = [...updatedOriginalPoints];

layer.batchDraw();

});

anchor.on('dragend', () => {

isDraggingAnchor = false;

if (dragStartPoints) {

const dragEndPoints = line.points().map(p => p / scale);

if (JSON.stringify(dragStartPoints) !== JSON.stringify(dragEndPoints)) {

addToHistory({

type: 'modifyRegion',

regionId: region.id,

oldPoints: [...dragStartPoints],

newPoints: [...dragEndPoints]

});

}

}

});

}

return anchor;

}

function createRegion(points, id = Date.now(), defectType = 'scratch') {

const scaledPoints = points.map(p => p * scale);

const line = new Konva.Line({

points: scaledPoints,

closed: true,

stroke: '#ff0000',

strokeWidth: 3,

fill: 'rgba(255,0,0,0.1)',

listening: true,

hitStrokeWidth: 10,

});

const anchors = [];

// ✅ 只在编辑模式下创建锚点

if (isEditing) {

for (let i = 0; i < scaledPoints.length; i += 2) {

const anchor = createAnchor(scaledPoints[i], scaledPoints[i + 1], line, i, { points: [...points], id, defectType }, false);

anchors.push(anchor);

}

}

const region = { id, shape: line, anchors, points: [...points], defectType };

setupHover(region);

return region;

}

function setupHover(region) {

region.shape.on('mouseenter', () => {

region.shape.stroke('#00ff00').strokeWidth(4).fill('rgba(0,255,0,0.2)');

layer.batchDraw();

});

region.shape.on('mouseleave', () => {

region.shape.stroke('#ff0000').strokeWidth(3).fill('rgba(255,0,0,0.1)');

layer.batchDraw();

});

}

// ========== 缩放逻辑(关键修复) ==========

function updateScale(newScale, centerPoint) {

if (isDraggingAnchor) {

setTimeout(() => {

if (!isDraggingAnchor) {

updateScale(newScale, centerPoint);

}

}, 100);

return;

}

const oldScale = scale;

scale = Math.max(minScale, Math.min(maxScale, newScale));

document.getElementById('zoomDisplay').textContent = Math.round(scale * 100) + '%';

if (centerPoint) {

const mousePointTo = {

x: (centerPoint.x - offsetX) / oldScale,

y: (centerPoint.y - offsetY) / oldScale

};

offsetX = centerPoint.x - mousePointTo.x * scale;

offsetY = centerPoint.y - mousePointTo.y * scale;

const maxX = Math.max(0, imgWidth * scale - stage.width());

const maxY = Math.max(0, imgHeight * scale - stage.height());

offsetX = Math.max(-maxX, Math.min(0, offsetX));

offsetY = Math.max(-maxY, Math.min(0, offsetY));

stage.position({ x: offsetX, y: offsetY });

}

const imageNode = layer.findOne('Image');

if (imageNode) {

imageNode.width(imgWidth * scale);

imageNode.height(imgHeight * scale);

}

// 更新已保存区域

regions.forEach(region => {

const currentOriginalPoints = region.shape.points().map(p => p / oldScale);

const newScaledPoints = currentOriginalPoints.map(p => p * scale);

region.shape.points(newScaledPoints);

for (let i = 0; i < region.anchors.length; i++) {

const anchor = region.anchors[i];

const pointIndex = i * 2;

anchor.x(newScaledPoints[pointIndex]);

anchor.y(newScaledPoints[pointIndex + 1]);

}

region.points = [...currentOriginalPoints];

});

// 更新正在绘制中的临时图形

if (drawingLine && currentDrawingPoints.length > 0) {

const newScaledTempPoints = currentDrawingPoints.map(p => p * scale);

drawingLine.points(newScaledTempPoints);

if (drawingAnchors.length === currentDrawingPoints.length / 2) {

for (let i = 0; i < drawingAnchors.length; i++) {

drawingAnchors[i].position({

x: currentDrawingPoints[i * 2] * scale,

y: currentDrawingPoints[i * 2 + 1] * scale

});

}

}

}

layer.batchDraw();

// ✅ 关键:缩放后刷新预览线(防止漂移)

if (isEditing && currentDrawingPoints.length > 0) {

const pos = stage.getPointerPosition();

if (pos) {

const originalX = (pos.x - offsetX) / scale;

const originalY = (pos.y - offsetY) / scale;

const pts = [...currentDrawingPoints, originalX, originalY].map(p => p * scale);

drawingLine.points(pts);

layer.batchDraw();

}

}

}

// ========== 初始化 ==========

function initStage() {

const originalImgWidth = imageObj.naturalWidth;

const originalImgHeight = imageObj.naturalHeight;

const maxWidth = window.innerWidth * 0.9;

const maxHeight = window.innerHeight * 0.96;

let initialDisplayScale = Math.min(maxWidth / originalImgWidth, maxHeight / originalImgHeight);

if (initialDisplayScale > 1) initialDisplayScale = 1;

const displayWidth = originalImgWidth * initialDisplayScale;

const displayHeight = originalImgHeight * initialDisplayScale;

stage = new Konva.Stage({

container: 'container',

width: displayWidth,

height: displayHeight,

});

layer = new Konva.Layer();

stage.add(layer);

const image = new Konva.Image({

x: 0,

y: 0,

image: imageObj,

width: displayWidth,

height: displayHeight,

listening: false,

});

layer.add(image);

// ✅ 保存原始尺寸!

imgWidth = originalImgWidth;

imgHeight = originalImgHeight;

scale = initialDisplayScale;

document.getElementById('zoomDisplay').textContent = Math.round(scale * 100) + '%';

drawRegions(suspiciousRegions);

layer.draw();

bindGlobalEvents();

}

function drawRegions(regionData) {

regions = [];

regionData.forEach(data => {

const region = createRegion(data.points, data.id, data.defectType);

regions.push(region);

layer.add(region.shape);

region.anchors.forEach(a => layer.add(a));

});

}

// 用于检测点是否在多边形内

function isPointInPolygon(point, vs) {

var x = point.x, y = point.y;

var inside = false;

for (var i = 0, j = vs.length - 2; i < vs.length; i += 2) {

var xi = vs[i], yi = vs[i + 1];

var xj = vs[j];

var yj = vs[j + 1];

var intersect = ((yi > y) != (yj > y))

&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);

if (intersect) inside = !inside;

j = i;

}

return inside;

}

// ========== 事件绑定 ==========

function bindGlobalEvents() {

stage.on('wheel', (e) => {

e.evt.preventDefault();

const pointer = stage.getPointerPosition();

const direction = e.evt.deltaY > 0 ? -1 : 1;

const zoomIntensity = 0.1;

const newScale = scale * (1 + direction * zoomIntensity);

updateScale(newScale, pointer);

});

// ✅ 右键删除菜单

stage.on('contextmenu', (e) => {

e.evt.preventDefault();

const pos = stage.getPointerPosition();

if (!pos) return;

const clickedShape = stage.getIntersection(pos);

const clickedRegion = regions.find(r => r.shape === clickedShape);

if (clickedRegion) {

currentSelectedRegion = clickedRegion;

const menu = document.getElementById('deleteMenu');

const x = Math.min(pos.x, window.innerWidth - 120);

const y = Math.min(pos.y, window.innerHeight - 80);

menu.style.left = x + 'px';

menu.style.top = y + 'px';

menu.style.display = 'block';

} else {

hideDeleteMenu();

}

});

stage.on('mousedown', () => hideDeleteMenu());

document.addEventListener('click', (e) => {

if (!e.target.closest('#deleteMenu')) hideDeleteMenu();

});

// 控制按钮

document.getElementById('editBtn').addEventListener('click', toggleEditMode);

document.getElementById('undoBtn').addEventListener('click', undoLastOperation);

document.getElementById('zoomInBtn').addEventListener('click', () => {

const pointer = stage.getPointerPosition() || { x: stage.width() / 2, y: stage.height() / 2 };

updateScale(scale * 1.2, pointer);

});

document.getElementById('zoomOutBtn').addEventListener('click', () => {

const pointer = stage.getPointerPosition() || { x: stage.width() / 2, y: stage.height() / 2 };

updateScale(scale / 1.2, pointer);

});

document.getElementById('handBtn').addEventListener('click', toggleDragMode);

}

// ========== 编辑与拖拽模式 ==========

function toggleEditMode() {

if (isDragModeActive) {

document.getElementById('handBtn').classList.remove('active');

disableDragMode();

}

isEditing = !isEditing;

document.getElementById('editBtn').textContent = isEditing ? '退出编辑' : '进入编辑模式';

const regionData = regions.map(r => ({ id: r.id, points: [...r.points], defectType: r.defectType }));

regions.forEach(r => {

r.shape.destroy();

r.anchors.forEach(a => a.destroy());

});

regions = [];

drawRegions(regionData);

if (isEditing) enableDrawingMode();

else disableDrawingMode();

layer.batchDraw();

}

function enableDragMode() {

isDragModeActive = true;

stage.container().style.cursor = 'grab';

let isDraggingStage = false;

let lastPointerPosition;

stage.on('mousedown touchstart', (e) => {

if (!isDragModeActive) return;

isDraggingStage = true;

lastPointerPosition = stage.getPointerPosition();

stage.container().style.cursor = 'grabbing';

});

stage.on('mousemove touchmove', (e) => {

if (!isDragModeActive || !isDraggingStage) return;

const pos = stage.getPointerPosition();

const dx = pos.x - lastPointerPosition.x;

const dy = pos.y - lastPointerPosition.y;

offsetX += dx;

offsetY += dy;

const maxX = Math.max(0, imgWidth * scale - stage.width());

const maxY = Math.max(0, imgHeight * scale - stage.height());

offsetX = Math.max(-maxX, Math.min(0, offsetX));

offsetY = Math.max(-maxY, Math.min(0, offsetY));

stage.position({ x: offsetX, y: offsetY });

lastPointerPosition = pos;

stage.batchDraw();

});

stage.on('mouseup touchend mouseleave', () => {

isDraggingStage = false;

stage.container().style.cursor = 'grab';

});

}

function disableDragMode() {

isDragModeActive = false;

stage.container().style.cursor = 'default';

stage.off('mousedown touchstart mousemove touchmove mouseup touchend mouseleave');

}

function toggleDragMode() {

if (isEditing) {

isEditing = false;

document.getElementById('editBtn').textContent = '进入编辑模式';

const regionData = regions.map(r => ({ id: r.id, points: [...r.points], defectType: r.defectType }));

regions.forEach(r => {

r.shape.destroy();

r.anchors.forEach(a => a.destroy());

});

regions = [];

drawRegions(regionData);

disableDrawingMode();

}

const handBtn = document.getElementById('handBtn');

if (isDragModeActive) {

handBtn.classList.remove('active');

disableDragMode();

} else {

handBtn.classList.add('active');

enableDragMode();

}

layer.batchDraw();

}

// ========== 绘制新区域 ==========

function enableDrawingMode() {

stage.on('click tap', handleStageClick);

stage.on('mousemove', handleMouseMove);

}

function disableDrawingMode() {

stage.off('click tap', handleStageClick);

stage.off('mousemove', handleMouseMove);

resetDrawingState();

}

function resetDrawingState() {

currentDrawingPoints = [];

if (drawingLine) drawingLine.destroy();

drawingLine = null;

drawingAnchors.forEach(anchor => anchor.destroy());

drawingAnchors = [];

layer.draw();

tempNewRegion = null;

}

function createDrawingAnchor(x, y, isStart = false) {

const anchor = new Konva.Circle({

x: x,

y: y,

radius: isStart ? 9 : 7,

fill: isStart ? '#ff6b6b' : '#4ecdc4',

stroke: '#ffffff',

strokeWidth: 2,

listening: true,

});

if (isStart) {

anchor.on('click', function (e) {

e.cancelBubble = true;

if (!isEditing || currentDrawingPoints.length < 6) return;

finishDrawing([...currentDrawingPoints]);

});

anchor.on('mouseenter', () => {

anchor.stroke('#ffff00').strokeWidth(3);

layer.batchDraw();

});

anchor.on('mouseleave', () => {

anchor.stroke('#ffffff').strokeWidth(2);

layer.batchDraw();

});

}

return anchor;

}

function handleStageClick(e) {

//✅ 只响应左键点击(button === 0)

if (e.evt.button !== 0) {

return; // 忽略右键、中键等

}

const target = e.target;

if (target && (target.className === 'Line' || target.className === 'Circle')) {

const isInExistingRegion = regions.some(r =>

r.shape === target || r.anchors.includes(target)

);

if (isInExistingRegion) return;

}

if (!isEditing) return;

const pos = stage.getPointerPosition();

const originalX = (pos.x - offsetX) / scale;

const originalY = (pos.y - offsetY) / scale;

if (originalX < 0 || originalX > imgWidth || originalY < 0 || originalY > imgHeight) return;

if (currentDrawingPoints.length === 0) {

currentDrawingPoints.push(originalX, originalY);

const scaledX = originalX * scale;

const scaledY = originalY * scale;

const startAnchor = createDrawingAnchor(scaledX, scaledY, true);

drawingAnchors.push(startAnchor);

layer.add(startAnchor);

layer.draw();

return;

}

currentDrawingPoints.push(originalX, originalY);

const scaledX = originalX * scale;

const scaledY = originalY * scale;

const anchor = createDrawingAnchor(scaledX, scaledY, false);

drawingAnchors.push(anchor);

layer.add(anchor);

if (!drawingLine) {

drawingLine = new Konva.Line({

points: currentDrawingPoints.map(p => p * scale),

stroke: '#ffff00',

strokeWidth: 2,

dash: [5, 5],

closed: false,

listening: false,

});

layer.add(drawingLine);

} else {

drawingLine.points(currentDrawingPoints.map(p => p * scale));

}

layer.batchDraw();

}

function handleMouseMove(e) {

if (!isEditing || !drawingLine || currentDrawingPoints.length === 0) return;

const pos = stage.getPointerPosition();

const originalX = (pos.x - offsetX) / scale;

const originalY = (pos.y - offsetY) / scale;

const pts = [...currentDrawingPoints, originalX, originalY].map(p => p * scale);

drawingLine.points(pts);

layer.batchDraw();

}

function finishDrawing(points) {

if (points.length < 6) {

alert('至少需要3个点才能形成区域');

resetDrawingState();

return;

}

const newRegion = createRegion(points, Date.now(), 'scratch');

regions.push(newRegion);

layer.add(newRegion.shape);

newRegion.anchors.forEach(a => layer.add(a));

tempNewRegion = newRegion;

// 不再调用resetDrawingState(),而是手动清理绘制状态但保留tempNewRegion

currentDrawingPoints = [];

if (drawingLine) drawingLine.destroy();

drawingLine = null;

drawingAnchors.forEach(anchor => anchor.destroy());

drawingAnchors = [];

layer.draw();

document.getElementById('popup').style.display = 'block';

}

// ========== 弹窗控制 ==========

function closePopup() {

document.getElementById('popup').style.display = 'none';

}

function saveRegion() {

const defectType = document.getElementById('defectType').value;

if (tempNewRegion) {

tempNewRegion.defectType = defectType;

addToHistory({

type: 'addRegion',

regionId: tempNewRegion.id,

points: [...tempNewRegion.points],

defectType: defectType

});

}

closePopup();

tempNewRegion = null;

layer.batchDraw();

}

function cancelRegion() {

// 清除已完成但未保存的区域(tempNewRegion)

if (tempNewRegion) {

tempNewRegion.shape.destroy();

tempNewRegion.anchors.forEach(anchor => anchor.destroy());

regions = regions.filter(r => r.id !== tempNewRegion.id);

}

// 清除绘制过程中的临时元素

if (drawingLine) {

drawingLine.destroy();

drawingLine = null;

}

drawingAnchors.forEach(anchor => anchor.destroy());

drawingAnchors = [];

currentDrawingPoints = [];

closePopup();

tempNewRegion = null;

layer.batchDraw();

}

// ========== 删除菜单控制 ==========

function hideDeleteMenu() {

document.getElementById('deleteMenu').style.display = 'none';

currentSelectedRegion = null;

}

function deleteSelectedRegion() {

if (!currentSelectedRegion) return;

addToHistory({

type: 'addRegion',

regionId: currentSelectedRegion.id,

points: [...currentSelectedRegion.points],

defectType: currentSelectedRegion.defectType

});

currentSelectedRegion.shape.destroy();

currentSelectedRegion.anchors.forEach(anchor => anchor.destroy());

regions = regions.filter(r => r.id !== currentSelectedRegion.id);

layer.batchDraw();

hideDeleteMenu();

}

// ========== 启动 ==========

imageObj.onload = initStage;

// imageObj.src = imageUrl;

imageObj.src = "http://192.168.77.249:7745/Data/6a8c44a4-b8cd-4e8f-9bf4-55bce793a1b6.jpg";

</script>

</body>

</html>

相关推荐
jingling5552 小时前
Mark3D | 用 Mars3D 实现一个炫酷的三维地图
前端·javascript·3d·前端框架·html
前端白袍2 小时前
Vue:如何实现日志导出下载功能?
javascript·vue.js·ecmascript
这是个栗子2 小时前
【前端知识点总结】请求/响应拦截器的介绍
前端·拦截器
Y‍waiX‍‍‮‪‎⁠‌‫‎‌‫‬2 小时前
【npm】从零到一基于Vite+vue3制作自己的Vue3项目基础的npm包并发布npm
前端·npm·node.js
专注VB编程开发20年2 小时前
vb.net宿主程序通过统一接口直接调用,命名空间要一致
服务器·前端·.net
2503_928411562 小时前
12.18 中后台项目-权限管理
前端·javascript·数据库
Y‍waiX‍‍‮‪‎⁠‌‫‎‌‫‬2 小时前
NRM-NPM的镜像源管理工具使用方法
前端·npm·node.js
hssfscv2 小时前
JAVAweb学习笔记——JS
javascript·笔记·学习
茶憶3 小时前
UniApp 安卓端实现文件的生成,写入,获取文件大小以及压缩功能
android·javascript·vue.js·uni-app