huabu/templates/components/node_macro.html

155 lines
6.5 KiB
HTML
Raw Permalink Normal View History

2026-02-07 00:17:23 +08:00
<!-- 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 %}