From 6b8e51124697979f0bfe06135822b4bcf13d204a Mon Sep 17 00:00:00 2001 From: OoO Date: Fri, 1 May 2026 00:43:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E8=A3=9C=E9=BD=8A=E6=B4=BB?= =?UTF-8?q?=E5=8B=95=E7=9C=8B=E6=9D=BF=E7=AF=A9=E9=81=B8=E8=88=87=E5=83=B9?= =?UTF-8?q?=E6=A0=BC=E6=AD=B7=E5=8F=B2=E5=8D=80=E9=96=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONSTITUTION.md | 2 +- app.py | 4 +- routes/api_routes.py | 82 +++++-- templates/dashboard_v2.html | 72 +++++- templates/edm_dashboard_v2.html | 401 ++++++++++++++++++++++++++++++- tests/test_frontend_v2_assets.py | 26 +- 6 files changed, 552 insertions(+), 35 deletions(-) diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 26b5274..9d4682b 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -2,7 +2,7 @@ > 本文件定義專案開發的核心準則與不可違反的規範 > **建立日期**: 2026-01-12 -> **當前版本**: V10.41 (Dashboard V2 price history chart restored) +> **當前版本**: V10.42 (Campaign V2 filters and ranged price history charts) > **最後更新**: 2026-05-01 --- diff --git a/app.py b/app.py index e83df17..87038cb 100644 --- a/app.py +++ b/app.py @@ -95,8 +95,8 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-05-01 V10.41: Restore dashboard v2 price history chart -SYSTEM_VERSION = "V10.41" +# 🚩 2026-05-01 V10.42: Campaign v2 filters and ranged price history charts +SYSTEM_VERSION = "V10.42" # ========================================== # 🔒 SQL Injection 防護函數 diff --git a/routes/api_routes.py b/routes/api_routes.py index 6dab103..9f995e7 100644 --- a/routes/api_routes.py +++ b/routes/api_routes.py @@ -286,27 +286,62 @@ def test_notification(): # 歷史查詢 API # ========================================== +PRICE_HISTORY_RANGES = { + 'week': {'days': 7, 'label': '近 7 天'}, + 'month': {'days': 30, 'label': '近 30 天'}, + 'quarter': {'days': 90, 'label': '近 90 天'}, + 'year': {'days': 365, 'label': '近 365 天'}, +} + + +def _resolve_history_range(): + range_key = request.args.get('range', 'month') + if range_key not in PRICE_HISTORY_RANGES: + range_key = 'month' + return range_key, PRICE_HISTORY_RANGES[range_key] + + +def _build_price_history_payload(session, product): + range_key, range_meta = _resolve_history_range() + start_date = datetime.now(TAIPEI_TZ) - timedelta(days=range_meta['days']) + + records = session.query(PriceRecord).filter( + PriceRecord.product_id == product.id, + PriceRecord.timestamp >= start_date + ).order_by(PriceRecord.timestamp).all() + + data = [{ + 't': r.timestamp.strftime('%Y-%m-%d %H:%M'), + 'p': r.price + } for r in records] + + return { + 'range': range_key, + 'range_label': range_meta['label'], + 'product': { + 'id': product.id, + 'i_code': product.i_code, + 'name': product.name, + }, + 'data': data + } + + @api_bp.route('/api/history/') @login_required def get_price_history(product_id): - """API: 取得商品過去 180 天的價格歷史""" + """API: 取得商品價格歷史,支援 week/month/quarter/year 區間""" db = DatabaseManager() session = db.get_session() try: - # 計算 180 天前的日期 (保持台北時區) - start_date = datetime.now(TAIPEI_TZ) - timedelta(days=180) + product = session.query(Product).filter(Product.id == product_id).first() + if not product: + return jsonify({'data': [], 'message': '找不到商品'}), 404 - records = session.query(PriceRecord).filter( - PriceRecord.product_id == product_id, - PriceRecord.timestamp >= start_date - ).order_by(PriceRecord.timestamp).all() - - data = [{ - 't': r.timestamp.strftime('%Y-%m-%d %H:%M'), - 'p': r.price - } for r in records] - - return jsonify(data) + payload = _build_price_history_payload(session, product) + if request.args.get('format') == 'v2': + return jsonify(payload) + return jsonify(payload['data']) except Exception as e: sys_log.error(f"[Web] [History] 獲取歷史價格失敗 | ProductID: {product_id} | Error: {e}") return jsonify([]), 500 @@ -314,6 +349,25 @@ def get_price_history(product_id): session.close() +@api_bp.route('/api/history/i-code/') +@login_required +def get_price_history_by_i_code(i_code): + """API: 以 MOMO 商品 i_code 取得主商品價格歷史""" + db = DatabaseManager() + session = db.get_session() + try: + product = session.query(Product).filter(Product.i_code == str(i_code)).first() + if not product: + return jsonify({'data': [], 'message': '找不到商品'}) + + return jsonify(_build_price_history_payload(session, product)) + except Exception as e: + sys_log.error(f"[Web] [History] 以 i_code 獲取歷史價格失敗 | ICode: {i_code} | Error: {e}") + return jsonify({'data': []}), 500 + finally: + session.close() + + @api_bp.route('/api/price_change_details') @login_required def get_price_change_details(): diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index 174cf3b..1ca4f1b 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -475,6 +475,31 @@ max-height: 380px; } + .dashboard-history-range { + display: inline-flex; + padding: 2px; + margin-top: 10px; + gap: 0; + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 4px; + } + + .dashboard-history-range button { + padding: 5px 10px; + color: var(--momo-text-secondary); + background: transparent; + border: 0; + border-radius: 3px; + font-size: 12px; + font-weight: 800; + } + + .dashboard-history-range button.is-active { + color: var(--momo-text-inverse); + background: var(--momo-ink); + } + @media (max-width: 980px) { .dashboard-kpi-grid, .dashboard-focus-grid { @@ -766,7 +791,13 @@ @@ -785,6 +816,9 @@