Files
KSP_project/app/templates/asset_entry_form.html
T
2026-05-08 15:16:04 +08:00

264 lines
11 KiB
HTML

{% extends "base.html" %}
{% from "_datetime_picker_field.html" import datetime_picker_field %}
{% block content %}
{% include "_mission_ops_tabs.html" %}
{% macro render_end_at_field(end_at_value) %}
{{ datetime_picker_field('End At', 'end_at', end_at_value, field_id='entry-end-at-input', wrapper_class='field-span-two mission-datetime-field', wrapper_id='entry-end-at-field') }}
{% endmacro %}
{% macro render_state_node_row(node, row_index) %}
<div class="state-node-row" data-state-node-row>
<input type="hidden" name="state_node_id" value="{{ node.id if node else '' }}">
<label>
<span class="field-label">Node Title</span>
<input type="text" name="state_node_title" value="{{ node.title if node else '' }}" placeholder="State Node Title">
</label>
{{ datetime_picker_field('Node At', 'state_node_at', node.at if node else '', field_id='state-node-at-' ~ row_index, wrapper_class='state-node-datetime') }}
<label class="state-node-detail-field">
<span class="field-label">Node Detail</span>
<textarea name="state_node_detail" rows="2" placeholder="State Node Detail">{{ node.detail if node else '' }}</textarea>
</label>
<button class="secondary-button state-node-remove-button" type="button" data-state-node-remove>移除</button>
</div>
{% endmacro %}
<section class="hero-panel compact page-header-compact">
<div class="hero-copy">
<p class="eyebrow">资产日志</p>
<h1>{{ heading }}</h1>
</div>
</section>
<form method="post" class="edit-layout">
<input type="hidden" name="sim_time" value="{{ simulation_time_input }}">
<section class="panel panel-wide">
{% set is_event_kind = form_values.entry_kind == 'event' %}
<div class="form-grid resource-form-grid">
<label>
<span class="field-label">Asset</span>
<input type="text" value="{{ asset.name }}" disabled>
</label>
<label>
<span class="field-label">Entry Type</span>
<select id="entry-kind-select" name="entry_kind">
{% for option in kind_options %}
<option value="{{ option.value }}" {% if option.value == form_values.entry_kind %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
</label>
<label>
<span class="field-label">Location</span>
<input type="text" name="location" value="{{ form_values.location }}">
</label>
<label class="field-span-three">
<span class="field-label">Title</span>
<input type="text" name="title" value="{{ form_values.title }}" required>
</label>
<label>
<span class="field-label">State Label</span>
<input type="text" name="state_label" value="{{ form_values.state_label }}">
<small class="field-hint">可单独设置显示标签;多个标签用分号分隔。State Interval 留空时使用 Title。</small>
</label>
<label class="field-span-two">
<span class="field-label">Mission Label</span>
<input type="text" name="mission_label" value="{{ form_values.mission_label }}">
<small class="field-hint">多个 Mission Label 可用分号分隔。</small>
</label>
{{ datetime_picker_field('At' if is_event_kind else 'Start At', 'start_at', form_values.start_at, field_id='entry-start-at-input', required=True, wrapper_class='field-span-two mission-datetime-field', wrapper_id='entry-start-at-field', label_id='entry-start-at-label') }}
{% if not is_event_kind %}
{{ render_end_at_field(form_values.end_at) }}
{% endif %}
<div class="field-span-three state-node-panel" id="state-node-panel" {% if is_event_kind %}hidden{% endif %}>
<div class="state-node-panel-header">
<div>
<span class="field-label">State Nodes</span>
<p class="field-hint">每个 State Node 包含时间、标题和 Node Detail,并会在 Timeline 的 Event Point 泳道中显示。</p>
</div>
<button class="secondary-button" type="button" data-state-node-add>新增 State Node</button>
</div>
<div class="state-node-list" data-state-node-list>
{% for node in form_values.state_nodes %}
{{ render_state_node_row(node, loop.index0) }}
{% endfor %}
</div>
<p class="small-muted state-node-empty" data-state-node-empty {% if form_values.state_nodes %}hidden{% endif %}>当前没有 State Node,可按需添加。</p>
</div>
<label class="field-span-three">
<span class="field-label">Summary</span>
<textarea name="summary" rows="4">{{ form_values.summary }}</textarea>
</label>
<label class="field-span-three">
<span class="field-label">Note</span>
<textarea name="note" rows="4">{{ form_values.note }}</textarea>
</label>
</div>
<div class="button-row top-gap">
<button class="primary-button" type="submit">{{ submit_label }}</button>
<a class="secondary-button" href="{{ cancel_url }}">返回</a>
</div>
</section>
</form>
{% if delete_url %}
<section class="panel danger-panel top-gap">
<p class="small-muted">删除后不可恢复,请谨慎操作。</p>
<form method="post" action="{{ delete_url }}" class="inline-form" onsubmit="return confirm('确认删除这个日志条目?')">
<input type="hidden" name="sim_time" value="{{ simulation_time_input }}">
<button class="danger-link" type="submit">删除条目</button>
</form>
</section>
{% endif %}
<template id="entry-end-at-template">
{{ render_end_at_field(form_values.end_at) }}
</template>
<template id="state-node-row-template">
{{ render_state_node_row(none, '__INDEX__') }}
</template>
<script>
(() => {
const kindField = document.getElementById("entry-kind-select");
const startLabel = document.getElementById("entry-start-at-label");
const startField = document.getElementById("entry-start-at-field");
const endFieldTemplate = document.getElementById("entry-end-at-template");
const stateNodePanel = document.getElementById("state-node-panel");
const stateNodeList = stateNodePanel?.querySelector("[data-state-node-list]");
const stateNodeEmpty = stateNodePanel?.querySelector("[data-state-node-empty]");
const stateNodeAddButton = stateNodePanel?.querySelector("[data-state-node-add]");
const stateNodeRowTemplate = document.getElementById("state-node-row-template");
let endField = document.getElementById("entry-end-at-field");
let endInput = document.getElementById("entry-end-at-input");
let cachedEndValue = endInput?.value || "";
let stateNodeIndex = stateNodeList?.querySelectorAll("[data-state-node-row]").length || 0;
if (!kindField || !startLabel || !startField || !endFieldTemplate) {
return;
}
const refreshEndField = () => {
endField = document.getElementById("entry-end-at-field");
endInput = document.getElementById("entry-end-at-input");
};
const rememberEndValue = () => {
cachedEndValue = endInput?.value || "";
};
const restoreEndValue = () => {
if (!endInput) {
return;
}
endInput.value = cachedEndValue;
};
const ensureEndField = () => {
if (endField) {
restoreEndValue();
return;
}
const templateField = endFieldTemplate.content.firstElementChild;
if (!templateField) {
return;
}
startField.insertAdjacentElement("afterend", templateField.cloneNode(true));
refreshEndField();
restoreEndValue();
window.bindDatetimePickerTriggers?.(endField);
};
const removeEndField = () => {
if (!endField) {
return;
}
rememberEndValue();
endField.remove();
refreshEndField();
};
const stateNodeRows = () => Array.from(stateNodeList?.querySelectorAll("[data-state-node-row]") || []);
const updateStateNodeEmpty = () => {
if (!stateNodeEmpty) {
return;
}
stateNodeEmpty.hidden = stateNodeRows().length > 0;
};
const setStateNodePanelVisible = (visible) => {
if (!stateNodePanel) {
return;
}
stateNodePanel.hidden = !visible;
for (const field of stateNodePanel.querySelectorAll("input[name], textarea[name], select[name]")) {
field.disabled = !visible;
}
if (stateNodeAddButton) {
stateNodeAddButton.disabled = !visible;
}
for (const button of stateNodePanel.querySelectorAll("[data-state-node-remove]")) {
button.disabled = !visible;
}
};
const addStateNodeRow = () => {
if (!stateNodeList || !stateNodeRowTemplate) {
return;
}
const rowMarkup = stateNodeRowTemplate.innerHTML.replaceAll("__INDEX__", String(stateNodeIndex));
stateNodeIndex += 1;
stateNodeList.insertAdjacentHTML("beforeend", rowMarkup);
const row = stateNodeList.lastElementChild;
if (row) {
window.bindDatetimePickerTriggers?.(row);
row.querySelector('input[name="state_node_title"]')?.focus();
}
updateStateNodeEmpty();
};
stateNodeAddButton?.addEventListener("click", addStateNodeRow);
stateNodeList?.addEventListener("click", (event) => {
const removeButton = event.target.closest("[data-state-node-remove]");
if (!removeButton) {
return;
}
removeButton.closest("[data-state-node-row]")?.remove();
updateStateNodeEmpty();
});
const syncEntryKind = () => {
const isEvent = kindField.value === "event";
startLabel.textContent = isEvent ? "At" : "Start At";
if (isEvent) {
removeEndField();
setStateNodePanelVisible(false);
return;
}
ensureEndField();
setStateNodePanelVisible(true);
};
kindField.addEventListener("change", syncEntryKind);
updateStateNodeEmpty();
syncEntryKind();
})();
</script>
{% endblock %}