155 lines
6.5 KiB
HTML
155 lines
6.5 KiB
HTML
|
|
<!-- 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 %}
|