This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.207 強化 `/observability/ppt_audit_history` 同頁線上預覽:所有可預覽簡報按鈕改為開啟頁內 PDF 預覽抽屜,保留開新頁與下載,降低產線頁來回跳轉成本並改善手機操作。
|
||||
- V10.205 補 `/observability/ppt_audit_history` 本頁批次 PDF 預熱:Preview Workbench 可一鍵預熱頁面上尚未快取的 PPTX,沿用單檔 JSON 端點逐一建立 PDF 快取並即時更新狀態。
|
||||
- V10.203 補 `/observability/ppt_audit_history` 單檔 PDF 預熱操作:未快取的可預覽 PPTX 會顯示「預熱 PDF」,透過 JSON 端點建立 PDF 快取並即時更新頁面狀態。
|
||||
- V10.201 強化 `/observability/ppt_audit_history` 線上預覽可診斷性:產線清單不觸發轉檔即可顯示 PDF 預覽快取狀態,Pipeline Health、Preview Workbench 與已產檔案表同步標記「PDF 快取 / 首次轉檔」。
|
||||
|
||||
@@ -124,7 +124,16 @@
|
||||
</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">
|
||||
<a class="btn btn-outline-primary btn-sm"
|
||||
href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-ppt-open-preview
|
||||
data-ppt-filename="{{ f.name }}"
|
||||
data-ppt-preview-title="{{ selected_report_type.label }} · {{ f.name }}"
|
||||
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='pdf') }}"
|
||||
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}"
|
||||
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
|
||||
<i class="fas fa-eye me-1"></i>線上預覽
|
||||
</a>
|
||||
{% if not f.preview_cache_ready %}
|
||||
@@ -196,7 +205,16 @@
|
||||
<div class="ppt-action-foot">
|
||||
<span class="ppt-run-status is-{{ lane.status }}">{{ item.status_label }}</span>
|
||||
{% if item.filename and lane.key in ['preview', 'database', 'triage'] %}
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename) }}" target="_blank" rel="noopener">
|
||||
<a class="btn btn-outline-primary btn-sm"
|
||||
href="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-ppt-open-preview
|
||||
data-ppt-filename="{{ item.filename }}"
|
||||
data-ppt-preview-title="{{ item.title }} · {{ item.filename }}"
|
||||
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename, action='pdf') }}"
|
||||
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename) }}"
|
||||
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename, action='download') }}">
|
||||
<i class="fas fa-eye me-1"></i>預覽
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -312,7 +330,16 @@
|
||||
<span class="ppt-run-status is-{{ run.status }}">{{ run.status_label }}</span>
|
||||
<small class="text-muted">{{ run.started_at }}{% if run.finished_at %} → {{ run.finished_at }}{% endif %}</small>
|
||||
{% if run.file_name %}
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name) }}" target="_blank" rel="noopener">
|
||||
<a class="btn btn-outline-primary btn-sm"
|
||||
href="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-ppt-open-preview
|
||||
data-ppt-filename="{{ run.file_name }}"
|
||||
data-ppt-preview-title="{{ run.report_label }} · {{ run.file_name }}"
|
||||
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name, action='pdf') }}"
|
||||
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name) }}"
|
||||
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name, action='download') }}">
|
||||
<i class="fas fa-eye me-1"></i>預覽
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -450,7 +477,16 @@
|
||||
<div class="ppt-file-actions">
|
||||
{% if f.file_exists %}
|
||||
{% if 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">
|
||||
<a class="btn btn-outline-primary btn-sm"
|
||||
href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-ppt-open-preview
|
||||
data-ppt-filename="{{ f.name }}"
|
||||
data-ppt-preview-title="{{ selected_report_type.label }} · {{ f.name }}"
|
||||
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='pdf') }}"
|
||||
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}"
|
||||
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
|
||||
<i class="fas fa-eye me-1"></i>線上預覽
|
||||
</a>
|
||||
{% if not f.preview_cache_ready %}
|
||||
@@ -569,6 +605,34 @@
|
||||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — PPT 視覺 QA 產線</small></p>
|
||||
</div>
|
||||
|
||||
<div class="ppt-preview-modal" data-ppt-preview-modal hidden aria-hidden="true">
|
||||
<div class="ppt-preview-backdrop" data-ppt-preview-close></div>
|
||||
<section class="ppt-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="pptPreviewTitle">
|
||||
<header class="ppt-preview-head">
|
||||
<div>
|
||||
<div class="ppt-label">Inline PDF Preview</div>
|
||||
<h2 id="pptPreviewTitle" data-ppt-preview-title>簡報線上預覽</h2>
|
||||
<small class="text-muted" data-ppt-preview-filename>尚未選擇檔案</small>
|
||||
</div>
|
||||
<div class="ppt-preview-actions">
|
||||
<a class="btn btn-outline-secondary btn-sm" href="#" target="_blank" rel="noopener" data-ppt-preview-open-page>
|
||||
<i class="fas fa-up-right-from-square me-1"></i>開新頁
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="#" data-ppt-preview-download>
|
||||
<i class="fas fa-download me-1"></i>下載
|
||||
</a>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-preview-close aria-label="關閉預覽">
|
||||
<i class="fas fa-xmark me-1"></i>關閉
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ppt-preview-frame-wrap">
|
||||
<div class="ppt-preview-loading" data-ppt-preview-loading>正在載入 PDF 預覽</div>
|
||||
<iframe data-ppt-preview-frame title="PPT PDF 線上預覽" loading="lazy"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template id="obs-ppt-audit-data">{{ audit_30d_stats | default({}) | tojson }}</template>
|
||||
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
|
||||
|
||||
@@ -279,6 +279,9 @@ def test_ppt_audit_history_shows_recent_preview_workbench(client, monkeypatch, t
|
||||
assert '最近可預覽簡報' in html
|
||||
assert 'ocbot_daily_20260517.pptx' in html
|
||||
assert '線上預覽' in html
|
||||
assert 'data-ppt-open-preview' in html
|
||||
assert 'data-ppt-preview-modal' in html
|
||||
assert 'data-ppt-preview-pdf' in html
|
||||
assert 'PDF 快取' in html
|
||||
assert '1</strong> 份 PDF 快取' in html
|
||||
|
||||
|
||||
@@ -199,6 +199,110 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ppt-preview-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.ppt-preview-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ppt-preview-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1080;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--momo-space-4, 16px);
|
||||
}
|
||||
|
||||
.ppt-preview-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(42, 37, 32, 0.48);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.ppt-preview-dialog {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
width: min(1180px, 100%);
|
||||
height: min(82vh, 860px);
|
||||
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),
|
||||
var(--obs-paper);
|
||||
background-size: 10px 10px, auto;
|
||||
box-shadow: 0 28px 80px rgba(42, 37, 32, 0.24);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ppt-preview-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--momo-space-3, 12px);
|
||||
padding: var(--momo-space-4, 16px);
|
||||
border-bottom: 1px solid var(--obs-line);
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.ppt-preview-head h2 {
|
||||
margin: 0 0 var(--momo-space-1, 4px);
|
||||
color: var(--obs-ink);
|
||||
font-size: var(--momo-text-title, 18px);
|
||||
font-weight: var(--momo-font-weight-black, 800);
|
||||
letter-spacing: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ppt-preview-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--momo-space-2, 8px);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ppt-preview-frame-wrap {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
background: rgba(255, 255, 255, 0.52);
|
||||
}
|
||||
|
||||
.ppt-preview-frame-wrap iframe {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 560px;
|
||||
border: 0;
|
||||
background: var(--obs-paper);
|
||||
}
|
||||
|
||||
.ppt-preview-loading {
|
||||
position: absolute;
|
||||
inset: var(--momo-space-4, 16px);
|
||||
z-index: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px dashed var(--obs-line);
|
||||
border-radius: var(--momo-radius-md, 6px);
|
||||
color: var(--obs-muted);
|
||||
font-family: var(--momo-font-mono);
|
||||
background:
|
||||
radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px),
|
||||
rgba(255, 255, 255, 0.42);
|
||||
background-size: 10px 10px, auto;
|
||||
}
|
||||
|
||||
.ppt-preview-frame-wrap.is-loaded .ppt-preview-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ppt-health-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.36fr) minmax(0, 0.64fr);
|
||||
@@ -800,10 +904,30 @@
|
||||
.ppt-table-title,
|
||||
.ppt-workbench-head,
|
||||
.ppt-workbench-actions,
|
||||
.ppt-preview-head,
|
||||
.ppt-run-log-head,
|
||||
.ppt-run-row,
|
||||
.ppt-coverage-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ppt-preview-modal {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ppt-preview-dialog {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ppt-preview-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ppt-preview-frame-wrap iframe {
|
||||
min-height: calc(100vh - 168px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,6 +596,71 @@
|
||||
function initPptAutoGeneration() {
|
||||
const panel = document.querySelector('[data-ppt-auto-generation]');
|
||||
const pageStatus = document.querySelector('[data-ppt-auto-status]');
|
||||
const previewModal = document.querySelector('[data-ppt-preview-modal]');
|
||||
const previewFrame = previewModal ? previewModal.querySelector('[data-ppt-preview-frame]') : null;
|
||||
const previewTitle = previewModal ? previewModal.querySelector('[data-ppt-preview-title]') : null;
|
||||
const previewFilename = previewModal ? previewModal.querySelector('[data-ppt-preview-filename]') : null;
|
||||
const previewOpenPage = previewModal ? previewModal.querySelector('[data-ppt-preview-open-page]') : null;
|
||||
const previewDownload = previewModal ? previewModal.querySelector('[data-ppt-preview-download]') : null;
|
||||
const previewLoading = previewModal ? previewModal.querySelector('[data-ppt-preview-loading]') : null;
|
||||
const previewFrameWrap = previewModal ? previewModal.querySelector('.ppt-preview-frame-wrap') : null;
|
||||
|
||||
function closePreviewModal() {
|
||||
if (!previewModal) return;
|
||||
previewModal.hidden = true;
|
||||
previewModal.setAttribute('aria-hidden', 'true');
|
||||
document.body.classList.remove('ppt-preview-open');
|
||||
if (previewFrame) {
|
||||
previewFrame.removeAttribute('src');
|
||||
}
|
||||
}
|
||||
|
||||
function openPreviewModal(trigger) {
|
||||
if (!previewModal || !previewFrame) return false;
|
||||
const pdfUrl = trigger.dataset.pptPreviewPdf || '';
|
||||
if (!pdfUrl) return false;
|
||||
const title = trigger.dataset.pptPreviewTitle || '簡報線上預覽';
|
||||
const filename = trigger.dataset.pptFilename || '';
|
||||
if (previewTitle) previewTitle.textContent = title;
|
||||
if (previewFilename) previewFilename.textContent = filename;
|
||||
if (previewOpenPage) previewOpenPage.href = trigger.dataset.pptPreviewPage || trigger.href || pdfUrl;
|
||||
if (previewDownload) previewDownload.href = trigger.dataset.pptDownloadUrl || '#';
|
||||
if (previewLoading) previewLoading.textContent = filename ? `${filename} 正在載入 PDF 預覽` : '正在載入 PDF 預覽';
|
||||
previewModal.hidden = false;
|
||||
previewModal.setAttribute('aria-hidden', 'false');
|
||||
document.body.classList.add('ppt-preview-open');
|
||||
if (previewFrameWrap) previewFrameWrap.classList.remove('is-loaded');
|
||||
previewFrame.src = pdfUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (previewModal) {
|
||||
if (previewFrame && previewFrame.dataset.boundLoad !== '1') {
|
||||
previewFrame.dataset.boundLoad = '1';
|
||||
previewFrame.addEventListener('load', () => {
|
||||
if (previewFrameWrap) previewFrameWrap.classList.add('is-loaded');
|
||||
});
|
||||
}
|
||||
previewModal.querySelectorAll('[data-ppt-preview-close]').forEach(button => {
|
||||
if (button.dataset.bound === '1') return;
|
||||
button.dataset.bound = '1';
|
||||
button.addEventListener('click', closePreviewModal);
|
||||
});
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' && !previewModal.hidden) closePreviewModal();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-ppt-open-preview]').forEach(link => {
|
||||
if (link.dataset.bound === '1') return;
|
||||
link.dataset.bound = '1';
|
||||
link.addEventListener('click', event => {
|
||||
if (openPreviewModal(link)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-ppt-aider-heal]').forEach(button => {
|
||||
if (button.dataset.bound === '1') return;
|
||||
button.dataset.bound = '1';
|
||||
|
||||
Reference in New Issue
Block a user