feat: improve mission asset operations
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user