This commit is contained in:
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.101"
|
||||
SYSTEM_VERSION = "V10.102"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def load_scheduler_stats():
|
||||
return {}
|
||||
|
||||
|
||||
def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order, requested_slot=None):
|
||||
"""
|
||||
通用的促銷儀表板數據建構函數
|
||||
用於 edm 和 festival 兩種頁面類型
|
||||
@@ -135,20 +135,23 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
return f"{current_slot_hour:02d}:00"
|
||||
|
||||
active_tab = get_current_time_slot()
|
||||
if requested_slot in sorted_grouped_items:
|
||||
active_tab = requested_slot
|
||||
if active_tab not in sorted_grouped_items and sorted_grouped_items:
|
||||
active_tab = next(iter(sorted_grouped_items))
|
||||
|
||||
# 7. 計算在架天數與總銷量
|
||||
all_icodes_in_batch = [item.i_code for item in items_in_batch]
|
||||
# 7. 僅為首屏目前時段補齊列級資訊;其他時段以連結切換後再載入。
|
||||
visible_items = sorted_grouped_items.get(active_tab, [])
|
||||
visible_icodes = [item.i_code for item in visible_items]
|
||||
product_categories = {}
|
||||
days_on_shelf_map = {}
|
||||
total_sold_map = {}
|
||||
history_map = {}
|
||||
|
||||
if all_icodes_in_batch:
|
||||
if visible_icodes:
|
||||
# 從主商品表查詢分類
|
||||
main_products = session.query(Product.i_code, Product.category).filter(
|
||||
Product.i_code.in_(all_icodes_in_batch)
|
||||
Product.i_code.in_(visible_icodes)
|
||||
).all()
|
||||
product_categories = {p.i_code: p.category for p in main_products}
|
||||
|
||||
@@ -159,7 +162,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
PromoProduct.i_code,
|
||||
func.count(func.distinct(func.date(PromoProduct.crawled_at)))
|
||||
).filter(
|
||||
PromoProduct.i_code.in_(all_icodes_in_batch),
|
||||
PromoProduct.i_code.in_(visible_icodes),
|
||||
PromoProduct.page_type == page_type
|
||||
).group_by(PromoProduct.i_code).all()
|
||||
else:
|
||||
@@ -168,7 +171,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
PromoProduct.i_code,
|
||||
func.count(func.distinct(func.strftime('%Y-%m-%d', PromoProduct.crawled_at)))
|
||||
).filter(
|
||||
PromoProduct.i_code.in_(all_icodes_in_batch),
|
||||
PromoProduct.i_code.in_(visible_icodes),
|
||||
PromoProduct.page_type == page_type
|
||||
).group_by(PromoProduct.i_code).all()
|
||||
days_on_shelf_map = {r[0]: r[1] for r in days_on_shelf_q}
|
||||
@@ -180,7 +183,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
PromoProduct.i_code,
|
||||
func.min(PromoProduct.id).label('min_id')
|
||||
).filter(
|
||||
PromoProduct.i_code.in_(all_icodes_in_batch),
|
||||
PromoProduct.i_code.in_(visible_icodes),
|
||||
PromoProduct.remain_qty.isnot(None),
|
||||
PromoProduct.page_type == 'edm'
|
||||
).group_by(PromoProduct.i_code).subquery()
|
||||
@@ -190,7 +193,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
).join(first_qty_subq, PromoProduct.id == first_qty_subq.c.min_id).all()
|
||||
first_qty_map = {r[0]: r[1] for r in first_qty_records}
|
||||
|
||||
for item in items_in_batch:
|
||||
for item in visible_items:
|
||||
if item.i_code in first_qty_map and item.remain_qty is not None:
|
||||
initial_qty = first_qty_map[item.i_code]
|
||||
current_qty = item.remain_qty
|
||||
@@ -204,7 +207,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
PromoProduct.remain_qty,
|
||||
PromoProduct.crawled_at
|
||||
).filter(
|
||||
PromoProduct.i_code.in_(all_icodes_in_batch),
|
||||
PromoProduct.i_code.in_(visible_icodes),
|
||||
PromoProduct.crawled_at >= today_start
|
||||
).order_by(PromoProduct.crawled_at).all()
|
||||
|
||||
@@ -216,8 +219,8 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
if not history_map[key] or (history_map[key] and history_map[key][-1]['qty'] != rec.remain_qty):
|
||||
history_map[key].append({'time': rec.crawled_at.strftime('%H:%M'), 'qty': rec.remain_qty})
|
||||
|
||||
# 8. 附加分類資訊到每個 item
|
||||
for item in items_in_batch:
|
||||
# 8. 附加分類資訊到首屏可見 item
|
||||
for item in visible_items:
|
||||
item.safe_product_url = normalize_momo_product_url(item.url, item.i_code) or build_momo_product_url(item.i_code)
|
||||
item.main_category = product_categories.get(item.i_code)
|
||||
if item.main_category:
|
||||
@@ -226,9 +229,11 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
||||
item.total_sold = total_sold_map.get(item.i_code, 0)
|
||||
item.qty_history = history_map.get((item.i_code, item.time_slot), [])
|
||||
|
||||
# 9. 排序邏輯
|
||||
# 9. 排序邏輯:首屏只需要排序目前時段
|
||||
reverse = (order == 'desc')
|
||||
for time_slot in sorted_grouped_items:
|
||||
for time_slot in [active_tab]:
|
||||
if time_slot not in sorted_grouped_items:
|
||||
continue
|
||||
if sort_by == 'name':
|
||||
sorted_grouped_items[time_slot].sort(key=lambda x: x.name or '', reverse=reverse)
|
||||
elif sort_by == 'remain_qty':
|
||||
@@ -307,9 +312,10 @@ def edm_dashboard():
|
||||
|
||||
sort_by = request.args.get('sort_by', 'default')
|
||||
order = request.args.get('order', 'desc')
|
||||
requested_slot = request.args.get('slot')
|
||||
|
||||
try:
|
||||
data = _build_promo_dashboard_data(session, 'edm', '限時搶購', sort_by, order)
|
||||
data = _build_promo_dashboard_data(session, 'edm', '限時搶購', sort_by, order, requested_slot)
|
||||
|
||||
# 建立儀表板頁籤
|
||||
promo_pages = [
|
||||
@@ -362,9 +368,10 @@ def festival_dashboard():
|
||||
|
||||
sort_by = request.args.get('sort_by', 'default')
|
||||
order = request.args.get('order', 'desc')
|
||||
requested_slot = request.args.get('slot')
|
||||
|
||||
try:
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
|
||||
|
||||
# 建立儀表板頁籤
|
||||
promo_pages = [
|
||||
@@ -414,9 +421,10 @@ def mothers_day_dashboard():
|
||||
|
||||
sort_by = request.args.get('sort_by', 'default')
|
||||
order = request.args.get('order', 'desc')
|
||||
requested_slot = request.args.get('slot')
|
||||
|
||||
try:
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
|
||||
|
||||
# 建立儀表板頁籤
|
||||
promo_pages = [
|
||||
@@ -466,9 +474,10 @@ def valentine_520_dashboard():
|
||||
|
||||
sort_by = request.args.get('sort_by', 'default')
|
||||
order = request.args.get('order', 'desc')
|
||||
requested_slot = request.args.get('slot')
|
||||
|
||||
try:
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
|
||||
|
||||
# 建立儀表板頁籤
|
||||
promo_pages = [
|
||||
@@ -518,9 +527,10 @@ def labor_day_dashboard():
|
||||
|
||||
sort_by = request.args.get('sort_by', 'default')
|
||||
order = request.args.get('order', 'desc')
|
||||
requested_slot = request.args.get('slot')
|
||||
|
||||
try:
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
||||
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
|
||||
|
||||
# 建立儀表板頁籤
|
||||
promo_pages = [
|
||||
|
||||
@@ -333,6 +333,7 @@
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -824,10 +825,10 @@
|
||||
<div class="campaign-slot-tabs" role="tablist">
|
||||
{% for slot, stats in slot_stats.items() %}
|
||||
{% set slot_id = slugify(slot) %}
|
||||
<button class="campaign-slot-tab {% if slot == active_tab %}active{% endif %}" id="slot-{{ slot_id }}-tab" data-bs-toggle="tab" data-bs-target="#slot-{{ slot_id }}" type="button" role="tab">
|
||||
<a class="campaign-slot-tab {% if slot == active_tab %}active{% endif %}" id="slot-{{ slot_id }}-tab" href="{{ url_for(current_endpoint, slot=slot, sort_by=current_sort, order=current_order) }}" role="tab" aria-selected="{{ 'true' if slot == active_tab else 'false' }}">
|
||||
<span class="momo-mono">{{ slot }}</span>
|
||||
<span class="campaign-slot-count momo-mono">{{ stats.get('on_shelf', 0) }} 件</span>
|
||||
</button>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -839,9 +840,10 @@
|
||||
|
||||
{% if slot_stats %}
|
||||
<section class="tab-content">
|
||||
{% for slot, stats in slot_stats.items() %}
|
||||
{% set slot_id = slugify(slot) %}
|
||||
{% set items = grouped_items.get(slot, []) %}
|
||||
{% set slot = active_tab %}
|
||||
{% set stats = slot_stats.get(slot, {'new': 0, 'up': 0, 'down': 0, 'delisted_last_run': 0, 'on_shelf': 0}) %}
|
||||
{% set slot_id = slugify(slot) %}
|
||||
{% set items = grouped_items.get(slot, []) %}
|
||||
<div class="tab-pane fade {% if slot == active_tab %}show active{% endif %}" id="slot-{{ slot_id }}" role="tabpanel">
|
||||
<div class="campaign-table-card">
|
||||
<div class="campaign-table-head">
|
||||
@@ -863,16 +865,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, sort_by='name', order=next_order_name) }}">商品資訊</a>
|
||||
<a href="{{ url_for(current_endpoint, slot=active_tab, 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, sort_by='price', order=next_order_price) }}">價格</a>
|
||||
<a href="{{ url_for(current_endpoint, slot=active_tab, 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, sort_by='remain_qty', order=next_order_qty) }}">銷售 / 庫存</a>
|
||||
<a href="{{ url_for(current_endpoint, slot=active_tab, sort_by='remain_qty', order=next_order_qty) }}">銷售 / 庫存</a>
|
||||
{% else %}
|
||||
狀態
|
||||
{% endif %}
|
||||
@@ -1027,7 +1029,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user