huabu/templates/components/node_macro.html
2026-02-07 00:17:23 +08:00

155 lines
6.5 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Jinja2 Macro: 定义一个通用的节点结构 -->
{% macro render_node(node_data, node_instance_id, x=100, y=100) %}
<div class="node-box" id="{{ node_instance_id }}" data-node-type="{{ node_data.id }}"
style="top: {{ y }}px; left: {{ x }}px; width: 280px;" x-data="{
x: {{ x }},
y: {{ y }},
w: 280,
h: 0,
dragging: false,
resizing: false,
lastMouseX: 0,
lastMouseY: 0,
startDrag(e) {
if (e.target.closest('.node-header')) {
this.dragging = true;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
window.addEventListener('mousemove', this.handleDrag.bind(this));
window.addEventListener('mouseup', this.stopDrag.bind(this));
}
},
handleDrag(e) {
if (this.dragging) {
const zoom = window.currentZoom || 1;
const dx = (e.clientX - this.lastMouseX) / zoom;
const dy = (e.clientY - this.lastMouseY) / zoom;
// 计算连续移动值
this.x += dx;
this.y += dy;
// 应用 30px 的网格吸附
const snapX = Math.round(this.x / 30) * 30;
const snapY = Math.round(this.y / 30) * 30;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
$el.style.left = snapX + 'px';
$el.style.top = snapY + 'px';
if (window.updateLines) window.updateLines();
}
},
stopDrag() {
this.dragging = false;
window.removeEventListener('mousemove', this.handleDrag);
window.removeEventListener('mouseup', this.stopDrag);
},
startResize(e) {
e.preventDefault();
e.stopPropagation();
this.resizing = true;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
// 获取当前宽高作为基准
const rect = $el.getBoundingClientRect();
const zoom = window.currentZoom || 1;
this.w = rect.width / zoom;
this.h = rect.height / zoom;
window.addEventListener('mousemove', this.handleResize.bind(this));
window.addEventListener('mouseup', this.stopResize.bind(this));
},
handleResize(e) {
if (this.resizing) {
const zoom = window.currentZoom || 1;
const dx = (e.clientX - this.lastMouseX) / zoom;
const dy = (e.clientY - this.lastMouseY) / zoom;
this.w += dx;
this.h += dy;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
$el.style.width = Math.max(250, this.w) + 'px';
$el.style.height = Math.max(280, this.h) + 'px';
if (window.updateLines) window.updateLines();
}
},
stopResize() {
this.resizing = false;
window.removeEventListener('mousemove', this.handleResize);
window.removeEventListener('mouseup', this.stopResize);
}
}" @mousedown="startDrag">
<!-- 标题栏 -->
<div class="node-header">
<span class="node-title">{{ node_data.name }}</span>
<div class="header-tools">
<div class="status-dot"></div>
<button class="node-delete-btn" @click.stop="window.removeNode('{{ node_instance_id }}')"
title="删除节点">×</button>
</div>
</div>
<!-- 输入区域 -->
<div class="node-body">
<div class="node-inputs">
{% for input in node_data.inputs %}
<div class="slot-group">
<div class="slot input-slot" id="{{ node_instance_id }}_in_{{ input.name }}"
data-node-id="{{ node_instance_id }}" data-slot-name="{{ input.name }}" data-slot-type="input"
data-data-type="{{ input.data_type }}" onmousedown="startConnectionDrag(event, this)">
<div class="slot-dot"></div>
<label>{{ input.label }}</label>
</div>
{% if input.ui_widget == 'text_area' %}
<textarea name="{{ input.name }}" placeholder="在此输入内容..." class="node-input-field"></textarea>
{% elif input.ui_widget == 'text_input' %}
<input type="text" name="{{ input.name }}" placeholder="在此输入..." class="node-input-field">
{% elif input.ui_widget == 'select' %}
<select name="{{ input.name }}" class="node-input-field">
{% for option in input.options %}
<option value="{{ option }}">{{ option }}</option>
{% endfor %}
</select>
{% elif input.ui_widget == 'file_upload' %}
<label class="file-upload-label">
<input type="file" name="{{ input.name }}" class="hidden-input" hx-post="/api/upload_asset"
hx-encoding="multipart/form-data" hx-target="#content-{{ node_instance_id }}">
<div class="upload-box">
<span class="upload-icon"></span>
<span class="upload-text">点击上传或直接粘贴</span>
</div>
</label>
{% elif input.ui_widget == 'hidden' %}
<input type="hidden" name="{{ input.name }}">
{% endif %}
</div>
{% endfor %}
</div>
<!-- 预览/输出区域 -->
<div class="node-content" id="content-{{ node_instance_id }}">
<div class="placeholder">
<div class="loader-ripple" hx-indicator></div>
<span>等待操作...</span>
</div>
</div>
<!-- 输出插槽 -->
<div class="node-outputs">
{% for output in node_data.outputs %}
<div class="slot output-slot" id="{{ node_instance_id }}_out_{{ output.name }}"
data-node-id="{{ node_instance_id }}" data-slot-name="{{ output.name }}" data-slot-type="output"
data-data-type="{{ output.data_type }}" onmousedown="startConnectionDrag(event, this)">
<label>{{ output.label }}</label>
<div class="slot-dot"></div>
</div>
{% endfor %}
</div>
</div>
<!-- 缩放句柄 -->
<div class="node-resizer" @mousedown="startResize"></div>
</div>
{% endmacro %}