news 2026/4/14 15:10:08

konva实现canvas画图基础版本

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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: hidden; }
#controls {
margin-top: 10px;
display: flex;
gap: 10px;
align-items: center;
}
button {
padding: 8px 16px;
}
.form-popup {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
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 {
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;
}
</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">
<option value="add">新增</option>
<option value="edit">修改</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>

<script src="https://unpkg.com/konva@9/konva.min.js"></script>
<script>
// ========== 模拟后端数据 ==========
const imageUrl = 'https://picsum.photos/800/600';

// 生成一个复杂不规则形状(例如:扰动的五角星)
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 irregularPoints = generateIrregularShape(600, 200);

const suspiciousRegions = [
{ 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: irregularPoints, defectType: 'irregular' },
];

// ========== 初始化 Konva ==========
const stage = new Konva.Stage({
container: 'container',
width: 800,
height: 600,
});

const layer = new Konva.Layer();
stage.add(layer);

let imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 0,
y: 0,
image: imageObj,
width: 800,
height: 600,
listening: false, // 不监听图片点击
});
layer.add(image);
drawRegions(suspiciousRegions);
layer.draw();
};
imageObj.src = imageUrl;

// ========== 全局状态 ==========
let isEditing = false;
let isDragging = false;
let isDragModeActive = false;
let regions = [];
let currentDrawingPoints = [];
let drawingLine = null;
let drawingAnchors = [];
let history = []; // 操作历史栈
let tempNewRegion = null; // 临时存储新绘制的区域,等待用户确认
let scale = 1; // 当前缩放级别
const minScale = 1; // 100%
const maxScale = 199.9; // 19990%
let offsetX = 0; // X轴偏移
let offsetY = 0; // Y轴偏移
let isDraggingAnchor = false; // 标记是否有锚点正在被拖拽

// ========== 操作历史管理 ==========
function addToHistory(operation) {
history.push(operation);
// 限制历史长度,避免内存过大
if (history.length > 50) {
history.shift();
}
console.log('操作已记录到历史:', operation);
}

function undoLastOperation() {
if (history.length === 0) {
alert('没有可撤销的操作');
return;
}

const operation = history.pop();
console.log('执行撤销操作:', operation.type, '剩余历史:', history.length);

switch (operation.type) {
case 'addRegion':
// 删除最新添加的区域
const regionIndex = regions.findIndex(r => r.id === operation.regionId);
if (regionIndex !== -1) {
const regionToRemove = regions[regionIndex];
regionToRemove.shape.destroy();
regionToRemove.anchors.forEach(anchor => anchor.destroy());
regions.splice(regionIndex, 1);
layer.batchDraw();
}
break;
case 'modifyRegion':
// 恢复到修改前的状态
const regionToRestore = regions.find(r => r.id === operation.regionId);
if (regionToRestore) {
console.log('撤销前状态:', regionToRestore.originalPoints);
console.log('要恢复到状态:', operation.oldPoints);

// 将原始坐标转换为缩放坐标
const scaledOldPoints = operation.oldPoints.map(p => p * scale);

// 1. 设置线条的点
regionToRestore.shape.points(scaledOldPoints);

// 2. 同步更新区域的点数据
regionToRestore.points = [...operation.oldPoints];

// 3. 更新所有锚点的位置
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]);
}

// 4. 重绘
layer.batchDraw();
console.log('区域已恢复到之前状态');
} else {
console.warn('找不到要恢复的区域:', operation.regionId);
}
break;
default:
console.warn('未知操作类型:', operation.type);
}
}

// ========== 绘制所有区域 ==========
function drawRegions(regionData) {
regions = [];
regionData.forEach(data => {
createRegion(data.points, data.id, data.defectType);
});
}

// 创建锚点(用于已存在的区域)
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(800 * scale, pos.x)),
y: Math.max(0, Math.min(600 * scale, pos.y)),
};
}
});

