update
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{% macro datetime_picker_field(label, name, value='', field_id=None, required=False, wrapper_class='', wrapper_id=None, label_id=None, button_label='选择') %}
|
||||
{% 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 %}>
|
||||
<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-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>
|
||||
</label>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<section class="panel mission-ops-tabs-panel">
|
||||
<nav class="mission-preview-nav mission-preview-nav-top" aria-label="任务运营二级导航">
|
||||
<a class="preview-tab {% if preview_tab == 'status' %}active{% endif %}" href="{{ board_nav_urls.status }}">状态板</a>
|
||||
<a class="preview-tab {% if preview_tab == 'location' %}active{% endif %}" href="{{ board_nav_urls.location }}">地点板</a>
|
||||
<a class="preview-tab {% if preview_tab == 'mission' %}active{% endif %}" href="{{ board_nav_urls.mission }}">任务</a>
|
||||
<a class="preview-tab {% if preview_tab == 'asset' %}active{% endif %}" href="{{ board_nav_urls.asset }}">任务资产</a>
|
||||
<a class="preview-tab {% if preview_tab == 'timeline' %}active{% endif %}" href="{{ board_nav_urls.timeline }}">时间线</a>
|
||||
</nav>
|
||||
</section>
|
||||
@@ -0,0 +1,40 @@
|
||||
<section class="panel mission-control-panel top-gap">
|
||||
<div class="mission-toolbar-grid">
|
||||
<div class="mission-toolbar-block">
|
||||
<span class="field-label">Time Controls</span>
|
||||
<div class="mission-toolbar-inline">
|
||||
<input type="text" value="{{ filters.simulation_time }}" readonly>
|
||||
<button class="secondary-button" type="button">- 7d</button>
|
||||
<button class="secondary-button" type="button">+ 7d</button>
|
||||
<button class="primary-button" type="button">Set Simulation Time</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-toolbar-block">
|
||||
<span class="field-label">Board Filters</span>
|
||||
<div class="mission-filter-grid">
|
||||
<label>
|
||||
<span class="field-label">Asset Type</span>
|
||||
<select>
|
||||
<option>{{ filters.asset_type }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">Location</span>
|
||||
<select>
|
||||
<option>{{ filters.location }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">Records</span>
|
||||
<select>
|
||||
<option>{{ filters.record_scope }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary-button" type="button">Apply</button>
|
||||
<button class="secondary-button" type="button">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,40 @@
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
{% set header_metrics = metrics if metrics is defined else [] %}
|
||||
{% set header_eyebrow = preview_eyebrow if preview_eyebrow is defined else 'Mission Ops' %}
|
||||
{% set time_action = header_time_action if header_time_action is defined else None %}
|
||||
{% set time_submit_label = header_time_submit_label if header_time_submit_label is defined else 'Apply Time' %}
|
||||
{% set time_field_id = header_time_field_id if header_time_field_id is defined else 'page-header-sim-time' %}
|
||||
{% set hidden_fields = header_hidden_fields if header_hidden_fields is defined else [] %}
|
||||
|
||||
<section class="panel mission-page-header">
|
||||
<div class="mission-page-header-copy">
|
||||
<p class="eyebrow">{{ header_eyebrow }}</p>
|
||||
<h1>{{ preview_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mission-page-header-metrics">
|
||||
{% for metric in header_metrics %}
|
||||
<article class="summary-card compact mission-summary-card">
|
||||
<span class="summary-label">{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
<article class="summary-card compact mission-summary-card mission-summary-card-time">
|
||||
<span class="summary-label">Current Simulation Time</span>
|
||||
<strong>{{ simulation_time_display or simulation_time }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="mission-page-header-controls">
|
||||
{% if time_action and simulation_time_input %}
|
||||
<form method="get" action="{{ time_action }}" class="mission-page-time-form">
|
||||
{% for field in hidden_fields %}
|
||||
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
|
||||
{% endfor %}
|
||||
{{ datetime_picker_field('Edit Time', 'sim_time', simulation_time_input, field_id=time_field_id, wrapper_class='mission-page-inline-datetime') }}
|
||||
<button class="secondary-button" type="submit">{{ time_submit_label }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,23 @@
|
||||
<aside class="panel mission-upcoming-panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">即将到来</p>
|
||||
<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>
|
||||
<p class="small-muted">{{ event.scope }}</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<article class="mission-upcoming-item">
|
||||
<p class="mission-upcoming-date">-</p>
|
||||
<h3>暂无后续事件</h3>
|
||||
</article>
|
||||
{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
@@ -0,0 +1,185 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
|
||||
<section class="mission-detail-layout top-gap">
|
||||
<div class="mission-detail-main">
|
||||
<section class="panel mission-asset-overview">
|
||||
<div class="mission-asset-head">
|
||||
<div>
|
||||
<p class="eyebrow">Asset Summary</p>
|
||||
<h2>{{ asset.name }}</h2>
|
||||
<p class="small-muted">{{ asset.asset_type }} · {{ asset.program or 'No program assigned' }}</p>
|
||||
</div>
|
||||
<div class="mission-asset-chip-row">
|
||||
<span class="chip strong">{{ asset_summary.current_state }}</span>
|
||||
<span class="chip">{{ asset_summary.current_location }}</span>
|
||||
{% if asset.home_region %}
|
||||
<span class="chip">{{ asset.home_region }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-meta-grid">
|
||||
<div class="mission-meta-item">
|
||||
<dt>Current Mission</dt>
|
||||
<dd>{{ asset_summary.current_mission }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Active Window</dt>
|
||||
<dd>{{ asset_summary.state_window }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item field-span-two">
|
||||
<dt>Next Planned Event</dt>
|
||||
<dd>{{ asset_summary.next_event }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mission-log-panel">
|
||||
<div class="mission-log-top">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">历史记录</p>
|
||||
<h2>资产日志</h2>
|
||||
</div>
|
||||
<div class="mission-group-count">{{ filtered_entry_count }} / {{ total_entry_count }} entries</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="mission-log-filter-grid">
|
||||
<input type="hidden" name="sim_time" value="{{ simulation_time_input }}">
|
||||
{{ datetime_picker_field('Start', 'start', entry_filter_start_input, field_id='asset-log-start') }}
|
||||
{{ datetime_picker_field('End', 'end', entry_filter_end_input, field_id='asset-log-end') }}
|
||||
<button class="primary-button" type="submit">筛选</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.asset_detail', asset_id=asset.id, sim_time=simulation_time_input) }}">重置</a>
|
||||
</form>
|
||||
|
||||
{% if entries %}
|
||||
<div class="table-shell">
|
||||
<table class="data-table compact-table mission-log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Entry</th>
|
||||
<th>Labels</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td class="two-line-cell">
|
||||
<strong class="table-title">{{ entry.timestamp }}</strong>
|
||||
{% if entry.end_timestamp %}
|
||||
<div class="table-subline">→ {{ entry.end_timestamp }}</div>
|
||||
{% endif %}
|
||||
<div class="table-subline single-line-cell">{{ entry.location }}</div>
|
||||
</td>
|
||||
<td class="two-line-cell">
|
||||
<strong class="table-title">{{ entry.title }}</strong>
|
||||
<div class="table-subline">{{ entry.mode_label }} · {{ entry.status_label }}</div>
|
||||
<div class="table-subline clamp-line">{{ entry.summary }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if entry.state_labels or entry.mission_labels %}
|
||||
<div class="mission-asset-chip-row mission-entry-label-row">
|
||||
{% for label in entry.state_labels %}<span class="chip">{{ label }}</span>{% endfor %}
|
||||
{% for label in entry.mission_labels %}<span class="chip">{{ label }}</span>{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="small-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.asset_entry_edit', asset_id=asset.id, entry_id=entry.id, sim_time=simulation_time_input) }}">编辑条目</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if entry_total_pages > 1 %}
|
||||
<nav class="pagination-bar" aria-label="Asset timeline pagination">
|
||||
{% if entry_has_prev %}
|
||||
<a class="pagination-link" href="{{ entry_prev_url }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="pagination-pages">
|
||||
{% for item in entry_page_links %}
|
||||
{% if item.ellipsis %}
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% elif item.current %}
|
||||
<span class="pagination-link current">{{ item.number }}</span>
|
||||
{% else %}
|
||||
<a class="pagination-link" href="{{ item.url }}">{{ item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if entry_has_next %}
|
||||
<a class="pagination-link" href="{{ entry_next_url }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">下一页</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
{% if total_entry_count %}
|
||||
<h2>没有命中筛选条件的日志条目</h2>
|
||||
<p>调整起止时间,或者重置筛选后重试。</p>
|
||||
{% else %}
|
||||
<h2>还没有日志条目</h2>
|
||||
<p>先为这个资产新增一个 Event Point 或 State Interval。</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="mission-detail-side">
|
||||
<section class="panel mission-current-state-panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">当前状态</p>
|
||||
<h2>当前派生状态</h2>
|
||||
</div>
|
||||
<div class="mission-meta-grid">
|
||||
<div class="mission-meta-item">
|
||||
<dt>Status</dt>
|
||||
<dd>{{ asset_summary.current_state }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Location</dt>
|
||||
<dd>{{ asset_summary.current_location }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item field-span-two">
|
||||
<dt>Mission</dt>
|
||||
<dd>{{ asset_summary.current_mission }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include "_mission_upcoming_events.html" %}
|
||||
|
||||
<section class="panel mission-entry-actions-panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">操作</p>
|
||||
<h2>日志操作</h2>
|
||||
</div>
|
||||
<div class="mission-action-stack">
|
||||
<a class="primary-button" href="{{ url_for('web.asset_entry_new', asset_id=asset.id, kind='event', sim_time=simulation_time_input) }}">新增事件点</a>
|
||||
<a class="primary-button" href="{{ url_for('web.asset_entry_new', asset_id=asset.id, kind='state', sim_time=simulation_time_input) }}">新增状态区间</a>
|
||||
<a class="secondary-button" href="{{ url_for('web.asset_edit', asset_id=asset.id, sim_time=simulation_time_input) }}">编辑资产</a>
|
||||
<a class="secondary-button" href="{{ url_for('web.asset_list', sim_time=simulation_time_input) }}">返回任务资产</a>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
|
||||
<section class="panel catalog-actions-panel top-gap">
|
||||
<div>
|
||||
<p class="eyebrow">Actions</p>
|
||||
<h2>新增内容</h2>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="primary-button" href="{{ url_for('web.asset_new', sim_time=simulation_time_input) }}">新增任务资产</a>
|
||||
<form method="post" action="{{ url_for('web.asset_import_log_book') }}" class="inline-form">
|
||||
<input type="hidden" name="sim_time" value="{{ simulation_time_input }}">
|
||||
<button class="secondary-button" type="submit">导入 Log Book</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide top-gap">
|
||||
<form class="search-form filter-row" method="get">
|
||||
<input type="search" name="q" value="{{ search_term }}" placeholder="Asset / Type / Program / Region">
|
||||
|
||||
<select name="asset_type">
|
||||
<option value="">全部类型</option>
|
||||
{% for option in asset_type_options %}
|
||||
<option value="{{ option }}" {% if option == asset_type_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="sort">
|
||||
{% for option in sort_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == sort_option %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.asset_list') }}">重置</a>
|
||||
</form>
|
||||
|
||||
{% if rows %}
|
||||
<div class="table-shell top-gap">
|
||||
<table class="data-table compact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Type</th>
|
||||
<th>Program</th>
|
||||
<th>Current Event</th>
|
||||
<th>Next Event</th>
|
||||
<th>Entries</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a class="table-link" href="{{ url_for('web.asset_detail', asset_id=row.asset.id, sim_time=simulation_time_input) }}">{{ row.asset.name }}</a></strong>
|
||||
</td>
|
||||
<td>{{ row.asset.asset_type }}</td>
|
||||
<td>{{ row.asset.program or '-' }}</td>
|
||||
<td>{{ row.current_event }}</td>
|
||||
<td>{{ row.next_event }}</td>
|
||||
<td>{{ row.entry_count }}</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.asset_edit', asset_id=row.asset.id, sim_time=simulation_time_input) }}">编辑</a>
|
||||
<a class="table-link" href="{{ url_for('web.asset_entry_new', asset_id=row.asset.id, sim_time=simulation_time_input) }}">加条目</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination-bar" aria-label="Asset pagination">
|
||||
{% if has_prev %}
|
||||
<a class="pagination-link" href="{{ prev_url }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="pagination-pages">
|
||||
{% for item in page_links %}
|
||||
{% if item.ellipsis %}
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% elif item.current %}
|
||||
<span class="pagination-link current">{{ item.number }}</span>
|
||||
{% else %}
|
||||
<a class="pagination-link" href="{{ item.url }}">{{ item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if has_next %}
|
||||
<a class="pagination-link" href="{{ next_url }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">下一页</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
<h2>没有命中结果</h2>
|
||||
<p>调整筛选条件或重置后重试。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,284 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>{{ title }} · {{ config["PROJECT_TITLE"] }}</title>
|
||||
<script>
|
||||
(() => {
|
||||
const storageKey = 'ksp-theme';
|
||||
const cookieKey = 'ksp_theme';
|
||||
const readSavedTheme = () => {
|
||||
try {
|
||||
const savedTheme = localStorage.getItem(storageKey);
|
||||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
||||
return savedTheme;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore storage access failures and fall back to cookies/system preference.
|
||||
}
|
||||
|
||||
const cookieMatch = document.cookie.match(/(?:^|; )ksp_theme=(dark|light)(?:;|$)/);
|
||||
return cookieMatch ? cookieMatch[1] : null;
|
||||
};
|
||||
const persistTheme = (theme) => {
|
||||
try {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
} catch (error) {
|
||||
// Ignore storage access failures and rely on cookies for persistence.
|
||||
}
|
||||
|
||||
document.cookie = `${cookieKey}=${theme}; path=/; max-age=31536000; SameSite=Lax`;
|
||||
};
|
||||
const applyDocumentTheme = (theme) => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
};
|
||||
const savedTheme = readSavedTheme();
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const resolvedTheme = savedTheme === 'dark' || savedTheme === 'light'
|
||||
? savedTheme
|
||||
: (prefersDark ? 'dark' : 'light');
|
||||
|
||||
applyDocumentTheme(resolvedTheme);
|
||||
window.__kspTheme = {
|
||||
storageKey,
|
||||
readSavedTheme,
|
||||
persistTheme,
|
||||
applyDocumentTheme,
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-shell">
|
||||
<header class="site-header">
|
||||
<a class="brand" href="{{ url_for('web.dashboard') }}">
|
||||
<span class="brand-name">KSP Operation Hangar</span>
|
||||
</a>
|
||||
<div class="site-header-tools">
|
||||
<nav class="site-nav">
|
||||
<a href="{{ url_for('web.dashboard') }}">控制台</a>
|
||||
<a href="{{ url_for('web.engine_list') }}">引擎目录</a>
|
||||
<a href="{{ url_for('web.communication_list') }}">通信部件</a>
|
||||
<a href="{{ url_for('web.tank_list') }}">燃料箱</a>
|
||||
<a href="{{ url_for('web.vehicle_list') }}">载具成本</a>
|
||||
<a href="{{ url_for('web.mission_preview_home') }}">任务运营</a>
|
||||
<a href="{{ url_for('web.fuel_converter') }}">燃料换算</a>
|
||||
<a href="{{ url_for('api.health') }}">API 健康检查</a>
|
||||
</nav>
|
||||
<button class="theme-toggle" type="button" data-theme-toggle aria-label="切换深浅色模式" aria-pressed="false">
|
||||
<span class="theme-toggle-track" aria-hidden="true">
|
||||
<span class="theme-toggle-thumb"></span>
|
||||
</span>
|
||||
<span class="theme-toggle-copy">
|
||||
<span class="theme-toggle-label">主题</span>
|
||||
<span class="theme-toggle-value" data-theme-toggle-label>浅色</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-stack">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-message {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const themeManager = window.__kspTheme || null;
|
||||
const themeStorageKey = themeManager?.storageKey || 'ksp-theme';
|
||||
const themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const resolveTheme = () => {
|
||||
const savedTheme = themeManager?.readSavedTheme?.();
|
||||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
||||
return savedTheme;
|
||||
}
|
||||
return themeMediaQuery.matches ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
const applyTheme = (theme) => {
|
||||
const resolvedTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
if (themeManager?.applyDocumentTheme) {
|
||||
themeManager.applyDocumentTheme(resolvedTheme);
|
||||
} else {
|
||||
document.documentElement.dataset.theme = resolvedTheme;
|
||||
document.documentElement.style.colorScheme = resolvedTheme;
|
||||
}
|
||||
|
||||
for (const button of document.querySelectorAll('[data-theme-toggle]')) {
|
||||
button.setAttribute('aria-pressed', String(resolvedTheme === 'dark'));
|
||||
}
|
||||
|
||||
for (const label of document.querySelectorAll('[data-theme-toggle-label]')) {
|
||||
label.textContent = resolvedTheme === 'dark' ? '深色' : '浅色';
|
||||
}
|
||||
};
|
||||
|
||||
const bindThemeToggle = (root = document) => {
|
||||
for (const button of root.querySelectorAll('[data-theme-toggle]')) {
|
||||
button.onclick = () => {
|
||||
const nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
||||
if (themeManager?.persistTheme) {
|
||||
themeManager.persistTheme(nextTheme);
|
||||
} else {
|
||||
localStorage.setItem(themeStorageKey, nextTheme);
|
||||
}
|
||||
applyTheme(nextTheme);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const formatDisplayDatetime = (isoValue) => {
|
||||
if (!isoValue) {
|
||||
return '';
|
||||
}
|
||||
return isoValue.slice(0, 16).replace(/-/g, '/').replace('T', ' ');
|
||||
};
|
||||
|
||||
const parseDisplayDatetime = (rawValue) => {
|
||||
const cleaned = (rawValue || '').trim();
|
||||
if (!cleaned) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const normalized = cleaned
|
||||
.replace(/[年.]/g, '/')
|
||||
.replace(/月/g, '/')
|
||||
.replace(/日/g, ' ')
|
||||
.replace(/T/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const match = normalized.match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})\s+(\d{1,2}):(\d{1,2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, yearValue, monthValue, dayValue, hourValue, minuteValue] = match;
|
||||
const year = Number(yearValue);
|
||||
const month = Number(monthValue);
|
||||
const day = Number(dayValue);
|
||||
const hour = Number(hourValue);
|
||||
const minute = Number(minuteValue);
|
||||
const isoValue = `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}T${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
||||
const parsedDate = new Date(`${isoValue}:00`);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsedDate.getFullYear() !== year
|
||||
|| parsedDate.getMonth() + 1 !== month
|
||||
|| parsedDate.getDate() !== day
|
||||
|| parsedDate.getHours() !== hour
|
||||
|| parsedDate.getMinutes() !== minute
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return isoValue;
|
||||
};
|
||||
|
||||
const bindDatetimePickerTriggers = (root = document) => {
|
||||
const controls = root.querySelectorAll('[data-datetime-control]');
|
||||
for (const control of controls) {
|
||||
if (control.dataset.datetimeControlBound === 'true') {
|
||||
continue;
|
||||
}
|
||||
control.dataset.datetimeControlBound = 'true';
|
||||
|
||||
const displayInput = control.querySelector('.mission-datetime-display-input');
|
||||
const button = control.querySelector('[data-datetime-picker-target]');
|
||||
if (!displayInput || !button) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pickerInput = document.getElementById(button.dataset.datetimePickerTarget || '');
|
||||
if (!pickerInput) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const syncPickerFromDisplay = () => {
|
||||
const parsedValue = parseDisplayDatetime(displayInput.value);
|
||||
if (parsedValue === '') {
|
||||
pickerInput.value = '';
|
||||
return '';
|
||||
}
|
||||
if (parsedValue === null) {
|
||||
return null;
|
||||
}
|
||||
pickerInput.value = parsedValue;
|
||||
displayInput.value = formatDisplayDatetime(parsedValue);
|
||||
return parsedValue;
|
||||
};
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
syncPickerFromDisplay();
|
||||
if (typeof pickerInput.showPicker === 'function') {
|
||||
pickerInput.showPicker();
|
||||
return;
|
||||
}
|
||||
pickerInput.focus();
|
||||
pickerInput.click();
|
||||
});
|
||||
|
||||
pickerInput.addEventListener('change', () => {
|
||||
displayInput.value = formatDisplayDatetime(pickerInput.value);
|
||||
});
|
||||
|
||||
displayInput.addEventListener('blur', () => {
|
||||
syncPickerFromDisplay();
|
||||
});
|
||||
|
||||
displayInput.addEventListener('input', () => {
|
||||
if (!displayInput.value.trim()) {
|
||||
pickerInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (pickerInput.value && !displayInput.value.trim()) {
|
||||
displayInput.value = formatDisplayDatetime(pickerInput.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.bindDatetimePickerTriggers = bindDatetimePickerTriggers;
|
||||
window.bindThemeToggle = bindThemeToggle;
|
||||
bindThemeToggle();
|
||||
applyTheme(resolveTheme());
|
||||
|
||||
const handleThemeMediaChange = (event) => {
|
||||
if (localStorage.getItem(themeStorageKey)) {
|
||||
return;
|
||||
}
|
||||
applyTheme(event.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
if (typeof themeMediaQuery.addEventListener === 'function') {
|
||||
themeMediaQuery.addEventListener('change', handleThemeMediaChange);
|
||||
} else if (typeof themeMediaQuery.addListener === 'function') {
|
||||
themeMediaQuery.addListener(handleThemeMediaChange);
|
||||
}
|
||||
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key && event.key !== themeStorageKey) {
|
||||
return;
|
||||
}
|
||||
applyTheme(resolveTheme());
|
||||
});
|
||||
|
||||
bindDatetimePickerTriggers();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="catalog-header panel catalog-header-tight catalog-page-header">
|
||||
<div class="catalog-header-copy">
|
||||
<p class="eyebrow">Communication</p>
|
||||
<h1>通信部件目录</h1>
|
||||
<p class="hero-text">支持分页、筛选、排序和快速编辑。</p>
|
||||
</div>
|
||||
<div class="catalog-header-side">
|
||||
<div class="catalog-metrics">
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">总记录</span>
|
||||
<strong>{{ total_catalog_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前结果</span>
|
||||
<strong>{{ total_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前页</span>
|
||||
<strong>{{ page }} / {{ total_pages }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel catalog-actions-panel top-gap">
|
||||
<div>
|
||||
<p class="eyebrow">Actions</p>
|
||||
<h2>新增内容</h2>
|
||||
</div>
|
||||
<a class="primary-button" href="{{ url_for('web.communication_new') }}">新增通信部件</a>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide top-gap">
|
||||
<form class="search-form filter-row" method="get">
|
||||
<input type="search" name="q" value="{{ search_term }}" placeholder="Part / Display / Source / Type">
|
||||
|
||||
<select name="source">
|
||||
<option value="">全部来源</option>
|
||||
{% for option in source_options %}
|
||||
<option value="{{ option }}" {% if option == source_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="antenna_type">
|
||||
<option value="">全部类型</option>
|
||||
{% for option in type_options %}
|
||||
<option value="{{ option }}" {% if option == antenna_type_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="sort">
|
||||
{% for option in sort_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == sort_option %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.communication_list') }}">重置</a>
|
||||
</form>
|
||||
|
||||
{% if rows %}
|
||||
<div class="table-shell top-gap">
|
||||
<table class="data-table compact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
<th>Display</th>
|
||||
<th>Type</th>
|
||||
<th>Range (km)</th>
|
||||
<th>Mass (t)</th>
|
||||
<th>Idle (W)</th>
|
||||
<th>Source</th>
|
||||
<th>Flags</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td><strong>{{ row.part_name }}</strong></td>
|
||||
<td>{{ row.display_name or '-' }}</td>
|
||||
<td>{{ row.antenna_type or '-' }}</td>
|
||||
<td>{{ row.range_km if row.range_km is not none else '-' }}</td>
|
||||
<td>{{ row.mass_t if row.mass_t is not none else '-' }}</td>
|
||||
<td>{{ row.idle_power_watt if row.idle_power_watt is not none else '-' }}</td>
|
||||
<td>{{ row.source or '-' }}</td>
|
||||
<td>
|
||||
{% if row.is_active %}Active{% else %}-{% endif %}
|
||||
{% if row.is_deployable %} / Deployable{% endif %}
|
||||
{% if row.is_feeder %} / Feeder{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.communication_edit', part_id=row.id) }}">编辑</a>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('web.communication_delete', part_id=row.id) }}"
|
||||
class="inline-form"
|
||||
onsubmit="return confirm('确认删除 {{ row.part_name }} ?')"
|
||||
>
|
||||
<button class="danger-link" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination-bar" aria-label="Communication pagination">
|
||||
{% if has_prev %}
|
||||
<a class="pagination-link" href="{{ prev_url }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="pagination-pages">
|
||||
{% for item in page_links %}
|
||||
{% if item.ellipsis %}
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% elif item.current %}
|
||||
<span class="pagination-link current">{{ item.number }}</span>
|
||||
{% else %}
|
||||
<a class="pagination-link" href="{{ item.url }}">{{ item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if has_next %}
|
||||
<a class="pagination-link" href="{{ next_url }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">下一页</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
<h2>没有命中结果</h2>
|
||||
<p>调整筛选条件或重置后重试。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero-panel">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">项目起步版本</p>
|
||||
<h1>先把数据模型和导入边界定死,再做 CRUD。</h1>
|
||||
<p class="hero-text">
|
||||
这个骨架现在已经提供 Flask 应用、规范化数据库模型、Docker 运行基线、Excel 巡检脚本,
|
||||
以及第一块可直接使用的 Fuel Chart 页面工具。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-callout">
|
||||
<p class="callout-label">关键风险</p>
|
||||
<p class="callout-number">{{ duplicate_conflicts }}</p>
|
||||
<p class="callout-text">组 Engine + Config Name 在原始工作簿中存在重复,不能作为数据库硬唯一键。</p>
|
||||
<p class="callout-subtext">即便补上 Fuel Type,仍然还有 {{ duplicate_conflicts_with_fuel }} 组冲突。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="metric-grid">
|
||||
{% for metric in metrics %}
|
||||
<article class="metric-card">
|
||||
<span class="metric-value">{{ metric.value }}</span>
|
||||
<span class="metric-label">{{ metric.label }}</span>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="content-grid">
|
||||
<article class="panel panel-wide">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">建模决策</p>
|
||||
<h2>当前推荐的数据库拆分</h2>
|
||||
</div>
|
||||
<div class="decision-grid">
|
||||
<div>
|
||||
<h3>engine_families</h3>
|
||||
<p>只放经 Excel 校验后稳定的字段:Engine、Name、Cycle、Size、entry Cost。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>engine_variants</h3>
|
||||
<p>放所有配置级字段:Fuel Type、Work Env、ISP、推力、TVC、价格、点火次数、Tech、备注。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>代理主键</h3>
|
||||
<p>所有核心表使用 UUID 主键,导入时保留原始自然键,并单独输出重复告警。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Fuel Chart</h3>
|
||||
<p>不先入库,先作为常量配置和 API 小工具,后面再看是否要抽成字典表。</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">模块状态</p>
|
||||
<h2>第一批交付</h2>
|
||||
</div>
|
||||
<div class="module-list">
|
||||
{% for module in modules %}
|
||||
<div class="module-card {{ module.status }}">
|
||||
<div class="module-meta">
|
||||
<span class="module-status">{{ module.status }}</span>
|
||||
<h3>{{ module.title }}</h3>
|
||||
</div>
|
||||
<p>{{ module.description }}</p>
|
||||
{% if module.endpoint %}
|
||||
<a class="module-link" href="{{ url_for(module.endpoint) }}">立即打开</a>
|
||||
{% else %}
|
||||
<span class="module-link muted">下一阶段实现</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero-panel compact page-header-compact">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Engine Create</p>
|
||||
<h1>新增引擎</h1>
|
||||
<p class="hero-text">创建一个新的引擎家族,并同时录入第一条配置项。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="post" class="edit-layout">
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Family</p>
|
||||
<h2>基础字段</h2>
|
||||
</div>
|
||||
<div class="form-grid family-grid">
|
||||
{% for field in family_fields %}
|
||||
{% set field_value = request.form.get(field.name, '') %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
{% if field.kind == 'combo' %}
|
||||
{% set options = form_options.get(field.name, []) %}
|
||||
{% set custom_value = '' if not field_value or field_value in options else field_value %}
|
||||
<select name="{{ field.name }}" {% if field.required %}required{% endif %}>
|
||||
<option value="">请选择</option>
|
||||
{% for option in options %}
|
||||
<option value="{{ option }}" {% if option == field_value %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
<option value="{{ custom_option_value }}" {% if custom_value %}selected{% endif %}>新增选项</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ field.name }}__custom"
|
||||
value="{{ custom_value }}"
|
||||
placeholder="没有合适选项时,在这里输入新的 {{ field.label }}"
|
||||
>
|
||||
{% else %}
|
||||
<input
|
||||
type="text"
|
||||
name="{{ field.name }}"
|
||||
value="{{ field_value }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Initial Variant</p>
|
||||
<h2>首个配置项</h2>
|
||||
</div>
|
||||
<div class="form-grid variant-add-grid">
|
||||
{% for field in variant_fields %}
|
||||
{% set input_name = 'initial-' ~ field.name %}
|
||||
{% set field_value = request.form.get(input_name, '') %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
{% if field.kind == 'combo' %}
|
||||
{% set options = form_options.get(field.name, []) %}
|
||||
{% set custom_value = '' if not field_value or field_value in options else field_value %}
|
||||
<select name="{{ input_name }}" {% if field.required and not custom_value %}required{% endif %}>
|
||||
<option value="">请选择</option>
|
||||
{% for option in options %}
|
||||
<option value="{{ option }}" {% if option == field_value %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
<option value="{{ custom_option_value }}" {% if custom_value %}selected{% endif %}>新增选项</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ input_name }}__custom"
|
||||
value="{{ custom_value }}"
|
||||
placeholder="没有合适选项时,在这里输入新的 {{ field.label }}"
|
||||
>
|
||||
{% else %}
|
||||
<input
|
||||
type="text"
|
||||
name="{{ input_name }}"
|
||||
value="{{ field_value }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="button-row top-gap">
|
||||
<button class="primary-button" type="submit">创建引擎</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.engine_list') }}">取消</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero-panel compact page-header-compact engine-detail-hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Engine Detail</p>
|
||||
<h1>{{ family.engine_name }}</h1>
|
||||
<p class="hero-text">查看家族级字段,以及该引擎下全部配置项的关键性能参数。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-grid">
|
||||
<article class="panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Family</p>
|
||||
<h2>基础信息</h2>
|
||||
</div>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Part Name</span>
|
||||
<strong>{{ family.part_name or "-" }}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Cycle</span>
|
||||
<strong>{{ family.cycle or "-" }}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Size</span>
|
||||
<strong>{{ family.size_m|fmt_decimal }}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Entry Cost</span>
|
||||
<strong>{{ family.entry_cost if family.entry_cost is not none else "-" }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row top-gap">
|
||||
<a class="secondary-button" href="{{ url_for('web.engine_edit', family_id=family.id) }}">编辑这个引擎</a>
|
||||
<a class="module-link" href="{{ url_for('web.engine_list') }}">返回列表</a>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('web.engine_delete', family_id=family.id) }}"
|
||||
class="inline-form"
|
||||
onsubmit="return confirm('确认删除 {{ family.engine_name }} ?')"
|
||||
>
|
||||
<button class="danger-link" type="submit">删除引擎</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel panel-wide">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Variants</p>
|
||||
<h2>配置列表</h2>
|
||||
</div>
|
||||
<div class="table-shell">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Config</th>
|
||||
<th>Fuel</th>
|
||||
<th>Env</th>
|
||||
<th>SL Thrust</th>
|
||||
<th>Vac Thrust</th>
|
||||
<th>SL ISP</th>
|
||||
<th>Vac ISP</th>
|
||||
<th>Mass</th>
|
||||
<th>TWR</th>
|
||||
<th>Price</th>
|
||||
<th>Ignitions</th>
|
||||
<th>Mod</th>
|
||||
<th>Tech</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in variant_rows %}
|
||||
{% set variant = row.variant %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ variant.config_name or "Default" }}</strong>
|
||||
{% if variant.config_note %}
|
||||
<div class="variant-note">{{ variant.config_note }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ variant.fuel_type }}</td>
|
||||
<td>{{ variant.work_env or "-" }}</td>
|
||||
<td>{{ row.sl_thrust_kn|fmt_decimal }}</td>
|
||||
<td>{{ row.vac_thrust_kn|fmt_decimal }}</td>
|
||||
<td>{{ variant.sl_isp|fmt_decimal }}</td>
|
||||
<td>{{ variant.vac_isp|fmt_decimal }}</td>
|
||||
<td>{{ variant.mass_t|fmt_decimal }}</td>
|
||||
<td>{{ variant.twr|fmt_decimal }}</td>
|
||||
<td>{{ variant.price_kd if variant.price_kd is not none else "-" }}</td>
|
||||
<td>
|
||||
{% if variant.has_unlimited_ignitions %}
|
||||
Unlimited
|
||||
{% else %}
|
||||
{{ variant.ignitions if variant.ignitions is not none else "-" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ variant.mod_source or "-" }}</td>
|
||||
<td>{{ variant.tech_required or "-" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,247 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero-panel compact page-header-compact">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Engine Edit</p>
|
||||
<h1>编辑 {{ family.engine_name }}</h1>
|
||||
<p class="hero-text">支持家族字段编辑、配置项新增、配置项删除,以及整机删除。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel top-gap">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Add Variant</p>
|
||||
<h2>新增配置项</h2>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('web.engine_variant_add', family_id=family.id) }}" class="form-grid variant-add-grid">
|
||||
{% for field in variant_create_fields %}
|
||||
{% set input_name = 'add-' ~ field.name %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
{% if field.kind == 'combo' %}
|
||||
{% set options = form_options.get(field.name, []) %}
|
||||
<select name="{{ input_name }}" {% if field.required %}required{% endif %}>
|
||||
<option value="">请选择</option>
|
||||
{% for option in options %}
|
||||
<option value="{{ option }}">{{ option }}</option>
|
||||
{% endfor %}
|
||||
<option value="{{ custom_option_value }}">新增选项</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ input_name }}__custom"
|
||||
value=""
|
||||
placeholder="没有合适选项时,在这里输入新的 {{ field.label }}"
|
||||
>
|
||||
{% else %}
|
||||
<input
|
||||
type="text"
|
||||
name="{{ input_name }}"
|
||||
value=""
|
||||
{% if field.required %}required{% endif %}
|
||||
>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div class="button-row">
|
||||
<button class="primary-button" type="submit">新增配置</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<form method="post" class="edit-layout">
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Family</p>
|
||||
<h2>基础字段</h2>
|
||||
</div>
|
||||
<div class="form-grid family-grid">
|
||||
{% for field in family_fields %}
|
||||
{% set current_value = family|attr(field.name) if family|attr(field.name) is not none else '' %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
{% if field.kind == 'combo' %}
|
||||
{% set options = form_options.get(field.name, []) %}
|
||||
{% set custom_value = '' if not current_value or current_value in options else current_value %}
|
||||
<select name="{{ field.name }}" {% if field.required and not custom_value %}required{% endif %}>
|
||||
<option value="">请选择</option>
|
||||
{% for option in options %}
|
||||
<option value="{{ option }}" {% if option == current_value %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
{% if custom_value %}
|
||||
<option value="{{ custom_option_value }}" selected>新增选项</option>
|
||||
{% else %}
|
||||
<option value="{{ custom_option_value }}">新增选项</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ field.name }}__custom"
|
||||
value="{{ custom_value }}"
|
||||
placeholder="没有合适选项时,在这里输入新的 {{ field.label }}"
|
||||
>
|
||||
{% else %}
|
||||
<input
|
||||
type="text"
|
||||
name="{{ field.name }}"
|
||||
value="{{ current_value }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Variants</p>
|
||||
<h2>配置字段</h2>
|
||||
</div>
|
||||
|
||||
<div class="variant-editor-stack">
|
||||
{% for variant in family.variants %}
|
||||
<article class="variant-editor" data-engine-calculator>
|
||||
<div class="module-meta">
|
||||
<h3>{{ variant.config_name or "Default" }}</h3>
|
||||
<div class="button-row compact-row">
|
||||
<span class="chip">Row {{ variant.source_sheet_row or "-" }}</span>
|
||||
<button
|
||||
class="danger-link"
|
||||
type="submit"
|
||||
formaction="{{ url_for('web.engine_variant_delete', family_id=family.id, variant_id=variant.id) }}"
|
||||
formmethod="post"
|
||||
formnovalidate
|
||||
onclick="return confirm('确认删除这个配置项?')"
|
||||
>删除配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid variant-editor-grid">
|
||||
{% for field in variant_fields %}
|
||||
{% set current_value = variant|attr(field.name) %}
|
||||
{% set input_name = 'variant-' ~ variant.id ~ '-' ~ field.name %}
|
||||
{% if field.kind == 'textarea' %}
|
||||
<label class="field-span-two">
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
<textarea name="{{ input_name }}" rows="3">{{ current_value if current_value is not none else '' }}</textarea>
|
||||
</label>
|
||||
{% elif field.kind == 'checkbox' %}
|
||||
<label class="checkbox-row field-span-two">
|
||||
<input type="checkbox" name="{{ input_name }}" {% if current_value %}checked{% endif %}>
|
||||
<span>{{ field.label }}</span>
|
||||
</label>
|
||||
{% elif field.kind == 'combo' %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
{% set options = form_options.get(field.name, []) %}
|
||||
{% set normalized_value = current_value if current_value is not none else '' %}
|
||||
{% set custom_value = '' if not normalized_value or normalized_value in options else normalized_value %}
|
||||
<select name="{{ input_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>
|
||||
{% endfor %}
|
||||
{% if custom_value %}
|
||||
<option value="{{ custom_option_value }}" selected>新增选项</option>
|
||||
{% else %}
|
||||
<option value="{{ custom_option_value }}">新增选项</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ input_name }}__custom"
|
||||
value="{{ custom_value }}"
|
||||
placeholder="没有合适选项时,在这里输入新的 {{ field.label }}"
|
||||
>
|
||||
</label>
|
||||
{% else %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ input_name }}"
|
||||
value="{{ current_value if current_value is not none else '' }}"
|
||||
{% if field.name in ['min_thrust_kn', 'max_thrust_kn', 'mass_t', 'twr', 'throttle_ratio'] %}data-calc-field="{{ field.name }}"{% endif %}
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.name in ['twr', 'throttle_ratio'] %}readonly{% endif %}
|
||||
>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="button-row top-gap">
|
||||
<button class="primary-button" type="submit">保存修改</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.engine_detail', family_id=family.id) }}">取消</a>
|
||||
<button
|
||||
class="danger-link"
|
||||
type="submit"
|
||||
formaction="{{ url_for('web.engine_delete', family_id=family.id) }}"
|
||||
formmethod="post"
|
||||
formnovalidate
|
||||
onclick="return confirm('确认删除整个引擎 {{ family.engine_name }} ?')"
|
||||
>删除整个引擎</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const gravity = 9.80665;
|
||||
const formatDecimal = (value) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return "";
|
||||
}
|
||||
return value.toFixed(4).replace(/\.?0+$/, "");
|
||||
};
|
||||
|
||||
const parseNumber = (input) => {
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
const cleaned = input.value.trim();
|
||||
if (!cleaned) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(cleaned);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const recalculate = (container) => {
|
||||
const maxThrustInput = container.querySelector('[data-calc-field="max_thrust_kn"]');
|
||||
const minThrustInput = container.querySelector('[data-calc-field="min_thrust_kn"]');
|
||||
const massInput = container.querySelector('[data-calc-field="mass_t"]');
|
||||
const twrInput = container.querySelector('[data-calc-field="twr"]');
|
||||
const throttleRatioInput = container.querySelector('[data-calc-field="throttle_ratio"]');
|
||||
|
||||
const maxThrust = parseNumber(maxThrustInput);
|
||||
const minThrust = parseNumber(minThrustInput);
|
||||
const mass = parseNumber(massInput);
|
||||
|
||||
if (twrInput) {
|
||||
twrInput.value = maxThrust !== null && mass !== null && mass !== 0
|
||||
? formatDecimal(maxThrust / gravity / mass)
|
||||
: "";
|
||||
}
|
||||
|
||||
if (throttleRatioInput) {
|
||||
throttleRatioInput.value = minThrust !== null && maxThrust !== null && maxThrust !== 0
|
||||
? formatDecimal(minThrust / maxThrust)
|
||||
: "";
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-engine-calculator]').forEach((container) => {
|
||||
container.querySelectorAll('[data-calc-field="min_thrust_kn"], [data-calc-field="max_thrust_kn"], [data-calc-field="mass_t"]').forEach((input) => {
|
||||
input.addEventListener('input', () => recalculate(container));
|
||||
});
|
||||
recalculate(container);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,262 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="catalog-header panel catalog-header-tight catalog-page-header engine-catalog-hero">
|
||||
<div class="catalog-header-copy engine-catalog-copy">
|
||||
<p class="eyebrow">Engine Catalog</p>
|
||||
<h1>引擎目录</h1>
|
||||
<p class="hero-text">宽屏高密度视图,支持分页、燃料/循环/Mod 筛选,并按推力或比冲排序。</p>
|
||||
</div>
|
||||
<div class="catalog-header-side">
|
||||
<div class="catalog-metrics">
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">总引擎家族</span>
|
||||
<strong>{{ total_catalog_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前结果</span>
|
||||
<strong>{{ total_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前页</span>
|
||||
<strong>{{ page }} / {{ total_pages }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="catalog-layout">
|
||||
<div class="catalog-sidebar">
|
||||
<aside class="panel filter-panel filter-panel-tight">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Filters</p>
|
||||
<h2>筛选和排序</h2>
|
||||
</div>
|
||||
<form class="filter-form" method="get">
|
||||
<label>
|
||||
<span class="field-label">搜索</span>
|
||||
<input type="search" name="q" value="{{ search_term }}" placeholder="Engine / Part / Config">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span class="field-label">燃料种类</span>
|
||||
<select name="fuel_type">
|
||||
<option value="">全部</option>
|
||||
{% for option in fuel_options %}
|
||||
<option value="{{ option }}" {% if option == fuel_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span class="field-label">循环类型</span>
|
||||
<select name="cycle">
|
||||
<option value="">全部</option>
|
||||
{% for option in cycle_options %}
|
||||
<option value="{{ option }}" {% if option == cycle_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span class="field-label">Mod 来源</span>
|
||||
<select name="mod_source">
|
||||
<option value="">全部</option>
|
||||
{% for option in mod_source_options %}
|
||||
<option value="{{ option }}" {% if option == mod_source_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.engine_list') }}">重置</a>
|
||||
</div>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<section class="panel catalog-actions-panel">
|
||||
<div>
|
||||
<p class="eyebrow">Actions</p>
|
||||
<h2>新增内容</h2>
|
||||
<p class="small-muted">创建新的引擎家族和初始配置。</p>
|
||||
</div>
|
||||
<a class="primary-button" href="{{ url_for('web.engine_new') }}">新增引擎</a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel panel-wide catalog-panel">
|
||||
<div class="results-header">
|
||||
<div>
|
||||
<p class="eyebrow">Results</p>
|
||||
<h2>结果列表</h2>
|
||||
<p class="small-muted">每页 30 条,单行压缩并补充海平面/真空核心参数。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if catalog_rows %}
|
||||
<div class="table-shell">
|
||||
<table class="data-table compact-table">
|
||||
<colgroup>
|
||||
<col class="engine-name-col">
|
||||
<col class="cycle-col">
|
||||
<col class="fuel-col">
|
||||
<col class="mod-col">
|
||||
<col class="count-col">
|
||||
<col class="numeric-col">
|
||||
<col class="numeric-col">
|
||||
<col class="numeric-col">
|
||||
<col class="numeric-col">
|
||||
<col class="numeric-col">
|
||||
<col class="numeric-col">
|
||||
<col class="action-col">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="engine-name-col">
|
||||
<div class="column-sort-head">
|
||||
<span>Engine</span>
|
||||
{% for column in sort_columns if column.key == 'engine_name' %}
|
||||
<span class="sort-chip-group">
|
||||
<a class="sort-chip {% if column.asc_active %}active{% endif %}" href="{{ column.asc_url }}">升</a>
|
||||
<a class="sort-chip {% if column.desc_active %}active{% endif %}" href="{{ column.desc_url }}">降</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="cycle-col">Cycle</th>
|
||||
<th class="fuel-col">Fuel</th>
|
||||
<th class="mod-col">Mod</th>
|
||||
<th class="count-col">Configs</th>
|
||||
<th class="numeric-col">
|
||||
<div class="column-sort-head numeric-head">
|
||||
<span>Mass</span>
|
||||
{% for column in sort_columns if column.key == 'mass_t' %}
|
||||
<span class="sort-chip-group">
|
||||
<a class="sort-chip {% if column.asc_active %}active{% endif %}" href="{{ column.asc_url }}">升</a>
|
||||
<a class="sort-chip {% if column.desc_active %}active{% endif %}" href="{{ column.desc_url }}">降</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="numeric-col">
|
||||
<div class="column-sort-head numeric-head">
|
||||
<span>SL Thrust</span>
|
||||
{% for column in sort_columns if column.key == 'sl_thrust' %}
|
||||
<span class="sort-chip-group">
|
||||
<a class="sort-chip {% if column.asc_active %}active{% endif %}" href="{{ column.asc_url }}">升</a>
|
||||
<a class="sort-chip {% if column.desc_active %}active{% endif %}" href="{{ column.desc_url }}">降</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="numeric-col">
|
||||
<div class="column-sort-head numeric-head">
|
||||
<span>Vac Thrust</span>
|
||||
{% for column in sort_columns if column.key == 'vac_thrust' %}
|
||||
<span class="sort-chip-group">
|
||||
<a class="sort-chip {% if column.asc_active %}active{% endif %}" href="{{ column.asc_url }}">升</a>
|
||||
<a class="sort-chip {% if column.desc_active %}active{% endif %}" href="{{ column.desc_url }}">降</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="numeric-col">
|
||||
<div class="column-sort-head numeric-head">
|
||||
<span>SL ISP</span>
|
||||
{% for column in sort_columns if column.key == 'sl_isp' %}
|
||||
<span class="sort-chip-group">
|
||||
<a class="sort-chip {% if column.asc_active %}active{% endif %}" href="{{ column.asc_url }}">升</a>
|
||||
<a class="sort-chip {% if column.desc_active %}active{% endif %}" href="{{ column.desc_url }}">降</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="numeric-col">
|
||||
<div class="column-sort-head numeric-head">
|
||||
<span>Vac ISP</span>
|
||||
{% for column in sort_columns if column.key == 'vac_isp' %}
|
||||
<span class="sort-chip-group">
|
||||
<a class="sort-chip {% if column.asc_active %}active{% endif %}" href="{{ column.asc_url }}">升</a>
|
||||
<a class="sort-chip {% if column.desc_active %}active{% endif %}" href="{{ column.desc_url }}">降</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="numeric-col">TWR</th>
|
||||
<th class="action-col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in catalog_rows %}
|
||||
<tr>
|
||||
<td class="engine-cell two-line-cell engine-name-col">
|
||||
<strong class="table-title">{{ row.family.engine_name }}</strong>
|
||||
<div class="table-subline clamp-line">{{ row.family.part_name or "No part name" }}</div>
|
||||
</td>
|
||||
<td class="single-line-cell cycle-col">{{ row.family.cycle or "-" }}</td>
|
||||
<td class="single-line-cell fuel-col">{{ row.fuel_types|join(', ') if row.fuel_types else "-" }}</td>
|
||||
<td class="single-line-cell mod-col">{{ row.mod_sources|join(', ') if row.mod_sources else "-" }}</td>
|
||||
<td class="count-col">{{ row.config_count }}</td>
|
||||
<td class="numeric-col">{{ row.mass_t|fmt_decimal }}</td>
|
||||
<td class="numeric-col">{{ row.sl_thrust_kn|fmt_decimal }}</td>
|
||||
<td class="numeric-col">{{ row.vac_thrust_kn|fmt_decimal }}</td>
|
||||
<td class="numeric-col">{{ row.sl_isp|fmt_decimal }}</td>
|
||||
<td class="numeric-col">{{ row.vac_isp|fmt_decimal }}</td>
|
||||
<td class="numeric-col">{{ row.twr|fmt_decimal }}</td>
|
||||
<td class="action-col">
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.engine_detail', family_id=row.family.id) }}">详情</a>
|
||||
<a class="table-link" href="{{ url_for('web.engine_edit', family_id=row.family.id) }}">编辑</a>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('web.engine_delete', family_id=row.family.id) }}"
|
||||
class="inline-form"
|
||||
onsubmit="return confirm('确认删除 {{ row.family.engine_name }} ?')"
|
||||
>
|
||||
<button class="danger-link" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination-bar" aria-label="Engine pagination">
|
||||
{% if has_prev %}
|
||||
<a class="pagination-link" href="{{ prev_url }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="pagination-pages">
|
||||
{% for item in page_links %}
|
||||
{% if item.ellipsis %}
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% elif item.current %}
|
||||
<span class="pagination-link current">{{ item.number }}</span>
|
||||
{% else %}
|
||||
<a class="pagination-link" href="{{ item.url }}">{{ item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if has_next %}
|
||||
<a class="pagination-link" href="{{ next_url }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">下一页</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
<h2>没有命中结果</h2>
|
||||
<p>调整筛选条件,或者重置后重新查看全部引擎。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero-panel compact page-header-compact">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Fuel Chart 工具化</p>
|
||||
<h1>燃料体积 / 质量快速换算</h1>
|
||||
<p class="hero-text">
|
||||
系数直接来自工作簿第二行。输入升数可换算吨位,输入吨位可反推不同燃料对应的体积。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-grid fuel-layout">
|
||||
<article class="panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">换算输入</p>
|
||||
<h2>输入一个值</h2>
|
||||
</div>
|
||||
|
||||
<form id="converter-form" class="converter-form">
|
||||
<label class="field-label" for="mode">换算方向</label>
|
||||
<select id="mode" name="mode">
|
||||
<option value="volume">体积 L -> 质量 t</option>
|
||||
<option value="mass">质量 t -> 体积 L</option>
|
||||
</select>
|
||||
|
||||
<label class="field-label" for="value">数值</label>
|
||||
<input id="value" name="value" type="number" min="0.000001" step="any" value="75" required>
|
||||
|
||||
<button class="primary-button" type="submit">执行换算</button>
|
||||
<p id="form-error" class="form-error" hidden></p>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="panel panel-wide">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">换算结果</p>
|
||||
<h2>输出列表</h2>
|
||||
</div>
|
||||
|
||||
<div id="result-summary" class="result-summary">输入 75 t 或 75 L 后,这里会显示结果。</div>
|
||||
<div id="result-table" class="result-table"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">当前系数</p>
|
||||
<h2>每升对应吨位</h2>
|
||||
</div>
|
||||
<div class="factor-grid">
|
||||
{% for factor in fuel_factors %}
|
||||
<article class="factor-card">
|
||||
<h3>{{ factor.fuel }}</h3>
|
||||
<p>{{ factor.tonnes_per_liter }} t / L</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("converter-form");
|
||||
const modeField = document.getElementById("mode");
|
||||
const valueField = document.getElementById("value");
|
||||
const resultSummary = document.getElementById("result-summary");
|
||||
const resultTable = document.getElementById("result-table");
|
||||
const formError = document.getElementById("form-error");
|
||||
const endpoint = "{{ url_for('api.convert_fuel') }}";
|
||||
|
||||
async function renderResults(mode, value) {
|
||||
formError.hidden = true;
|
||||
const query = new URLSearchParams({ mode, value }).toString();
|
||||
const response = await fetch(`${endpoint}?${query}`);
|
||||
const payload = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || "换算失败");
|
||||
}
|
||||
|
||||
const outputUnit = payload.results[0]?.unit || "";
|
||||
resultSummary.textContent = `输入 ${payload.input_value} ${payload.input_unit},输出单位 ${outputUnit}`;
|
||||
resultTable.innerHTML = payload.results
|
||||
.map((item) => `
|
||||
<div class="result-row">
|
||||
<span>${item.fuel}</span>
|
||||
<strong>${item.value}</strong>
|
||||
<span>${item.unit}</span>
|
||||
</div>
|
||||
`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await renderResults(modeField.value, valueField.value);
|
||||
} catch (error) {
|
||||
formError.hidden = false;
|
||||
formError.textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
renderResults(modeField.value, valueField.value).catch((error) => {
|
||||
formError.hidden = false;
|
||||
formError.textContent = error.message;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,113 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
{% include "_mission_preview_controls.html" %}
|
||||
|
||||
<section class="mission-detail-layout top-gap">
|
||||
<div class="mission-detail-main">
|
||||
<section class="panel mission-asset-overview">
|
||||
<div class="mission-asset-head">
|
||||
<div>
|
||||
<p class="eyebrow">Asset Summary</p>
|
||||
<h2>{{ asset.name }}</h2>
|
||||
<p class="small-muted">{{ asset.asset_type }} · {{ asset.program }}</p>
|
||||
</div>
|
||||
<div class="mission-asset-chip-row">
|
||||
<span class="chip strong">{{ asset.current_state }}</span>
|
||||
<span class="chip">{{ asset.current_location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-meta-grid">
|
||||
<div class="mission-meta-item">
|
||||
<dt>Current Mission</dt>
|
||||
<dd>{{ asset.current_mission }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Active Window</dt>
|
||||
<dd>{{ asset.state_window }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item field-span-two">
|
||||
<dt>Next Planned Event</dt>
|
||||
<dd>{{ asset.next_event }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mission-log-panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">History</p>
|
||||
<h2>Asset Timeline Entries</h2>
|
||||
</div>
|
||||
|
||||
<div class="mission-log-list">
|
||||
{% for entry in entries %}
|
||||
<article class="mission-log-entry {{ entry.mode }}">
|
||||
<div class="mission-log-rail">
|
||||
<span class="mission-log-dot"></span>
|
||||
</div>
|
||||
|
||||
<div class="mission-log-body">
|
||||
<div class="mission-log-top">
|
||||
<span class="mission-log-kicker">{% if entry.mode == 'event' %}Event Point{% else %}State Interval{% endif %}</span>
|
||||
<span class="mission-status-pill">{{ entry.record_status }}</span>
|
||||
</div>
|
||||
|
||||
<h3>{{ entry.title }}</h3>
|
||||
|
||||
<p class="mission-log-time">
|
||||
{{ entry.timestamp }}
|
||||
{% if entry.end_timestamp %}
|
||||
→ {{ entry.end_timestamp }}
|
||||
{% endif %}
|
||||
· {{ entry.location }}
|
||||
</p>
|
||||
|
||||
<p class="mission-summary-text">{{ entry.summary }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="mission-detail-side">
|
||||
<section class="panel mission-current-state-panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Current</p>
|
||||
<h2>Current Derived State</h2>
|
||||
</div>
|
||||
<div class="mission-meta-grid">
|
||||
<div class="mission-meta-item">
|
||||
<dt>Status</dt>
|
||||
<dd>{{ asset.current_state }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Location</dt>
|
||||
<dd>{{ asset.current_location }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item field-span-two">
|
||||
<dt>Mission</dt>
|
||||
<dd>{{ asset.current_mission }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include "_mission_upcoming_events.html" %}
|
||||
|
||||
<section class="panel mission-entry-actions-panel">
|
||||
<div class="panel-heading">
|
||||
<p class="eyebrow">Authoring</p>
|
||||
<h2>Planned Entry Actions</h2>
|
||||
</div>
|
||||
<div class="mission-action-stack">
|
||||
<button class="primary-button" type="button">Add Event Point</button>
|
||||
<button class="primary-button" type="button">Add State Interval</button>
|
||||
<button class="secondary-button" type="button">Correct Current State</button>
|
||||
<button class="secondary-button" type="button">Import From Notes</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,137 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
|
||||
<section class="panel mission-control-panel top-gap">
|
||||
<form method="get" action="{{ url_for('web.mission_list') }}" class="mission-toolbar-grid">
|
||||
<input type="hidden" name="sim_time" value="{{ simulation_time_input }}">
|
||||
|
||||
<div class="mission-toolbar-block mission-toolbar-span-two">
|
||||
<span class="field-label">Mission Label 检索与筛选</span>
|
||||
<div class="mission-filter-grid">
|
||||
<label>
|
||||
<span class="field-label">Mission Label 搜索</span>
|
||||
<input type="search" name="q" value="{{ search_term }}" placeholder="支持模糊搜索 Mission Label">
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">Asset</span>
|
||||
<select name="asset_id">
|
||||
<option value="">全部资产</option>
|
||||
{% for option in asset_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == asset_id_filter %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">Mission Label</span>
|
||||
<select name="mission_label">
|
||||
<option value="">全部 Mission Label</option>
|
||||
{% for option in mission_label_options %}
|
||||
<option value="{{ option }}" {% if option == mission_label_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">地点</span>
|
||||
<select name="location">
|
||||
<option value="">全部地点</option>
|
||||
{% for option in location_options %}
|
||||
<option value="{{ option }}" {% if option == location_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{{ datetime_picker_field('开始', 'start', range_start_input, field_id='mission-list-start') }}
|
||||
{{ datetime_picker_field('结束', 'end', range_end_input, field_id='mission-list-end') }}
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.mission_list', sim_time=simulation_time_input) }}">重置</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide top-gap">
|
||||
<div class="mission-filter-pill-row">
|
||||
<span class="mission-filter-pill"><strong>Asset:</strong> {{ selected_asset_name }}</span>
|
||||
<span class="mission-filter-pill"><strong>Mission:</strong> {{ mission_label_filter or (search_term or '全部') }}</span>
|
||||
<span class="mission-filter-pill"><strong>地点:</strong> {{ location_filter or '全部地点' }}</span>
|
||||
<span class="mission-filter-pill"><strong>日志:</strong> {{ filtered_entry_count }} / {{ total_entry_count }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if mission_groups %}
|
||||
{% for group in mission_groups %}
|
||||
<section class="panel mission-group-panel top-gap">
|
||||
<div class="mission-group-header">
|
||||
<div>
|
||||
<p class="eyebrow">Mission Label</p>
|
||||
<h2>{{ group.mission_label }}</h2>
|
||||
<p class="mission-group-subtitle">{{ group.status_label }} · {{ group.asset_count }} 个 Asset · {{ group.entry_count }} 条日志</p>
|
||||
</div>
|
||||
<span class="mission-group-count">最近条目 {{ group.latest_timestamp }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mission-filter-pill-row">
|
||||
<span class="mission-filter-pill"><strong>Assets:</strong> {{ group.asset_names|join(' · ') }}</span>
|
||||
<span class="mission-filter-pill"><strong>地点:</strong> {{ group.locations|join(' · ') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="table-shell top-gap">
|
||||
<table class="data-table compact-table mission-log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Asset</th>
|
||||
<th>Entry</th>
|
||||
<th>Location</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in group.entries %}
|
||||
<tr>
|
||||
<td class="two-line-cell">
|
||||
<strong class="table-title">{{ entry.timestamp }}</strong>
|
||||
{% if entry.end_timestamp %}
|
||||
<div class="table-subline">→ {{ entry.end_timestamp }}</div>
|
||||
{% endif %}
|
||||
<div class="table-subline">{{ entry.status_label }}</div>
|
||||
</td>
|
||||
<td class="two-line-cell">
|
||||
<strong><a class="table-link" href="{{ url_for('web.asset_detail', asset_id=entry.asset.id, sim_time=simulation_time_input) }}">{{ entry.asset_name }}</a></strong>
|
||||
<div class="table-subline">{{ entry.asset_type }}</div>
|
||||
</td>
|
||||
<td class="two-line-cell">
|
||||
<strong class="table-title">{{ entry.title }}</strong>
|
||||
<div class="table-subline">{{ entry.mode_label }}</div>
|
||||
<div class="table-subline clamp-line">{{ entry.summary }}</div>
|
||||
{% if entry.state_labels or entry.mission_labels %}
|
||||
<div class="mission-asset-chip-row mission-entry-label-row">
|
||||
{% for label in entry.state_labels %}<span class="chip">{{ label }}</span>{% endfor %}
|
||||
{% for label in entry.mission_labels %}<span class="chip">{{ label }}</span>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ entry.location }}</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.asset_detail', asset_id=entry.asset.id, sim_time=simulation_time_input) }}">查看资产</a>
|
||||
<a class="table-link" href="{{ url_for('web.asset_entry_edit', asset_id=entry.asset.id, entry_id=entry.entry_id, sim_time=simulation_time_input) }}">编辑条目</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<section class="panel empty-state compact-empty top-gap">
|
||||
<h2>当前筛选条件下没有 Mission 日志</h2>
|
||||
<p>调整 Asset、时间、Mission Label、地点筛选,或者清空 Mission Label 搜索后重试。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
|
||||
<section class="panel mission-control-panel top-gap">
|
||||
<form method="get" action="{{ url_for('web.mission_location_board_preview') }}" class="mission-toolbar-grid">
|
||||
<div class="mission-toolbar-block">
|
||||
<span class="field-label">面板筛选</span>
|
||||
<div class="mission-filter-grid">
|
||||
<label>
|
||||
<span class="field-label">资产类型</span>
|
||||
<select name="asset_type">
|
||||
<option value="">全部类型</option>
|
||||
{% for option in asset_type_options %}
|
||||
<option value="{{ option }}" {% if option == asset_type_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">地点</span>
|
||||
<select name="location">
|
||||
<option value="">全部地点</option>
|
||||
{% for option in location_options %}
|
||||
<option value="{{ option }}" {% if option == location_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">记录范围</span>
|
||||
<select name="record_scope">
|
||||
{% for option in record_scope_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == record_scope %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ reset_url }}">重置</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="mission-board-shell top-gap">
|
||||
<div class="mission-board-main">
|
||||
{% if location_groups %}
|
||||
{% for group in location_groups %}
|
||||
<section class="panel mission-group-panel">
|
||||
<div class="mission-group-header">
|
||||
<div>
|
||||
<p class="eyebrow">地点分组</p>
|
||||
<h2>{{ group.title }}</h2>
|
||||
<p class="mission-group-subtitle">{{ group.subtitle }}</p>
|
||||
</div>
|
||||
<span class="mission-group-count">{{ group['items']|length }} 项资产</span>
|
||||
</div>
|
||||
|
||||
<div class="mission-card-grid">
|
||||
{% for item in group['items'] %}
|
||||
<article class="mission-asset-card mission-tone-soft">
|
||||
<div class="mission-card-top">
|
||||
<div>
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p class="small-muted mission-type">{{ item.asset_type }}</p>
|
||||
</div>
|
||||
<span class="mission-status-pill">{{ item.state }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mission-meta-grid">
|
||||
<div class="mission-meta-item">
|
||||
<dt>Location</dt>
|
||||
<dd>{{ item.location }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Mission</dt>
|
||||
<dd>{{ item.mission }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item field-span-two">
|
||||
<dt>Next Event</dt>
|
||||
<dd>{{ item.next_event }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-card-foot">
|
||||
<a class="table-link" href="{{ url_for('web.asset_list', q=item.name, sim_time=simulation_time_input) }}">查看任务资产</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<section class="panel empty-state compact-empty">
|
||||
<h2>当前筛选条件下没有资产</h2>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mission-board-side">
|
||||
{% include "_mission_upcoming_events.html" %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,111 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
|
||||
<section class="panel mission-control-panel top-gap">
|
||||
<form method="get" action="{{ url_for('web.mission_status_board_preview') }}" class="mission-toolbar-grid">
|
||||
<div class="mission-toolbar-block">
|
||||
<span class="field-label">面板筛选</span>
|
||||
<div class="mission-filter-grid">
|
||||
<label>
|
||||
<span class="field-label">资产类型</span>
|
||||
<select name="asset_type">
|
||||
<option value="">全部类型</option>
|
||||
{% for option in asset_type_options %}
|
||||
<option value="{{ option }}" {% if option == asset_type_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">地点</span>
|
||||
<select name="location">
|
||||
<option value="">全部地点</option>
|
||||
{% for option in location_options %}
|
||||
<option value="{{ option }}" {% if option == location_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">记录范围</span>
|
||||
<select name="record_scope">
|
||||
{% for option in record_scope_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == record_scope %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ reset_url }}">重置</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="mission-board-shell top-gap">
|
||||
<div class="mission-board-main">
|
||||
{% if status_groups %}
|
||||
{% for group in status_groups %}
|
||||
<section class="panel mission-group-panel">
|
||||
<div class="mission-group-header">
|
||||
<div>
|
||||
<p class="eyebrow">状态分组</p>
|
||||
<h2>{{ group.title }}</h2>
|
||||
</div>
|
||||
<span class="mission-group-count">{{ group['items']|length }} 项资产</span>
|
||||
</div>
|
||||
|
||||
<div class="mission-card-grid">
|
||||
{% for item in group['items'] %}
|
||||
<article class="mission-asset-card mission-tone-{{ group.accent }}">
|
||||
<div class="mission-card-top">
|
||||
<div>
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p class="small-muted mission-type">{{ item.asset_type }}</p>
|
||||
</div>
|
||||
<span class="mission-status-pill">{{ item.record_status }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mission-meta-grid">
|
||||
<div class="mission-meta-item">
|
||||
<dt>Location</dt>
|
||||
<dd>{{ item.location }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Mission</dt>
|
||||
<dd>{{ item.mission }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>State Window</dt>
|
||||
<dd>{{ item.state_window }}</dd>
|
||||
</div>
|
||||
<div class="mission-meta-item">
|
||||
<dt>Next Event</dt>
|
||||
<dd>{{ item.next_event }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mission-summary-text">{{ item.summary }}</p>
|
||||
|
||||
<div class="mission-card-foot">
|
||||
<a class="table-link" href="{{ url_for('web.asset_list', q=item.name, sim_time=simulation_time_input) }}">查看任务资产</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<section class="panel empty-state compact-empty">
|
||||
<h2>当前筛选条件下没有资产</h2>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mission-board-side">
|
||||
{% include "_mission_upcoming_events.html" %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,240 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_datetime_picker_field.html" import datetime_picker_field %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% include "_mission_preview_header.html" %}
|
||||
|
||||
<section class="panel mission-control-panel top-gap">
|
||||
<form method="get" action="{{ url_for('web.mission_timeline_preview') }}" class="mission-toolbar-grid">
|
||||
<input type="hidden" name="range_mode" value="{{ timeline_range_mode }}" id="timeline-range-mode">
|
||||
<div class="mission-toolbar-block">
|
||||
<span class="field-label">时间范围</span>
|
||||
<div class="mission-toolbar-inline">
|
||||
{{ datetime_picker_field('开始', 'start', timeline_range_start, field_id='timeline-start') }}
|
||||
{{ datetime_picker_field('结束', 'end', timeline_range_end, field_id='timeline-end') }}
|
||||
<label>
|
||||
<span class="field-label">尺度</span>
|
||||
<select name="scale">
|
||||
{% for option in timeline_scale_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == timeline_scale_mode %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-toolbar-block">
|
||||
<span class="field-label">时间线筛选</span>
|
||||
<div class="mission-filter-grid">
|
||||
<label>
|
||||
<span class="field-label">资产类型</span>
|
||||
<select name="asset_type">
|
||||
<option value="">全部类型</option>
|
||||
{% for option in asset_type_options %}
|
||||
<option value="{{ option }}" {% if option == asset_type_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">地点</span>
|
||||
<select name="location">
|
||||
<option value="">全部地点</option>
|
||||
{% for option in location_options %}
|
||||
<option value="{{ option }}" {% if option == location_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="field-label">记录范围</span>
|
||||
<select name="record_scope">
|
||||
{% for option in record_scope_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == record_scope %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ reset_url }}">重置</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-toolbar-block mission-toolbar-span-two">
|
||||
<span class="field-label">显示资产</span>
|
||||
{% if timeline_asset_options %}
|
||||
<div class="mission-asset-picker" data-asset-picker>
|
||||
<div class="mission-asset-picker-toolbar">
|
||||
<label class="mission-asset-picker-search">
|
||||
<span class="field-label">按名称搜索</span>
|
||||
<input type="search" placeholder="搜索资产名称" data-asset-picker-search>
|
||||
</label>
|
||||
<div class="mission-asset-picker-actions">
|
||||
<button class="secondary-button" type="button" data-asset-picker-select-all>全选可见</button>
|
||||
<button class="secondary-button" type="button" data-asset-picker-clear>清空选择</button>
|
||||
<span class="small-muted" data-asset-picker-summary></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-asset-picker-grid">
|
||||
{% for option in timeline_asset_options %}
|
||||
<label class="mission-asset-picker-item" data-asset-picker-item data-asset-picker-name="{{ option.label|lower }}">
|
||||
<input type="checkbox" name="asset_ids" value="{{ option.value }}" data-asset-picker-checkbox {% if option.selected %}checked{% endif %}>
|
||||
<span>
|
||||
<strong>{{ option.label }}</strong>
|
||||
<small>{{ option.asset_type }}</small>
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<p class="field-hint">未勾选任何 Asset 时,默认显示当前筛选命中的全部资产。</p>
|
||||
{% else %}
|
||||
<p class="small-muted">当前筛选条件下没有可选资产。</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel mission-timeline-panel top-gap">
|
||||
<div class="mission-timeline-toolbar">
|
||||
<div>
|
||||
<p class="eyebrow">Timeline</p>
|
||||
<h2>多资产时间线</h2>
|
||||
</div>
|
||||
<div class="mission-legend">
|
||||
<span class="legend-item"><span class="legend-swatch warning"></span>转移段</span>
|
||||
<span class="legend-item"><span class="legend-swatch primary"></span>持续运行</span>
|
||||
<span class="legend-item"><span class="legend-swatch neutral"></span>停泊 / 地表 / 待命</span>
|
||||
<span class="legend-item"><span class="legend-swatch current"></span>当前活动状态</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-filter-pill-row">
|
||||
<span class="mission-filter-pill"><strong>范围:</strong> {{ timeline_range_display }}</span>
|
||||
<span class="mission-filter-pill"><strong>尺度:</strong> {{ timeline_scale_mode|title }}</span>
|
||||
<span class="mission-filter-pill"><strong>资产:</strong> {% if selected_asset_count %}已选 {{ selected_asset_count }} 个{% else %}显示 {{ timeline_rows|length }} 条{% endif %}</span>
|
||||
<span class="mission-filter-pill"><strong>模式:</strong> {{ record_scope_options|selectattr('value', 'equalto', record_scope)|map(attribute='label')|first }}</span>
|
||||
</div>
|
||||
|
||||
{% if timeline_rows %}
|
||||
<div class="mission-timeline-scroll">
|
||||
<div class="mission-timeline-frame" style="min-width: {{ 320 + (timeline_scale|length * timeline_column_width) }}px;">
|
||||
<div class="mission-timeline-scale">
|
||||
<div class="mission-timeline-corner"></div>
|
||||
<div class="mission-scale-labels" style="--timeline-columns: {{ timeline_scale|length }}; --timeline-column-width: {{ timeline_column_width }}px;">
|
||||
{% for label in timeline_scale %}
|
||||
<span>{{ label }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mission-timeline-rows">
|
||||
{% for row in timeline_rows %}
|
||||
<div class="mission-timeline-row">
|
||||
<a class="mission-timeline-label mission-timeline-label-link" href="{{ url_for('web.asset_detail', asset_id=row.asset_id, sim_time=simulation_time_input) }}">
|
||||
<h3>{{ row.name }}</h3>
|
||||
<p class="small-muted mission-timeline-meta">{{ row.asset_type }}</p>
|
||||
<p class="small-muted mission-timeline-status">Current Status: {{ row.current_state }}</p>
|
||||
</a>
|
||||
|
||||
<div class="mission-timeline-track-stack">
|
||||
<div class="mission-timeline-lane mission-timeline-state-lane {% if timeline_scale_mode == 'year' %}mission-timeline-lane-year{% endif %}" style="--timeline-columns: {{ timeline_scale|length }}; --timeline-column-width: {{ timeline_column_width }}px; --timeline-track-count: {{ row.state_track_count }};">
|
||||
{% for segment in row.segments %}
|
||||
<a class="mission-segment mission-timeline-link {% if segment.compact %}mission-segment-compact {% endif %}{% for tone in segment.tone.split() %}mission-tone-{{ tone }} {% endfor %}" style="left: {{ segment.left_pct }}; width: {{ segment.track_width_pct }}; --timeline-track-index: {{ segment.track }};" title="{{ segment.label }}" data-full-label="{{ segment.label }}" href="{{ segment.url }}">
|
||||
<span class="mission-segment-body" style="width: {{ segment.body_width_pct }};"></span>
|
||||
<span class="mission-segment-text">{{ segment.label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mission-timeline-lane mission-timeline-event-lane {% if timeline_scale_mode == 'year' %}mission-timeline-lane-year{% endif %}" style="--timeline-columns: {{ timeline_scale|length }}; --timeline-column-width: {{ timeline_column_width }}px; --timeline-track-count: {{ row.event_track_count }};">
|
||||
{% for event in row.events %}
|
||||
<a class="mission-event-node mission-timeline-link {% if event.state_node %}mission-event-node-state{% endif %}{% if event.label_side == 'left' %} mission-event-node-label-left{% endif %}" style="left: {{ event.left_pct }}; --timeline-track-index: {{ event.track }}; --event-label-width: {{ event.label_width_px }}px;{% if event.connector_height_px %} --state-node-connector-height: {{ event.connector_height_px }};{% endif %}" title="{{ event.label }}" data-full-label="{{ event.full_label }}" href="{{ event.url }}">
|
||||
{% if event.state_node %}
|
||||
<span class="mission-state-node-connector" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
<span class="mission-event-node-label">{{ event.label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
<h2>当前时间范围内没有时间线条目</h2>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const picker = document.querySelector('[data-asset-picker]');
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchInput = picker.querySelector('[data-asset-picker-search]');
|
||||
const summary = picker.querySelector('[data-asset-picker-summary]');
|
||||
const items = Array.from(picker.querySelectorAll('[data-asset-picker-item]'));
|
||||
const rangeModeField = document.getElementById('timeline-range-mode');
|
||||
const rangeInputs = [document.getElementById('timeline-start'), document.getElementById('timeline-end')].filter(Boolean);
|
||||
|
||||
const checkboxFor = (item) => item.querySelector('[data-asset-picker-checkbox]');
|
||||
|
||||
const updateSummary = () => {
|
||||
const visibleCount = items.filter((item) => !item.hidden).length;
|
||||
const selectedCount = items.filter((item) => checkboxFor(item)?.checked).length;
|
||||
summary.textContent = `已选 ${selectedCount} 个 · 可见 ${visibleCount} 个`;
|
||||
};
|
||||
|
||||
const setRangeModeManual = () => {
|
||||
if (rangeModeField) {
|
||||
rangeModeField.value = 'manual';
|
||||
}
|
||||
};
|
||||
|
||||
const applySearch = () => {
|
||||
const term = (searchInput?.value || '').trim().toLowerCase();
|
||||
for (const item of items) {
|
||||
const assetName = item.dataset.assetPickerName || '';
|
||||
item.hidden = Boolean(term) && !assetName.includes(term);
|
||||
}
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
picker.querySelector('[data-asset-picker-select-all]')?.addEventListener('click', () => {
|
||||
for (const item of items) {
|
||||
const checkbox = checkboxFor(item);
|
||||
if (!item.hidden && checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
}
|
||||
updateSummary();
|
||||
});
|
||||
|
||||
picker.querySelector('[data-asset-picker-clear]')?.addEventListener('click', () => {
|
||||
for (const item of items) {
|
||||
const checkbox = checkboxFor(item);
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
}
|
||||
updateSummary();
|
||||
});
|
||||
|
||||
searchInput?.addEventListener('input', applySearch);
|
||||
for (const item of items) {
|
||||
checkboxFor(item)?.addEventListener('change', updateSummary);
|
||||
}
|
||||
for (const input of rangeInputs) {
|
||||
input.addEventListener('input', setRangeModeManual);
|
||||
input.addEventListener('change', setRangeModeManual);
|
||||
}
|
||||
|
||||
applySearch();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,89 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% if mission_ops_tabs %}
|
||||
{% include "_mission_ops_tabs.html" %}
|
||||
{% endif %}
|
||||
<section class="hero-panel compact page-header-compact">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">{{ eyebrow }}</p>
|
||||
<h1>{{ heading }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="post" class="edit-layout">
|
||||
{% set hidden_field_values = hidden_fields|default({}, true) %}
|
||||
{% for name, value in hidden_field_values.items() %}
|
||||
<input type="hidden" name="{{ name }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<section class="panel panel-wide">
|
||||
{% set option_map = form_options|default({}) %}
|
||||
{% set combo_custom_option_value = custom_option_value|default('__custom__') %}
|
||||
<div class="form-grid resource-form-grid">
|
||||
{% for field in fields %}
|
||||
{% set current_value = values.get(field.name) %}
|
||||
{% if field.kind == 'textarea' %}
|
||||
<label class="field-span-three">
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
<textarea name="{{ field.name }}" rows="4">{{ current_value if current_value is not none else '' }}</textarea>
|
||||
</label>
|
||||
{% elif field.kind == 'checkbox' %}
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="{{ field.name }}" {% if current_value %}checked{% endif %}>
|
||||
<span>{{ field.label }}</span>
|
||||
</label>
|
||||
{% elif field.kind == 'combo' %}
|
||||
{% 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>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
<select 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>
|
||||
{% endfor %}
|
||||
<option value="{{ combo_custom_option_value }}" {% if custom_value %}selected{% endif %}>新增选项</option>
|
||||
</select>
|
||||
<input
|
||||
class="combo-custom-input"
|
||||
type="text"
|
||||
name="{{ field.name }}__custom"
|
||||
value="{{ custom_value }}"
|
||||
placeholder="没有合适选项时,在这里输入新的 {{ field.label }}"
|
||||
>
|
||||
</label>
|
||||
{% else %}
|
||||
<label>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="{{ field.name }}"
|
||||
value="{{ current_value if current_value is not none else '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</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('{{ delete_confirm or '确认删除?' }}')">
|
||||
{% set delete_hidden_field_values = delete_hidden_fields|default(hidden_field_values, true) %}
|
||||
{% for name, value in delete_hidden_field_values.items() %}
|
||||
<input type="hidden" name="{{ name }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<button class="danger-link" type="submit">{{ delete_label or '删除' }}</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,146 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="catalog-header panel catalog-header-tight catalog-page-header">
|
||||
<div class="catalog-header-copy">
|
||||
<p class="eyebrow">Tank</p>
|
||||
<h1>燃料箱目录</h1>
|
||||
<p class="hero-text">支持分页、筛选、排序和快速编辑。</p>
|
||||
</div>
|
||||
<div class="catalog-header-side">
|
||||
<div class="catalog-metrics">
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">总记录</span>
|
||||
<strong>{{ total_catalog_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前结果</span>
|
||||
<strong>{{ total_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前页</span>
|
||||
<strong>{{ page }} / {{ total_pages }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel catalog-actions-panel top-gap">
|
||||
<div>
|
||||
<p class="eyebrow">Actions</p>
|
||||
<h2>新增内容</h2>
|
||||
</div>
|
||||
<a class="primary-button" href="{{ url_for('web.tank_new') }}">新增燃料箱</a>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide top-gap">
|
||||
<form class="search-form filter-row" method="get">
|
||||
<input type="search" name="q" value="{{ search_term }}" placeholder="Tank / Vehicle / Fuel / Source">
|
||||
|
||||
<select name="fuel_type">
|
||||
<option value="">全部燃料</option>
|
||||
{% for option in fuel_options %}
|
||||
<option value="{{ option }}" {% if option == fuel_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="source">
|
||||
<option value="">全部来源</option>
|
||||
{% for option in source_options %}
|
||||
<option value="{{ option }}" {% if option == source_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="sort">
|
||||
{% for option in sort_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == sort_option %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.tank_list') }}">重置</a>
|
||||
</form>
|
||||
|
||||
{% if rows %}
|
||||
<div class="table-shell top-gap">
|
||||
<table class="data-table compact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tank</th>
|
||||
<th>Fuel</th>
|
||||
<th>Vehicle</th>
|
||||
<th>Volume (L)</th>
|
||||
<th>Dry (t)</th>
|
||||
<th>Fuel (t)</th>
|
||||
<th>Wet (t)</th>
|
||||
<th>Ratio</th>
|
||||
<th>Source</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td><strong>{{ row.tank_name }}</strong></td>
|
||||
<td>{{ row.fuel_type or '-' }}</td>
|
||||
<td>{{ row.vehicle_name or '-' }}</td>
|
||||
<td>{{ row.tank_volume_l if row.tank_volume_l is not none else '-' }}</td>
|
||||
<td>{{ row.dry_mass_t if row.dry_mass_t is not none else '-' }}</td>
|
||||
<td>{{ row.fuel_mass_t if row.fuel_mass_t is not none else '-' }}</td>
|
||||
<td>{{ row.wet_mass_t if row.wet_mass_t is not none else '-' }}</td>
|
||||
<td>{{ row.mass_ratio if row.mass_ratio is not none else '-' }}</td>
|
||||
<td>{{ row.source or '-' }}</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.tank_edit', tank_id=row.id) }}">编辑</a>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('web.tank_delete', tank_id=row.id) }}"
|
||||
class="inline-form"
|
||||
onsubmit="return confirm('确认删除 {{ row.tank_name }} ?')"
|
||||
>
|
||||
<button class="danger-link" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination-bar" aria-label="Tank pagination">
|
||||
{% if has_prev %}
|
||||
<a class="pagination-link" href="{{ prev_url }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="pagination-pages">
|
||||
{% for item in page_links %}
|
||||
{% if item.ellipsis %}
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% elif item.current %}
|
||||
<span class="pagination-link current">{{ item.number }}</span>
|
||||
{% else %}
|
||||
<a class="pagination-link" href="{{ item.url }}">{{ item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if has_next %}
|
||||
<a class="pagination-link" href="{{ next_url }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">下一页</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
<h2>没有命中结果</h2>
|
||||
<p>调整筛选条件或重置后重试。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,127 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="catalog-header panel catalog-header-tight catalog-page-header">
|
||||
<div class="catalog-header-copy">
|
||||
<p class="eyebrow">Vehicle</p>
|
||||
<h1>载具成本目录</h1>
|
||||
<p class="hero-text">支持分页、来源筛选、成本排序与编辑。</p>
|
||||
</div>
|
||||
<div class="catalog-header-side">
|
||||
<div class="catalog-metrics">
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">总记录</span>
|
||||
<strong>{{ total_catalog_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前结果</span>
|
||||
<strong>{{ total_count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card compact">
|
||||
<span class="summary-label">当前页</span>
|
||||
<strong>{{ page }} / {{ total_pages }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel catalog-actions-panel top-gap">
|
||||
<div>
|
||||
<p class="eyebrow">Actions</p>
|
||||
<h2>新增内容</h2>
|
||||
</div>
|
||||
<a class="primary-button" href="{{ url_for('web.vehicle_new') }}">新增载具成本</a>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide top-gap">
|
||||
<form class="search-form filter-row" method="get">
|
||||
<input type="search" name="q" value="{{ search_term }}" placeholder="Vehicle / Source">
|
||||
|
||||
<select name="source">
|
||||
<option value="">全部来源</option>
|
||||
{% for option in source_options %}
|
||||
<option value="{{ option }}" {% if option == source_filter %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="sort">
|
||||
{% for option in sort_options %}
|
||||
<option value="{{ option.value }}" {% if option.value == sort_option %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="primary-button" type="submit">应用</button>
|
||||
<a class="secondary-button" href="{{ url_for('web.vehicle_list') }}">重置</a>
|
||||
</form>
|
||||
|
||||
{% if rows %}
|
||||
<div class="table-shell top-gap">
|
||||
<table class="data-table compact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vehicle</th>
|
||||
<th>Launch Price</th>
|
||||
<th>Source</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td><strong>{{ row.vehicle_name }}</strong></td>
|
||||
<td>{{ row.launch_price if row.launch_price is not none else '-' }}</td>
|
||||
<td>{{ row.source or '-' }}</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="table-link" href="{{ url_for('web.vehicle_edit', vehicle_id=row.id) }}">编辑</a>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('web.vehicle_delete', vehicle_id=row.id) }}"
|
||||
class="inline-form"
|
||||
onsubmit="return confirm('确认删除 {{ row.vehicle_name }} ?')"
|
||||
>
|
||||
<button class="danger-link" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination-bar" aria-label="Vehicle pagination">
|
||||
{% if has_prev %}
|
||||
<a class="pagination-link" href="{{ prev_url }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="pagination-pages">
|
||||
{% for item in page_links %}
|
||||
{% if item.ellipsis %}
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% elif item.current %}
|
||||
<span class="pagination-link current">{{ item.number }}</span>
|
||||
{% else %}
|
||||
<a class="pagination-link" href="{{ item.url }}">{{ item.number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if has_next %}
|
||||
<a class="pagination-link" href="{{ next_url }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="pagination-link disabled">下一页</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<section class="empty-state compact-empty">
|
||||
<h2>没有命中结果</h2>
|
||||
<p>调整筛选条件或重置后重试。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user