優化 PPT 最近預覽入口
Some checks failed
CD Pipeline / deploy (push) Has been cancelled

This commit is contained in:
OoO
2026-05-18 16:07:55 +08:00
parent 217aa4fd6d
commit 1262017261
5 changed files with 127 additions and 1 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.192 補 `/observability/ppt_audit_history` 最近可預覽簡報 workbench最新 4 份 PPT 直接在控制台下方提供線上預覽與下載,降低使用者找檔案的操作成本;完整檔案清單仍保留在下方表格。
- V10.190 補 `/observability/ppt_audit_file/<filename>` 站內線上預覽PPTX 由 LibreOffice 轉 PDF 快取後以 iframe 預覽,保留原始 PPTX 下載Dockerfile 加 `libreoffice-impress`compose 預設啟用 `PPT_VISION_ENABLED=true`PPT 產線頁新增視覺 QA 停用原因與更精簡的控制台式排版。
- V10.188 補強 `/observability/ppt_audit_history` PPT 視覺 QA 產線:頁面明確呈現每日、每週、每月、每季、每半年、每年定期產出節奏,並顯示 `ppt_generation_runs` DB 寫入紀錄;保留自動補齊缺漏與資料庫/檔案覆蓋狀態。
- V10.187 修正 `/daily_sales`、`/growth_analysis` 圖表空白Chart JSON 改從 `<template>.content.textContent` 讀取,補空資料診斷狀態;成長分析改用 realtime 明細新鮮度覆蓋過期月結摘要,並為 growth cache 加入資料指紋。

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.191"
SYSTEM_VERSION = "V10.192"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -89,6 +89,45 @@
{% endfor %}
</div>
</section>
{% if files %}
<section class="ppt-deck-workbench" aria-label="最近可預覽簡報">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">Preview Workbench</div>
<h2 class="ppt-panel-title">最近可預覽簡報</h2>
</div>
<small class="text-muted">最新 {{ files[:4]|length }} 份,直接線上預覽或下載原始 PPTX</small>
</div>
<div class="ppt-deck-rail">
{% for f in files[:4] %}
<article class="ppt-deck-card {% if not f.file_exists or not f.is_valid_ppt %}is-disabled{% endif %}">
<div class="ppt-deck-meta">
<span class="ppt-label">{{ selected_report_type.label }}</span>
<span>{{ f.mtime }}</span>
</div>
<h3>{{ f.name }}</h3>
<div class="ppt-deck-facts">
<span>{{ f.size_kb if f.size_kb is not none else '—' }} KB</span>
<span>{{ f.source }}</span>
{% if f.file_exists and f.is_valid_ppt %}<span class="status-good">可預覽</span>{% else %}<span class="status-bad">需回補</span>{% endif %}
</div>
<div class="ppt-file-actions">
{% if f.file_exists and f.is_valid_ppt %}
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}" target="_blank" rel="noopener">
<i class="fas fa-eye me-1"></i>線上預覽
</a>
{% endif %}
{% if f.file_exists %}
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
<i class="fas fa-download me-1"></i>下載
</a>
{% endif %}
</div>
</article>
{% endfor %}
</div>
</section>
{% endif %}
<section class="ppt-panel mt-3"
data-ppt-auto-generation
data-auto-start="{{ 'true' if auto_generation.can_auto_start else 'false' }}"

View File

@@ -217,6 +217,27 @@ def test_ppt_audit_history_shows_ppt_schedule_and_db_runs(client, monkeypatch):
assert '每日日報' in html
def test_ppt_audit_history_shows_recent_preview_workbench(client, monkeypatch, tmp_path):
"""有檔案時,頁面上方要先給可預覽簡報入口。"""
import zipfile
reports_dir = tmp_path / 'reports'
reports_dir.mkdir()
pptx = reports_dir / 'ocbot_daily_20260517.pptx'
with zipfile.ZipFile(pptx, 'w') as zf:
zf.writestr('[Content_Types].xml', '<Types></Types>')
monkeypatch.setenv('REPORTS_DIR', str(reports_dir))
r = client.get('/observability/ppt_audit_history?month=2026-05')
html = r.data.decode('utf-8')
assert r.status_code == 200
assert '最近可預覽簡報' in html
assert 'ocbot_daily_20260517.pptx' in html
assert '線上預覽' in html
def test_ppt_audit_file_view_renders_online_preview(client, monkeypatch, tmp_path):
"""PPTX view 入口應回站內預覽頁,而不是把 PPTX 直接丟給瀏覽器。"""
import zipfile

View File

@@ -123,6 +123,65 @@
gap: var(--momo-space-1, 4px);
}
.ppt-deck-workbench {
margin-top: var(--momo-space-4, 16px);
padding: var(--momo-space-4, 16px);
border: 1px solid var(--obs-line);
border-radius: var(--momo-radius-lg, 8px);
background:
radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px),
rgba(255, 255, 255, 0.5);
background-size: 10px 10px, auto;
box-shadow: var(--momo-shadow-md, 0 16px 38px rgba(70, 46, 28, 0.08));
}
.ppt-workbench-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--momo-space-3, 12px);
margin-bottom: var(--momo-space-3, 12px);
}
.ppt-deck-rail {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--momo-space-3, 12px);
}
.ppt-deck-card {
display: grid;
align-content: space-between;
min-height: 168px;
padding: var(--momo-space-3, 12px);
border: 1px solid var(--obs-line);
border-radius: var(--momo-radius-md, 6px);
background: rgba(255, 255, 255, 0.64);
}
.ppt-deck-card.is-disabled {
opacity: 0.68;
}
.ppt-deck-meta,
.ppt-deck-facts {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--momo-space-2, 8px);
color: var(--obs-muted);
font-size: var(--momo-text-caption, 12px);
}
.ppt-deck-card h3 {
margin: var(--momo-space-2, 8px) 0;
color: var(--obs-ink);
font-size: var(--momo-text-body, 14px);
font-weight: var(--momo-font-weight-black, 800);
line-height: 1.35;
overflow-wrap: anywhere;
}
.ppt-grid {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(330px, 0.8fr);
@@ -439,6 +498,10 @@
grid-template-columns: 1fr;
}
.ppt-deck-rail {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ppt-auto-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -452,6 +515,7 @@
.ppt-command,
.ppt-auto-grid,
.ppt-mini-grid,
.ppt-deck-rail,
.ppt-coverage-score,
.ppt-coverage-list {
grid-template-columns: 1fr;
@@ -459,6 +523,7 @@
.ppt-panel-head,
.ppt-table-title,
.ppt-workbench-head,
.ppt-run-log-head,
.ppt-run-row,
.ppt-coverage-row {