if (!isReadOnly) {
// 只有可编辑时才绑定拖拽事件
let dragStartPoints = null;

anchor.on('dragstart', function () {
isDraggingAnchor = true; // 标记有锚点正在被拖拽
// 保存当前线条的点作为原始状态(转换为原始坐标)
const currentScaledPoints = line.points();
dragStartPoints = [];
for (let i = 0; i < currentScaledPoints.length; i++) {
dragStartPoints.push(currentScaledPoints[i] / scale);
}
console.log('拖拽开始,保存原始点:', dragStartPoints);
});

// 拖拽移动时实时更新
anchor.on('dragmove', function () {
// 获取当前线条的所有点
const currentPoints = line.points().slice(); // 复制数组

// 更新对应索引的点
currentPoints[index] = this.x();
currentPoints[index + 1] = this.y();

// 设置新的点到线条
line.points(currentPoints);

// 同步更新区域的原始坐标点
const updatedOriginalPoints = [];
for (let i = 0; i < currentPoints.length; i++) {
updatedOriginalPoints.push(currentPoints[i] / scale);
}
region.points = [...updatedOriginalPoints];

// 重绘图层
layer.batchDraw();
});

// 拖拽结束时记录历史
anchor.on('dragend', function () {
isDraggingAnchor = false; // 标记锚点拖拽结束
if (dragStartPoints) {
// 获取拖拽结束后的状态(转换为原始坐标)
const currentScaledPoints = line.points();
const dragEndPoints = [];
for (let i = 0; i < currentScaledPoints.length; i++) {
dragEndPoints.push(currentScaledPoints[i] / scale);
}

// 只有当点确实发生变化时才记录
const pointsChanged = JSON.stringify(dragStartPoints) !== JSON.stringify(dragEndPoints);

if (pointsChanged) {
console.log('拖拽结束,原始点:', dragStartPoints, '结束点:', 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,
});

const anchors = [];
for (let i = 0; i < scaledPoints.length; i += 2) {
const anchor = createAnchor(scaledPoints[i], scaledPoints[i + 1], line, i, { points: [...points], id, defectType }, !isEditing);
anchors.push(anchor);
}

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

regions.push(region);

layer.add(line);
anchors.forEach(a => layer.add(a));
setupHover(region);
return region;
}

// ========== 悬停高亮 ==========
function setupHover(region) {
// 鼠标进入区域
region.shape.on('mouseenter', () => {
// 高亮当前区域
region.shape.stroke('#00ff00').strokeWidth(4).fill('rgba(0,255,0,0.2)'); // 悬停时加粗

// 非高亮其他区域
regions.forEach(r => {
if (r !== region) {
r.shape.stroke('#ff0000').strokeWidth(3).fill('rgba(255,0,0,0.1)');
}
});

layer.batchDraw();
});

// 鼠标离开区域
region.shape.on('mouseleave', () => {
// 恢复当前区域样式
region.shape.stroke('#ff0000').strokeWidth(3).fill('rgba(255,0,0,0.1)');

// 恢复其他区域样式
regions.forEach(r => {
if (r !== region) {
r.shape.stroke('#ff0000').strokeWidth(3).fill('rgba(255,0,0,0.1)');
}
});

layer.batchDraw();
});
}

// ========== 精确缩放功能 ==========
function updateScale(newScale, centerPoint) {
// 如果当前有锚点正在被拖拽,推迟缩放操作
if (isDraggingAnchor) {
console.log('有锚点正在被拖拽,推迟缩放操作');
// 稍后尝试再次缩放
setTimeout(() => {
if (!isDraggingAnchor) {
// 如果拖拽已经结束,则执行缩放
updateScale(newScale, centerPoint);
} else {
// 如果拖拽仍在进行,继续等待
console.log('拖拽仍在进行,继续等待');
}
}, 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, 800 * scale - stage.width());
const maxY = Math.max(0, 600 * 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(800 * scale);
imageNode.height(600 * scale);
}

// 重新绘制所有区域
regions.forEach(region => {
// 关键:使用当前线条的实际缩放坐标,而不是原始坐标
// 这样可以保留拖拽后的新坐标
const currentScaledPoints = region.shape.points(); // 获取当前线条的实际缩放坐标

// 重新计算基于新缩放比例的坐标
const currentOriginalPoints = [];
for (let i = 0; i < currentScaledPoints.length; i++) {
currentOriginalPoints.push(currentScaledPoints[i] / oldScale); // 转换回原始坐标
}

// 将原始坐标转换为新缩放级别的坐标
const newScaledPoints = currentOriginalPoints.map(p => p * scale);

// 更新线条
region.shape.points(newScaledPoints);
region.shape.strokeWidth(3); // 保持线条粗细不变
region.shape.fill('rgba(255,0,0,0.1)'); // 保持透明度不变

// 更新锚点位置(保持大小不变)
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];
});

layer.batchDraw();
}

// ========== 鼠标滚轮缩放 ==========
stage.on('wheel', (e) => {
e.evt.preventDefault();

const oldScale = scale;
const pointer = stage.getPointerPosition();

// 计算缩放增量
let direction = e.evt.deltaY > 0 ? -1 : 1;
const zoomIntensity = 0.1; // 缩放强度
const newScale = oldScale * (1 + direction * zoomIntensity);

// 更新缩放,基于鼠标位置
updateScale(newScale, pointer);
});

// ========== 拖拽功能 ==========
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, 800 * scale - stage.width());
const maxY = Math.max(0, 600 * 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', () => {
isDraggingStage = false;
stage.container().style.cursor = 'grab';
});

stage.on('mouseleave', () => {
isDraggingStage = false;
stage.container().style.cursor = 'default';
});
}

