update
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user