feat: timeline refactor, docking UI, location/state dropdowns, sim_time cleanup

Templates:
- mission_timeline_preview: client-side rendering with lane stacking, wheel zoom,
  drag pan, percentage-based layout, event dedup, scale labels
- asset_detail: dock/undock modals with time selection, future event warning,
  Mission Label column replacing Location/State
- asset_entry_form: all_locations expanded to 72 items (draft board hierarchy),
  all_states dynamic from DB, event point dropdown uses same list
- Remove sim_time from all template url_for links
- base.html: remove Today button

JS:
- app.js: sim_time controls use POST /api/v1/sim-time API instead of URL param

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 15:29:10 +08:00
co-authored by Claude Opus 4.7
parent cc4c460840
commit 2fef743e83
15 changed files with 1791 additions and 1966 deletions
+55 -255
View File
@@ -1,262 +1,62 @@
{% 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>
<div class="ph-row fi">
<div class="ph">
<div class="ey">Reference Data</div>
<h1>引擎目录</h1>
</div>
<a class="b ac" href="{{ url_for('web.engine_new') }}">+ New Engine</a>
</div>
<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>
<div class="fb fi">
<form method="get" action="{{ url_for('web.engine_list') }}" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" name="q" value="{{ search_term }}" placeholder="Search engines...">
<select name="fuel_type" onchange="this.form.submit()">
<option value="">All Fuel</option>
{% for f in fuel_options %}<option value="{{ f }}" {% if f == fuel_filter %}selected{% endif %}>{{ f }}</option>{% endfor %}
</select>
<select name="cycle" onchange="this.form.submit()">
<option value="">All Cycles</option>
{% for c in cycle_options %}<option value="{{ c }}" {% if c == cycle_filter %}selected{% endif %}>{{ c }}</option>{% endfor %}
</select>
<select name="mod_source" onchange="this.form.submit()">
<option value="">All Mods</option>
{% for m in mod_source_options %}<option value="{{ m }}" {% if m == mod_source_filter %}selected{% endif %}>{{ m }}</option>{% endfor %}
</select>
<button class="b" type="submit">Search</button>
</form>
</div>
<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>
<table class="fi">
<thead><tr>
<th>Name</th><th>Cycle</th><th>Mass</th><th>Vac Thrust</th><th>Vac ISP</th><th>Fuel</th><th>Mod</th><th>Action</th>
</tr></thead>
<tbody>
{% for row in catalog_rows %}
<tr>
<td><a href="{{ url_for('web.engine_detail', family_id=row.family.id) }}">{{ row.family.engine_name }}</a></td>
<td>{{ row.family.cycle or '—' }}</td>
<td>{{ '%.2f'|format(row.max_mass_t) if row.max_mass_t else '—' }}t</td>
<td>{{ '%.0f'|format(row.max_vac_thrust_kn) if row.max_vac_thrust_kn else '—' }}kN</td>
<td>{{ '%.0f'|format(row.max_vac_isp) if row.max_vac_isp else '—' }}s</td>
<td>{{ row.fuel_types or '—' }}</td>
<td class="mt">{{ row.mod_source or '—' }}</td>
<td><div class="act-btn"><a href="{{ url_for('web.engine_edit', family_id=row.family.id) }}">Edit</a></div></td>
</tr>
{% endfor %}
</tbody>
</table>
<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>
{% if total_pages > 1 %}
<div class="pg-bar fi">
{% if has_prev %}<a href="{{ prev_url }}">← Prev</a>{% endif %}
{% for link in page_links %}
{% if link.is_current %}<span class="cur">{{ link.page }}</span>
{% elif link.is_ellipsis %}<span>...</span>
{% else %}<a href="{{ link.url }}">{{ link.page }}</a>{% endif %}
{% endfor %}
{% if has_next %}<a href="{{ next_url }}">Next →</a>{% endif %}
</div>
{% endif %}
{% endblock %}