function disableDragMode() {
isDragModeActive = false;
stage.container().style.cursor = 'default';
stage.off('mousedown touchstart mousemove touchmove mouseup touchend mouseleave');
}

// ========== 编辑模式切换 ==========
document.getElementById('editBtn').addEventListener('click', () => {
// 退出拖拽模式
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 = [];

// 重新绘制所有区域(根据新的 isEditing 状态)
drawRegions(regionData);

// 处理绘制模式
if (isEditing) {
enableDrawingMode();
} else {
disableDrawingMode();
}

layer.batchDraw();
});

// ========== 撤销功能 ==========
document.getElementById('undoBtn').addEventListener('click', () => {
undoLastOperation();
});

// 监听键盘事件(Ctrl+Z)
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
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', () => {
// 退出编辑模式
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 = [];

// 重新绘制所有区域(根据新的 isEditing 状态)
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();
}

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, // 此次需要监听点击事件
name: isStart ? 'start-anchor' : 'normal-anchor',
});

// 单击起始锚点即闭合(更可靠)
if (isStart) {
anchor.on('click', function (e) {
e.cancelBubble = true; // 防止冒泡到 stage
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) {
// 如果点击的是起始锚点,Konva 会优先触发 anchor 的 click,不会进入这里
// 所以这里只处理"空白区域"或"普通点"的点击
if (e.target?.name() === 'start-anchor') {
return; // 应由 anchor 自己处理
}

if (!isEditing) return;
const pos = stage.getPointerPosition();

// 将屏幕坐标转换为原始图片坐标(缩放前的坐标)
const originalX = (pos.x - offsetX) / scale;
const originalY = (pos.y - offsetY) / scale;

// 第一次点击:记录起点并创建起点锚点
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));
drawingLine.stroke('#ffff00'); // 亮黄色连接线
}

layer.batchDraw();
}

function handleMouseMove(e) {
if (!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];
drawingLine.points(pts.map(p => p * scale)); // 缩放后的点
drawingLine.stroke('#ffff00'); // 亮黄色连接线
layer.batchDraw();
}

