-
04
+
04
{{ 'AI 挑品清單' if current_filter == 'ai_picks' else '商品列表' }}
{% if current_filter == 'ai_picks' %}
@@ -389,7 +389,7 @@
match {{ (competitor.match_score * 100) | round(0) | int }}%
{% endif %}
{% else %}
- 待比對
+ 待比對
{% endif %}
@@ -430,7 +430,7 @@
{% endif %}
{% else %}
- 尚無建議理由
+ 尚無建議理由
{% endif %}
|
{% endif %}
@@ -440,7 +440,7 @@
{% elif item.yesterday_diff < 0 %}
▼ -{{ item.yesterday_diff | abs | int | number_format }}
{% else %}
-
--
+
--
{% endif %}
@@ -450,13 +450,13 @@
{% elif week_diff < 0 %}
-{{ week_diff | abs | int | number_format }}
{% else %}
- --
+ --
{% endif %}
|
-
+ |
{{ item.record.timestamp.strftime('%m-%d %H:%M') if item.record.timestamp else '--' }}
|
-
+ |
{{ item.safe_created_at.strftime('%m-%d %H:%M') if item.safe_created_at else '--' }}
|
@@ -520,5 +520,6 @@
{% endblock %}
{% block extra_js %}
+
{% endblock %}
diff --git a/web/static/css/page-dashboard-v2.css b/web/static/css/page-dashboard-v2.css
index fa25cc5..869a866 100644
--- a/web/static/css/page-dashboard-v2.css
+++ b/web/static/css/page-dashboard-v2.css
@@ -389,6 +389,13 @@
flex-wrap: wrap;
}
+ .dashboard-section-index {
+ color: var(--momo-text-tertiary);
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ }
+
.dashboard-table-title {
color: var(--momo-text-primary);
font-size: 14px;
@@ -596,6 +603,14 @@
font-size: 10px;
}
+ .dashboard-muted {
+ color: var(--momo-text-tertiary);
+ }
+
+ .dashboard-table-time {
+ color: var(--momo-text-secondary);
+ }
+
.dashboard-pchome-price {
color: var(--momo-accent-strong);
font-size: 16px;
diff --git a/web/static/js/page-dashboard-v2.js b/web/static/js/page-dashboard-v2.js
index 2f5cd1e..7af936b 100644
--- a/web/static/js/page-dashboard-v2.js
+++ b/web/static/js/page-dashboard-v2.js
@@ -30,17 +30,27 @@ let priceChartInstance = null;
}
function ensureDashboardChart() {
+ if (window.EwoooCChartTheme && window.EwoooCChartTheme.loadChartJs) {
+ return window.EwoooCChartTheme.loadChartJs();
+ }
if (typeof Chart !== 'undefined') {
- return Promise.resolve();
+ return Promise.resolve(window.Chart);
}
if (dashboardChartLoader) {
return dashboardChartLoader;
}
dashboardChartLoader = new Promise((resolve, reject) => {
+ const existing = document.querySelector('script[data-chartjs-loader="dashboard"]');
+ if (existing) {
+ existing.addEventListener('load', () => resolve(window.Chart), { once: true });
+ existing.addEventListener('error', () => reject(new Error('Chart.js 載入失敗')), { once: true });
+ return;
+ }
const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
+ script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js';
script.async = true;
- script.onload = resolve;
+ script.dataset.chartjsLoader = 'dashboard';
+ script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error('Chart.js 載入失敗'));
document.head.appendChild(script);
});
@@ -235,29 +245,40 @@ let priceChartInstance = null;
});
});
- function triggerTask() {
- if (confirm('確定要手動執行全站爬蟲嗎?可能需要一段時間。')) {
- fetch('/api/run_task', {
- method: 'POST',
- headers: { 'X-CSRFToken': getCSRFToken() }
- })
- .then(response => response.json())
- .then(data => alert(data.message))
- .catch(error => alert('錯誤: ' + error));
+ document.querySelectorAll('[data-dashboard-auto-submit]').forEach(select => {
+ select.addEventListener('change', () => {
+ if (select.form) {
+ select.form.submit();
+ }
+ });
+ });
+
+ const dashboardTaskMap = {
+ crawler: {
+ confirmText: '確定要手動執行全站爬蟲嗎?可能需要一段時間。',
+ url: '/api/run_task'
+ },
+ notification: {
+ confirmText: '確定要發送今日商品異動通知嗎?',
+ url: '/api/trigger_momo_notification'
}
+ };
+
+ function runDashboardTask(taskName) {
+ const task = dashboardTaskMap[taskName];
+ if (!task || !confirm(task.confirmText)) return;
+ fetch(task.url, {
+ method: 'POST',
+ headers: { 'X-CSRFToken': getCSRFToken() }
+ })
+ .then(response => response.json())
+ .then(data => alert(data.message))
+ .catch(error => alert('錯誤: ' + error));
}
- function triggerNotification() {
- if (confirm('確定要發送今日商品異動通知嗎?')) {
- fetch('/api/trigger_momo_notification', {
- method: 'POST',
- headers: { 'X-CSRFToken': getCSRFToken() }
- })
- .then(response => response.json())
- .then(data => alert(data.message))
- .catch(error => alert('錯誤: ' + error));
- }
- }
+ document.querySelectorAll('[data-dashboard-task]').forEach(button => {
+ button.addEventListener('click', () => runDashboardTask(button.dataset.dashboardTask));
+ });
function trackMomoLinkClick(event) {
const link = event.target.closest('.momo-tracked-link');