feat: improve mission asset operations

This commit is contained in:
2026-05-28 11:32:11 +08:00
parent 1a8af63383
commit fee9b13ada
12 changed files with 562 additions and 134 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
{% macro datetime_picker_field(label, name, value='', field_id=None, required=False, wrapper_class='', wrapper_id=None, label_id=None, button_label='选择') %}
{% macro datetime_picker_field(label, name, value='', field_id=None, required=False, wrapper_class='', wrapper_id=None, label_id=None, button_label='选择', error=False) %}
{% set display_value = value|replace('-', '/')|replace('T', ' ') if value else '' %}
{% set picker_id = (field_id or name) ~ '-picker' %}
<label class="mission-datetime-control {{ wrapper_class }}"{% if wrapper_id %} id="{{ wrapper_id }}"{% endif %}>
<label class="mission-datetime-control {{ wrapper_class }}{% if error %} field-error-shell{% endif %}"{% if wrapper_id %} id="{{ wrapper_id }}"{% endif %}{% if error %} data-field-error="true"{% endif %}>
<span class="field-label"{% if label_id %} id="{{ label_id }}"{% endif %}>{{ label }}</span>
<div class="mission-datetime-control-row" data-datetime-control>
<input class="mission-datetime-display-input" type="text" name="{{ name }}" value="{{ display_value }}" placeholder="YYYY/MM/DD HH:MM" autocomplete="off"{% if field_id %} id="{{ field_id }}"{% endif %}{% if required %} required{% endif %}>
<input class="mission-datetime-display-input{% if error %} field-error-input{% endif %}" type="text" name="{{ name }}" value="{{ display_value }}" placeholder="YYYY/MM/DD HH:MM" autocomplete="off"{% if field_id %} id="{{ field_id }}"{% endif %}{% if required %} required{% endif %}>
<input class="mission-datetime-picker-proxy" type="datetime-local" value="{{ value }}" id="{{ picker_id }}" tabindex="-1" aria-hidden="true">
<button class="secondary-button mission-datetime-picker-button" type="button" data-datetime-picker-target="{{ picker_id }}">{{ button_label }}</button>
</div>
+13 -3
View File
@@ -1,16 +1,26 @@
<aside class="panel mission-upcoming-panel">
<div class="panel-heading">
<p class="eyebrow">即将到来</p>
<h2>后续事件</h2>
<h2>未来事件</h2>
</div>
<div class="mission-upcoming-list">
{% if upcoming_events %}
{% for event in upcoming_events %}
<article class="mission-upcoming-item">
<p class="mission-upcoming-date">{{ event.when }}</p>
<h3>{{ event.title }}</h3>
<div class="mission-upcoming-head">
<div>
<p class="mission-upcoming-date">{{ event.when }}</p>
<h3>{{ event.title }}</h3>
</div>
{% if event.edit_url %}
<a class="table-link mission-future-action" href="{{ event.edit_url }}">{{ event.action_label or '编辑条目' }}</a>
{% endif %}
</div>
<p class="small-muted">{{ event.scope }}</p>
{% if event.edit_hint %}
<p class="small-muted">{{ event.edit_hint }}</p>
{% endif %}
</article>
{% endfor %}
{% else %}
+50
View File
@@ -0,0 +1,50 @@
{% if validation_modal %}
<dialog class="validation-dialog" data-validation-dialog>
<form method="dialog" class="validation-dialog-card">
<p class="eyebrow">需要确认</p>
<h2>{{ validation_modal.title }}</h2>
<p class="small-muted">当前输入已保留。关闭这个窗口后,页面会高亮需要修改的位置。</p>
<ul class="validation-dialog-list">
{% for message in validation_modal.messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
<div class="button-row top-gap">
<button class="primary-button" type="submit">返回修改</button>
</div>
</form>
</dialog>
<script>
(() => {
const dialog = document.querySelector("[data-validation-dialog]");
if (!dialog) {
return;
}
const focusFirstInvalid = () => {
const control = document.querySelector(
"[data-field-error='true'] input:not([type='hidden']), [data-field-error='true'] select, [data-field-error='true'] textarea, [data-field-error='true'] button"
);
if (!control) {
return;
}
control.scrollIntoView?.({behavior: "smooth", block: "center"});
control.focus?.({preventScroll: true});
};
dialog.addEventListener("close", () => {
focusFirstInvalid();
});
if (typeof dialog.showModal === "function" && !dialog.open) {
dialog.showModal();
return;
}
dialog.setAttribute("open", "open");
})();
</script>
{% endif %}
+47 -1
View File
@@ -39,10 +39,56 @@
</div>
</section>
<section class="panel mission-future-panel">
<div class="mission-log-top">
<div class="panel-heading">
<p class="eyebrow">未来事件</p>
<h2>可编辑的未来记录</h2>
<p class="small-muted">这里汇总了未来的 Event Point、State Interval,以及当前区间里尚未发生的 State Node。</p>
</div>
<div class="mission-group-count">{{ future_events|length }} future items</div>
</div>
{% if future_events %}
<div class="mission-future-list">
{% for item in future_events %}
<article class="mission-upcoming-item mission-future-item">
<div class="mission-upcoming-head">
<div>
<p class="mission-upcoming-date">{{ item.when }}</p>
<h3>{{ item.title }}</h3>
<p class="small-muted">{{ item.kind_label }} · {{ item.status_label }}</p>
</div>
<a class="table-link mission-future-action" href="{{ item.edit_url }}">{{ item.action_label }}</a>
</div>
<p class="small-muted">{{ item.scope }}</p>
<p class="small-muted mission-future-summary">{{ item.summary }}</p>
{% if item.edit_hint %}
<p class="small-muted">{{ item.edit_hint }}</p>
{% endif %}
{% if item.state_labels or item.mission_labels %}
<div class="mission-asset-chip-row mission-entry-label-row">
{% for label in item.state_labels %}<span class="chip">{{ label }}</span>{% endfor %}
{% for label in item.mission_labels %}<span class="chip">{{ label }}</span>{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</div>
{% else %}
<section class="empty-state compact-empty">
<h2>没有未来记录</h2>
<p>当前选定的 Simulation Time 之后还没有排定的事件。</p>
</section>
{% endif %}
</section>
<section class="panel mission-log-panel">
<div class="mission-log-top">
<div class="panel-heading">
<p class="eyebrow">历史记录</p>
<p class="eyebrow">全部日志</p>
<h2>资产日志</h2>
</div>
<div class="mission-group-count">{{ filtered_entry_count }} / {{ total_entry_count }} entries</div>
+33 -27
View File
@@ -3,19 +3,22 @@
{% 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') }}
{% set field_error_map = field_errors|default({}, true) %}
{% macro render_end_at_field(end_at_value, has_error=False) %}
{{ 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', error=has_error) }}
{% endmacro %}
{% macro render_state_node_row(node, row_index) %}
<div class="state-node-row" data-state-node-row>
{% macro render_state_node_row(node, row_index, field_error_map) %}
{% set title_error = field_error_map.get('state_node_title:' ~ row_index) %}
{% set at_error = field_error_map.get('state_node_at:' ~ row_index) %}
<div class="state-node-row{% if title_error or at_error %} state-node-row-error{% endif %}" data-state-node-row{% if title_error or at_error %} data-field-error="true"{% endif %}>
<input type="hidden" name="state_node_id" value="{{ node.id if node else '' }}">
<label>
<label class="{% if title_error %}field-error-shell{% endif %}"{% if title_error %} data-field-error="true"{% endif %}>
<span class="field-label">Node Title</span>
<input type="text" name="state_node_title" value="{{ node.title if node else '' }}" placeholder="State Node Title">
<input class="{% if title_error %}field-error-input{% endif %}" 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') }}
{{ 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', error=at_error) }}
<label class="state-node-detail-field">
<span class="field-label">Node Detail</span>
@@ -43,69 +46,70 @@
<input type="text" value="{{ asset.name }}" disabled>
</label>
<label>
<label class="{% if field_error_map.get('entry_kind') %}field-error-shell{% endif %}"{% if field_error_map.get('entry_kind') %} data-field-error="true"{% endif %}>
<span class="field-label">Entry Type</span>
<select id="entry-kind-select" name="entry_kind">
<select id="entry-kind-select" name="entry_kind" class="{% if field_error_map.get('entry_kind') %}field-error-input{% endif %}">
{% 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>
<label class="{% if field_error_map.get('location') %}field-error-shell{% endif %}"{% if field_error_map.get('location') %} data-field-error="true"{% endif %}>
<span class="field-label">Location</span>
<input type="text" name="location" value="{{ form_values.location }}">
<input class="{% if field_error_map.get('location') %}field-error-input{% endif %}" type="text" name="location" value="{{ form_values.location }}">
</label>
<label class="field-span-three">
<label class="field-span-three{% if field_error_map.get('title') %} field-error-shell{% endif %}"{% if field_error_map.get('title') %} data-field-error="true"{% endif %}>
<span class="field-label">Title</span>
<input type="text" name="title" value="{{ form_values.title }}" required>
<input class="{% if field_error_map.get('title') %}field-error-input{% endif %}" type="text" name="title" value="{{ form_values.title }}" required>
</label>
<label>
<label class="{% if field_error_map.get('state_label') %}field-error-shell{% endif %}"{% if field_error_map.get('state_label') %} data-field-error="true"{% endif %}>
<span class="field-label">State Label</span>
<input type="text" name="state_label" value="{{ form_values.state_label }}">
<input class="{% if field_error_map.get('state_label') %}field-error-input{% endif %}" type="text" name="state_label" value="{{ form_values.state_label }}">
<small class="field-hint">可单独设置显示标签;多个标签用分号分隔。State Interval 留空时使用 Title。</small>
</label>
<label class="field-span-two">
<label class="field-span-two{% if field_error_map.get('mission_label') %} field-error-shell{% endif %}"{% if field_error_map.get('mission_label') %} data-field-error="true"{% endif %}>
<span class="field-label">Mission Label</span>
<input type="text" name="mission_label" value="{{ form_values.mission_label }}">
<input class="{% if field_error_map.get('mission_label') %}field-error-input{% endif %}" 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') }}
{{ 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', error=field_error_map.get('start_at')) }}
{% if not is_event_kind %}
{{ render_end_at_field(form_values.end_at) }}
{{ render_end_at_field(form_values.end_at, field_error_map.get('end_at')) }}
{% endif %}
<div class="field-span-three state-node-panel" id="state-node-panel" {% if is_event_kind %}hidden{% endif %}>
<div class="field-span-three state-node-panel{% if field_error_map.get('state_nodes') %} field-error-shell{% endif %}" id="state-node-panel" {% if is_event_kind %}hidden{% endif %}{% if field_error_map.get('state_nodes') %} data-field-error="true"{% 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>
<p class="field-hint">State Node 时间必须落在当前 State Interval 的 Start At 和 End At 之间。</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) }}
{{ render_state_node_row(node, loop.index0, field_error_map) }}
{% 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">
<label class="field-span-three{% if field_error_map.get('summary') %} field-error-shell{% endif %}"{% if field_error_map.get('summary') %} data-field-error="true"{% endif %}>
<span class="field-label">Summary</span>
<textarea name="summary" rows="4">{{ form_values.summary }}</textarea>
<textarea class="{% if field_error_map.get('summary') %}field-error-input{% endif %}" name="summary" rows="4">{{ form_values.summary }}</textarea>
</label>
<label class="field-span-three">
<label class="field-span-three{% if field_error_map.get('note') %} field-error-shell{% endif %}"{% if field_error_map.get('note') %} data-field-error="true"{% endif %}>
<span class="field-label">Note</span>
<textarea name="note" rows="4">{{ form_values.note }}</textarea>
<textarea class="{% if field_error_map.get('note') %}field-error-input{% endif %}" name="note" rows="4">{{ form_values.note }}</textarea>
</label>
</div>
@@ -127,13 +131,15 @@
{% endif %}
<template id="entry-end-at-template">
{{ render_end_at_field(form_values.end_at) }}
{{ render_end_at_field(form_values.end_at, false) }}
</template>
<template id="state-node-row-template">
{{ render_state_node_row(none, '__INDEX__') }}
{{ render_state_node_row(none, '__INDEX__', {}) }}
</template>
{% include "_validation_dialog.html" %}
<script>
(() => {
const kindField = document.getElementById("entry-kind-select");
+12 -7
View File
@@ -11,6 +11,8 @@
</div>
</section>
{% set field_error_map = field_errors|default({}, true) %}
<form method="post" class="edit-layout">
{% set hidden_field_values = hidden_fields|default({}, true) %}
{% for name, value in hidden_field_values.items() %}
@@ -22,13 +24,14 @@
<div class="form-grid resource-form-grid">
{% for field in fields %}
{% set current_value = values.get(field.name) %}
{% set has_error = field_error_map.get(field.name) %}
{% if field.kind == 'textarea' %}
<label class="field-span-three">
<label class="field-span-three{% if has_error %} field-error-shell{% endif %}"{% if has_error %} data-field-error="true"{% endif %}>
<span class="field-label">{{ field.label }}</span>
<textarea name="{{ field.name }}" rows="4">{{ current_value if current_value is not none else '' }}</textarea>
<textarea class="{% if has_error %}field-error-input{% endif %}" name="{{ field.name }}" rows="4">{{ current_value if current_value is not none else '' }}</textarea>
</label>
{% elif field.kind == 'checkbox' %}
<label class="checkbox-row">
<label class="checkbox-row{% if has_error %} field-error-shell{% endif %}"{% if has_error %} data-field-error="true"{% endif %}>
<input type="checkbox" name="{{ field.name }}" {% if current_value %}checked{% endif %}>
<span>{{ field.label }}</span>
</label>
@@ -36,9 +39,9 @@
{% set normalized_value = current_value if current_value is not none else '' %}
{% set options = option_map.get(field.name, []) %}
{% set custom_value = '' if not normalized_value or normalized_value in options else normalized_value %}
<label>
<label class="{% if has_error %}field-error-shell{% endif %}"{% if has_error %} data-field-error="true"{% endif %}>
<span class="field-label">{{ field.label }}</span>
<select name="{{ field.name }}" {% if field.required and not custom_value %}required{% endif %}>
<select class="{% if has_error %}field-error-input{% endif %}" name="{{ field.name }}" {% if field.required and not custom_value %}required{% endif %}>
<option value="">请选择</option>
{% for option in options %}
<option value="{{ option }}" {% if option == normalized_value %}selected{% endif %}>{{ option }}</option>
@@ -46,7 +49,7 @@
<option value="{{ combo_custom_option_value }}" {% if custom_value %}selected{% endif %}>新增选项</option>
</select>
<input
class="combo-custom-input"
class="combo-custom-input{% if has_error %} field-error-input{% endif %}"
type="text"
name="{{ field.name }}__custom"
value="{{ custom_value }}"
@@ -54,9 +57,10 @@
>
</label>
{% else %}
<label>
<label class="{% if has_error %}field-error-shell{% endif %}"{% if has_error %} data-field-error="true"{% endif %}>
<span class="field-label">{{ field.label }}</span>
<input
class="{% if has_error %}field-error-input{% endif %}"
type="text"
name="{{ field.name }}"
value="{{ current_value if current_value is not none else '' }}"
@@ -86,4 +90,5 @@
</form>
</section>
{% endif %}
{% include "_validation_dialog.html" %}
{% endblock %}