function finishDrawing(points) {
if (points.length < 6) {
alert('至少需要3个点才能形成区域');
resetDrawingState();
return;
}

// 创建新的临时区域,但先不加入regions数组
const newRegion = createRegion(points, Date.now(), 'scratch');
tempNewRegion = newRegion; // 临时存储

// 重置绘制状态,但保留新创建的临时区域
resetDrawingState();

// 弹窗让用户确认是否保存
document.getElementById('popup').style.display = 'block';
document.getElementById('opType').value = 'add';
}

// ========== 弹窗 ==========
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]
});

console.log('新区域已保存,瑕疵类型:', defectType);
}

closePopup();
tempNewRegion = null; // 清空临时存储
layer.batchDraw();
}

function cancelRegion() {
if (tempNewRegion) {
// 从画布上移除临时区域
tempNewRegion.shape.destroy();
tempNewRegion.anchors.forEach(anchor => anchor.destroy());

// 从regions数组中移除
const index = regions.findIndex(r => r.id === tempNewRegion.id);
if (index !== -1) {
regions.splice(index, 1);
}

console.log('临时区域已取消');
}

closePopup();
tempNewRegion = null; // 清空临时存储
layer.batchDraw();
}

// 初始化缩放显示
document.getElementById('zoomDisplay').textContent = Math.round(scale * 100) + '%';
</script>
</body>
</html>

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/11 2:00:02

uni-app的代码传参hbuilderx

&#xff08;推荐&#xff09;订单确认页 用户ID → 用 storage 或 eventChannel✅ 推荐写法一&#xff1a;eventChannel&#xff08;最适合你现在这个跳转&#xff09;发送页面uni.navigateTo({url: /pages/order/orderConfirm,success(res) {res.eventChannel.emit(juzhongy…

作者头像 李华
网站建设 2026/4/8 1:16:15

AI原生应用领域:用户意图理解的语音交互技术

AI原生语音交互技术&#xff1a;用户意图理解的理论框架、系统设计与应用实践 关键词 用户意图理解、语音交互系统、自然语言处理&#xff08;NLP&#xff09;、意图分类、多模态融合、上下文感知、对话管理 摘要 本文系统解析AI原生应用中用户意图理解的语音交互技术&#xff…

作者头像 李华
网站建设 2026/4/13 19:38:05

Dify平台在低代码AI开发趋势中的战略定位

Dify平台在低代码AI开发趋势中的战略定位 在企业争相拥抱大模型的今天&#xff0c;一个现实问题日益凸显&#xff1a;如何让AI真正“用起来”&#xff1f;很多公司买了API、搭了算力、招了算法工程师&#xff0c;却发现从想法到上线仍需数周甚至数月。提示词反复调试无效、知识…

作者头像 李华
网站建设 2026/4/12 0:45:09

基于C#实现一维码和二维码打印程序

一、技术选型方案 1. 核心库选择 库名称支持类型特点适用场景ZXing.Net一维/二维条码开源跨平台&#xff0c;支持30编码格式&#xff0c;社区活跃通用型条码解决方案QRCoder二维码专用支持彩色二维码、Logo嵌入、多种输出格式&#xff0c;API简洁高定制化二维码需求TBarCode一…

作者头像 李华
网站建设 2026/4/14 20:44:22

解密Cartographer:多传感器时间同步机制的技术内幕深度剖析

解密Cartographer&#xff1a;多传感器时间同步机制的技术内幕深度剖析 【免费下载链接】cartographer Cartographer is a system that provides real-time simultaneous localization and mapping (SLAM) in 2D and 3D across multiple platforms and sensor configurations. …

作者头像 李华
网站建设 2026/4/12 18:26:46

Moovie.js:打造极致体验的纯JavaScript视频播放器

Moovie.js&#xff1a;打造极致体验的纯JavaScript视频播放器 【免费下载链接】moovie.js Movie focused HTML5 Player 项目地址: https://gitcode.com/gh_mirrors/mo/moovie.js 在当今视频内容爆炸的时代&#xff0c;一个优秀的视频播放器对于网站体验至关重要。Moovie…

作者头像 李华