feat(frontend): 將 V2 設為正式預設介面
All checks were successful
CD Pipeline / deploy (push) Successful in 1m46s

This commit is contained in:
OoO
2026-05-01 00:28:56 +08:00
parent 15b3bae9cb
commit 8e9fb3dd17
11 changed files with 74 additions and 68 deletions

View File

@@ -2,7 +2,7 @@
> 本文件定義專案開發的核心準則與不可違反的規範
> **建立日期**: 2026-01-12
> **當前版本**: V10.39 (Vendor stockout import v2 feature flag)
> **當前版本**: V10.40 (Frontend V2 production default)
> **最後更新**: 2026-05-01
---

4
app.py
View File

@@ -95,8 +95,8 @@ except Exception as e:
sys_log.error(f"無法檢測磁碟空間: {e}")
# 🚩 系統版本定義 (備份與顯示用)
# 🚩 2026-05-01 V10.39: Vendor stockout import v2 feature flag
SYSTEM_VERSION = "V10.39"
# 🚩 2026-05-01 V10.40: Frontend V2 as production default
SYSTEM_VERSION = "V10.40"
# ==========================================
# 🔒 SQL Injection 防護函數

View File

@@ -619,7 +619,7 @@ def index():
category_name = item['record'].product.category
item['category_color'] = get_color_for_string(category_name)
template_name = 'dashboard_v2.html' if request.args.get('ui') == 'v2' else 'dashboard.html'
template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'
return render_template(template_name,
total_products=total_products_history,

View File

@@ -321,7 +321,7 @@ def edm_dashboard():
scheduler_stats = load_scheduler_stats()
now_taipei = datetime.now(TAIPEI_TZ)
template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html'
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
@@ -375,7 +375,7 @@ def festival_dashboard():
scheduler_stats = load_scheduler_stats()
template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html'
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
@@ -427,7 +427,7 @@ def mothers_day_dashboard():
scheduler_stats = load_scheduler_stats()
template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html'
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
@@ -479,7 +479,7 @@ def valentine_520_dashboard():
scheduler_stats = load_scheduler_stats()
template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html'
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
@@ -531,7 +531,7 @@ def labor_day_dashboard():
scheduler_stats = load_scheduler_stats()
template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html'
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,

View File

@@ -198,38 +198,38 @@ def _get_vendor_stockout_list_context():
def index():
"""廠商缺貨系統主頁"""
sys_log.info("[VendorStockout] 進入廠商缺貨系統主頁")
if request.args.get('ui') == 'v2':
return render_template(
'vendor_stockout_index_v2.html',
active_page='vendor_stockout',
stats=_get_vendor_dashboard_stats()
)
return render_template('vendor_stockout/index.html')
if request.args.get('ui') == 'legacy':
return render_template('vendor_stockout/index.html')
return render_template(
'vendor_stockout_index_v2.html',
active_page='vendor_stockout',
stats=_get_vendor_dashboard_stats()
)
@vendor_bp.route('/import')
def import_page():
"""Excel 匯入頁面"""
sys_log.info("[VendorStockout] 進入匯入頁面")
if request.args.get('ui') == 'v2':
return render_template(
'vendor_stockout_import_v2.html',
active_page='vendor_stockout'
)
return render_template('vendor_stockout/import.html')
if request.args.get('ui') == 'legacy':
return render_template('vendor_stockout/import.html')
return render_template(
'vendor_stockout_import_v2.html',
active_page='vendor_stockout'
)
@vendor_bp.route('/list')
def list_page():
"""缺貨清單頁面"""
sys_log.info("[VendorStockout] 進入缺貨清單頁面")
if request.args.get('ui') == 'v2':
return render_template(
'vendor_stockout_list_v2.html',
active_page='vendor_stockout',
**_get_vendor_stockout_list_context()
)
return render_template('vendor_stockout/list.html')
if request.args.get('ui') == 'legacy':
return render_template('vendor_stockout/list.html')
return render_template(
'vendor_stockout_list_v2.html',
active_page='vendor_stockout',
**_get_vendor_stockout_list_context()
)
@vendor_bp.route('/vendor-management')

View File

@@ -537,11 +537,11 @@
</button>
<div class="dashboard-segmented">
<a class="{% if current_filter == 'all' %}is-active{% endif %}" href="{{ url_for('dashboard.index', ui='v2', filter='all', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">全部</a>
<a class="{% if current_filter == 'new' %}is-active{% endif %}" href="{{ url_for('dashboard.index', ui='v2', filter='new', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">新上架</a>
<a class="{% if current_filter == 'increase' %}is-active{% endif %}" href="{{ url_for('dashboard.index', ui='v2', filter='increase', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">漲價</a>
<a class="{% if current_filter == 'decrease' %}is-active{% endif %}" href="{{ url_for('dashboard.index', ui='v2', filter='decrease', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">降價</a>
<a class="{% if current_filter == 'delisted' %}is-active{% endif %}" href="{{ url_for('dashboard.index', ui='v2', filter='delisted', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">下架</a>
<a class="{% if current_filter == 'all' %}is-active{% endif %}" href="{{ url_for('dashboard.index', filter='all', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">全部</a>
<a class="{% if current_filter == 'new' %}is-active{% endif %}" href="{{ url_for('dashboard.index', filter='new', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">新上架</a>
<a class="{% if current_filter == 'increase' %}is-active{% endif %}" href="{{ url_for('dashboard.index', filter='increase', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">漲價</a>
<a class="{% if current_filter == 'decrease' %}is-active{% endif %}" href="{{ url_for('dashboard.index', filter='decrease', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">降價</a>
<a class="{% if current_filter == 'delisted' %}is-active{% endif %}" href="{{ url_for('dashboard.index', filter='delisted', category=current_category, q=search_query, sort_by=current_sort, order=current_order) }}">下架</a>
</div>
<button class="dashboard-action-button" type="button" onclick="triggerTask()">
@@ -576,16 +576,16 @@
<th>分類</th>
<th>商品名稱</th>
<th class="text-end">
<a href="{{ url_for('dashboard.index', ui='v2', page=1, sort_by='price', order='asc' if current_sort == 'price' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">當天價格</a>
<a href="{{ url_for('dashboard.index', page=1, sort_by='price', order='asc' if current_sort == 'price' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">當天價格</a>
</th>
<th class="text-end">
<a href="{{ url_for('dashboard.index', ui='v2', page=1, sort_by='yesterday_change', order='asc' if current_sort == 'yesterday_change' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">昨日漲跌</a>
<a href="{{ url_for('dashboard.index', page=1, sort_by='yesterday_change', order='asc' if current_sort == 'yesterday_change' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">昨日漲跌</a>
</th>
<th class="text-end">
<a href="{{ url_for('dashboard.index', ui='v2', page=1, sort_by='week_change', order='asc' if current_sort == 'week_change' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">週漲跌</a>
<a href="{{ url_for('dashboard.index', page=1, sort_by='week_change', order='asc' if current_sort == 'week_change' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">週漲跌</a>
</th>
<th class="text-end">
<a href="{{ url_for('dashboard.index', ui='v2', page=1, sort_by='timestamp', order='asc' if current_sort == 'timestamp' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">更新時間</a>
<a href="{{ url_for('dashboard.index', page=1, sort_by='timestamp', order='asc' if current_sort == 'timestamp' and current_order == 'desc' else 'desc', category=current_category, filter=current_filter, q=search_query) }}">更新時間</a>
</th>
<th class="text-end">上架時間</th>
</tr>
@@ -654,11 +654,11 @@
{% if total_pages > 1 %}
<div class="dashboard-pagination">
{% if current_page > 1 %}
<a class="dashboard-action-link" href="{{ url_for('dashboard.index', ui='v2', page=current_page - 1, category=current_category, filter=current_filter, q=search_query, sort_by=current_sort, order=current_order) }}">上一頁</a>
<a class="dashboard-action-link" href="{{ url_for('dashboard.index', page=current_page - 1, category=current_category, filter=current_filter, q=search_query, sort_by=current_sort, order=current_order) }}">上一頁</a>
{% endif %}
<span class="dashboard-table-meta momo-mono">第 {{ current_page }} / {{ total_pages }} 頁</span>
{% if current_page < total_pages %}
<a class="dashboard-action-link" href="{{ url_for('dashboard.index', ui='v2', page=current_page + 1, category=current_category, filter=current_filter, q=search_query, sort_by=current_sort, order=current_order) }}">下一頁</a>
<a class="dashboard-action-link" href="{{ url_for('dashboard.index', page=current_page + 1, category=current_category, filter=current_filter, q=search_query, sort_by=current_sort, order=current_order) }}">下一頁</a>
{% endif %}
</div>
{% endif %}

View File

@@ -474,7 +474,7 @@
<section>
<div class="campaign-switcher">
{% for page in promo_pages %}
<a class="campaign-tab {% if page.id == current_promo_page %}is-active{% endif %}" href="{{ page.url }}?ui=v2">
<a class="campaign-tab {% if page.id == current_promo_page %}is-active{% endif %}" href="{{ page.url }}">
<span>{{ page.name }}</span>
{% if page.id == current_promo_page %}
<span class="campaign-tab-count momo-mono">{{ total_edm_products | number_format }}</span>
@@ -607,16 +607,16 @@
<th>分類</th>
<th>
{% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else 'desc' %}
<a href="{{ url_for(current_endpoint, ui='v2', sort_by='name', order=next_order_name) }}">商品資訊</a>
<a href="{{ url_for(current_endpoint, sort_by='name', order=next_order_name) }}">商品資訊</a>
</th>
<th class="text-end">
{% set next_order_price = 'asc' if current_sort == 'price' and current_order == 'desc' else 'desc' %}
<a href="{{ url_for(current_endpoint, ui='v2', sort_by='price', order=next_order_price) }}">價格</a>
<a href="{{ url_for(current_endpoint, sort_by='price', order=next_order_price) }}">價格</a>
</th>
<th class="text-center">
{% if current_promo_page == 'edm' %}
{% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else 'desc' %}
<a href="{{ url_for(current_endpoint, ui='v2', sort_by='remain_qty', order=next_order_qty) }}">倒數組數</a>
<a href="{{ url_for(current_endpoint, sort_by='remain_qty', order=next_order_qty) }}">倒數組數</a>
{% else %}
狀態
{% endif %}

View File

@@ -340,11 +340,11 @@
上傳正式缺貨 Excel 後,系統會使用既有匯入 API 寫入 vendor_stockout完成後直接回傳批次編號與成功、重複、失敗筆數。
</p>
<div class="stockout-import-actions mt-3">
<a class="stockout-action" href="{{ url_for('vendor.index', ui='v2') }}">
<a class="stockout-action" href="{{ url_for('vendor.index') }}">
<i class="fas fa-arrow-left"></i>
回總覽
</a>
<a class="stockout-action" href="{{ url_for('vendor.list_page', ui='v2') }}">
<a class="stockout-action" href="{{ url_for('vendor.list_page') }}">
<i class="fas fa-table-list"></i>
缺貨清單
</a>
@@ -436,7 +436,7 @@
</div>
<div class="stockout-batch-id momo-mono">批次編號 <span id="stockoutBatchId"></span></div>
<div class="stockout-import-actions mt-3">
<a class="stockout-action is-primary" href="{{ url_for('vendor.list_page', ui='v2') }}">
<a class="stockout-action is-primary" href="{{ url_for('vendor.list_page') }}">
<i class="fas fa-table-list"></i>
查看缺貨清單
</a>

View File

@@ -341,11 +341,11 @@
</p>
</div>
<div class="vendor-actions">
<a class="vendor-action is-primary" href="{{ url_for('vendor.import_page', ui='v2') }}">
<a class="vendor-action is-primary" href="{{ url_for('vendor.import_page') }}">
<i class="fas fa-file-import"></i>
匯入 Excel
</a>
<a class="vendor-action" href="{{ url_for('vendor.list_page', ui='v2') }}">
<a class="vendor-action" href="{{ url_for('vendor.list_page') }}">
<i class="fas fa-list-check"></i>
查看清單
</a>
@@ -408,14 +408,14 @@
<span class="title">工作入口</span>
</div>
<div class="vendor-flow-grid">
<a class="vendor-flow-card" href="{{ url_for('vendor.import_page', ui='v2') }}">
<a class="vendor-flow-card" href="{{ url_for('vendor.import_page') }}">
<span class="vendor-flow-icon"><i class="fas fa-file-import"></i></span>
<span>
<span class="vendor-flow-title">Excel 匯入</span>
<span class="vendor-flow-meta momo-mono">建立缺貨批次</span>
</span>
</a>
<a class="vendor-flow-card" href="{{ url_for('vendor.list_page', ui='v2') }}">
<a class="vendor-flow-card" href="{{ url_for('vendor.list_page') }}">
<span class="vendor-flow-icon"><i class="fas fa-table-list"></i></span>
<span>
<span class="vendor-flow-title">缺貨清單</span>

View File

@@ -330,11 +330,11 @@
</p>
</div>
<div class="stockout-actions">
<a class="stockout-action" href="{{ url_for('vendor.index', ui='v2') }}">
<a class="stockout-action" href="{{ url_for('vendor.index') }}">
<i class="fas fa-arrow-left"></i>
回總覽
</a>
<a class="stockout-action is-primary" href="{{ url_for('vendor.import_page', ui='v2') }}">
<a class="stockout-action is-primary" href="{{ url_for('vendor.import_page') }}">
<i class="fas fa-file-import"></i>
匯入 Excel
</a>
@@ -395,11 +395,11 @@
</section>
<nav class="stockout-status-tabs" aria-label="缺貨狀態篩選">
<a class="stockout-tab {% if current_status == 'all' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', ui='v2', q=search_query, batch=current_batch, sort=current_sort) }}">全部</a>
<a class="stockout-tab {% if current_status == 'pending' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', ui='v2', status='pending', q=search_query, batch=current_batch, sort=current_sort) }}">待發送</a>
<a class="stockout-tab {% if current_status == 'sent' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', ui='v2', status='sent', q=search_query, batch=current_batch, sort=current_sort) }}">已發送</a>
<a class="stockout-tab {% if current_status == 'failed' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', ui='v2', status='failed', q=search_query, batch=current_batch, sort=current_sort) }}">失敗</a>
<a class="stockout-tab {% if current_status == 'duplicate' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', ui='v2', status='duplicate', q=search_query, batch=current_batch, sort=current_sort) }}">重複</a>
<a class="stockout-tab {% if current_status == 'all' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', q=search_query, batch=current_batch, sort=current_sort) }}">全部</a>
<a class="stockout-tab {% if current_status == 'pending' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', status='pending', q=search_query, batch=current_batch, sort=current_sort) }}">待發送</a>
<a class="stockout-tab {% if current_status == 'sent' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', status='sent', q=search_query, batch=current_batch, sort=current_sort) }}">已發送</a>
<a class="stockout-tab {% if current_status == 'failed' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', status='failed', q=search_query, batch=current_batch, sort=current_sort) }}">失敗</a>
<a class="stockout-tab {% if current_status == 'duplicate' %}is-active{% endif %}" href="{{ url_for('vendor.list_page', status='duplicate', q=search_query, batch=current_batch, sort=current_sort) }}">重複</a>
</nav>
<section class="stockout-table-card" aria-label="缺貨資料表">
@@ -472,11 +472,11 @@
<div class="stockout-pagination">
{% if current_page > 1 %}
<a class="stockout-page-link" href="{{ url_for('vendor.list_page', ui='v2', page=current_page - 1, page_size=page_size, status=current_status, q=search_query, batch=current_batch, sort=current_sort) }}">上一頁</a>
<a class="stockout-page-link" href="{{ url_for('vendor.list_page', page=current_page - 1, page_size=page_size, status=current_status, q=search_query, batch=current_batch, sort=current_sort) }}">上一頁</a>
{% endif %}
<span class="stockout-page-link momo-mono">{{ current_page }} / {{ total_pages }}</span>
{% if current_page < total_pages %}
<a class="stockout-page-link" href="{{ url_for('vendor.list_page', ui='v2', page=current_page + 1, page_size=page_size, status=current_status, q=search_query, batch=current_batch, sort=current_sort) }}">下一頁</a>
<a class="stockout-page-link" href="{{ url_for('vendor.list_page', page=current_page + 1, page_size=page_size, status=current_status, q=search_query, batch=current_batch, sort=current_sort) }}">下一頁</a>
{% endif %}
</div>
</section>

View File

@@ -39,37 +39,40 @@ def test_frontend_v2_shell_uses_real_runtime_context():
assert all(marker not in combined for marker in forbidden_markers)
def test_dashboard_v2_is_feature_flagged_and_uses_real_dashboard_data():
def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
assert "request.args.get('ui') == 'v2'" in route_source
assert "template_name = 'dashboard_v2.html'" in route_source
assert "request.args.get('ui') == 'legacy'" in route_source
assert "template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'" in route_source
assert "get_full_dashboard_data()" in route_source
assert "MockRecord" not in route_source
assert "{% for item in items %}" in dashboard
assert "ui='v2'" not in dashboard
assert "mockProducts" not in dashboard
assert "假商品" not in dashboard
def test_edm_dashboard_v2_is_feature_flagged_and_uses_real_campaign_data():
def test_edm_dashboard_v2_is_production_default_and_uses_real_campaign_data():
route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8")
template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8")
assert route_source.count("request.args.get('ui') == 'v2'") == 5
assert "template_name = 'edm_dashboard_v2.html'" in route_source
assert route_source.count("request.args.get('ui') == 'legacy'") == 5
assert "template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'" in route_source
assert "{% for slot, stats in slot_stats.items() %}" in template
assert "{% for item in items %}" in template
assert "scheduler_stats.get(task_key, [])" in template
assert "?ui=v2" not in template
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "假商品" not in template
def test_vendor_stockout_v2_is_feature_flagged_and_uses_real_vendor_data():
def test_vendor_stockout_v2_is_production_default_and_uses_real_vendor_data():
route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
template = (ROOT / "templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8")
assert "request.args.get('ui') == 'v2'" in route_source
assert "request.args.get('ui') == 'legacy'" in route_source
assert not (ROOT / "web/templates/vendor_stockout_index_v2.html").exists()
assert "vendor_stockout_index_v2.html" in route_source
assert "_get_vendor_dashboard_stats()" in route_source
@@ -77,6 +80,7 @@ def test_vendor_stockout_v2_is_feature_flagged_and_uses_real_vendor_data():
assert "EmailSendLog" in route_source
assert "stats.pending_stockouts" in template
assert "stats.email_success_rate" in template
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "" not in template
@@ -97,6 +101,7 @@ def test_vendor_stockout_list_v2_is_feature_flagged_and_uses_real_stockout_rows(
assert "record.product_code" in template
assert "record.product_name" in template
assert "record.vendor_name" in template
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "" not in template
@@ -112,5 +117,6 @@ def test_vendor_stockout_import_v2_is_feature_flagged_and_does_not_ship_sample_r
assert "fetchWithCSRF('/vendor-stockout/api/import/excel'" in template
assert "vendor_stockout" in template
assert "範例" not in template_function
assert "ui='v2'" not in template
assert "mock" not in template.lower()
assert "" not in template