This commit is contained in:
@@ -4,6 +4,10 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.151 接續前端 V3 全站 UI/UX:廠商缺貨 `/vendor-stockout/vendor-management`、`/vendor-stockout/send-email`、`/vendor-stockout/history` 改走新版 `ewoooc_base.html` shell 與 `page-vendor-tools.css`,移除舊紫藍 navbar/live route。
|
||||
- V10.151 補 `/abc_analysis/detail` 新版 ABC 詳情頁與安全 loading state,移除 raw HTML fallback,資料表維持正式快取資料來源與匯出連結。
|
||||
- V10.151 補 `/login` 新版登入頁:暖紙背景、點陣視覺、EwoooC 品牌、手機版 390px 無水平 overflow。
|
||||
- V10.151 本機全站 responsive overflow guard 通過 147/147;`?ui=legacy` 入口已不再回到舊 `dashboard.html` / `edm_dashboard.html` / vendor legacy templates。
|
||||
- 新版設計系統資產已落到正式 Flask static path:`web/static/css/ewoooc-tokens.css`、`web/static/css/ewoooc-shell.css`、`web/static/css/ewoooc-dotmatrix.css`、`web/static/css/page-*.css` 與對應 `web/static/js/page-*.js`。
|
||||
- V3 page-level 資產已補做設計規範清理:移除 UI 表面殘留 radial-gradient / pure-white rgba / `bg-white` header,AI 推薦頁 keyword 狀態改用語意 class。
|
||||
- `ewoooc_base.html` 補 `extra_head` / `content` / `extra_scripts` 相容 block,支援新版包混用 block 命名,避免頁面空白或互動 JS 不輸出。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.150"
|
||||
SYSTEM_VERSION = "V10.151"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -1461,7 +1461,7 @@ def index():
|
||||
data['competitor_overview'] = competitor_overview
|
||||
_DASHBOARD_DATA_CACHE['full_data'] = data
|
||||
_write_shared_full_dashboard_cache(data)
|
||||
template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'
|
||||
template_name = 'dashboard_v2.html'
|
||||
|
||||
return render_template(template_name,
|
||||
total_products=total_products_history,
|
||||
|
||||
@@ -374,7 +374,7 @@ def edm_dashboard():
|
||||
scheduler_stats = load_scheduler_stats()
|
||||
now_taipei = datetime.now(TAIPEI_TZ)
|
||||
|
||||
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
|
||||
template_name = 'edm_dashboard_v2.html'
|
||||
|
||||
return render_template(template_name,
|
||||
promo_pages=promo_pages,
|
||||
@@ -429,7 +429,7 @@ def festival_dashboard():
|
||||
|
||||
scheduler_stats = load_scheduler_stats()
|
||||
|
||||
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
|
||||
template_name = 'edm_dashboard_v2.html'
|
||||
|
||||
return render_template(template_name,
|
||||
promo_pages=promo_pages,
|
||||
@@ -482,7 +482,7 @@ def mothers_day_dashboard():
|
||||
|
||||
scheduler_stats = load_scheduler_stats()
|
||||
|
||||
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
|
||||
template_name = 'edm_dashboard_v2.html'
|
||||
|
||||
return render_template(template_name,
|
||||
promo_pages=promo_pages,
|
||||
@@ -535,7 +535,7 @@ def valentine_520_dashboard():
|
||||
|
||||
scheduler_stats = load_scheduler_stats()
|
||||
|
||||
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
|
||||
template_name = 'edm_dashboard_v2.html'
|
||||
|
||||
return render_template(template_name,
|
||||
promo_pages=promo_pages,
|
||||
@@ -588,7 +588,7 @@ def labor_day_dashboard():
|
||||
|
||||
scheduler_stats = load_scheduler_stats()
|
||||
|
||||
template_name = 'edm_dashboard.html' if request.args.get('ui') == 'legacy' else 'edm_dashboard_v2.html'
|
||||
template_name = 'edm_dashboard_v2.html'
|
||||
|
||||
return render_template(template_name,
|
||||
promo_pages=promo_pages,
|
||||
|
||||
@@ -1938,45 +1938,19 @@ def abc_analysis_detail():
|
||||
target_df, cols_map, err = _get_filtered_sales_data(table_name)
|
||||
|
||||
if err:
|
||||
# V-Fix: 如果自動重載也失敗,則顯示稍後再試,並引導回主頁面
|
||||
return f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>數據加載中 - WOOO TECH</title>
|
||||
<style>
|
||||
body {{ font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f8f9fa; }}
|
||||
.card {{ background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); text-align: center; }}
|
||||
.spinner {{ border: 3px solid #f3f3f3; border-top: 3px solid #1e3c72; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 1rem; }}
|
||||
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="spinner"></div>
|
||||
<h3>數據準備中</h3>
|
||||
<p>正在自動重新加載數據,請稍後...</p>
|
||||
<script>
|
||||
// 1.5 秒後嘗試重載當前頁面
|
||||
setTimeout(function() {{
|
||||
window.location.reload();
|
||||
}}, 1500);
|
||||
|
||||
// 若重試 3 次仍失敗,引導回主頁
|
||||
let retryCount = parseInt(sessionStorage.getItem('abc_retry') || '0');
|
||||
if (retryCount > 3) {{
|
||||
sessionStorage.removeItem('abc_retry');
|
||||
alert('數據載入過久,請先在業績分析主頁重新整理。');
|
||||
window.location.href = '/sales_analysis';
|
||||
}} else {{
|
||||
sessionStorage.setItem('abc_retry', retryCount + 1);
|
||||
}}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''', 200
|
||||
return render_template(
|
||||
'abc_analysis_detail.html',
|
||||
loading_state=True,
|
||||
info={'title': '數據準備中', 'desc': '業績分析快取正在重新載入。'},
|
||||
items=[],
|
||||
cols={},
|
||||
current_factor=0,
|
||||
target_class=target_class,
|
||||
total_revenue=0,
|
||||
sort_col_index=0,
|
||||
query_string=request.query_string.decode(),
|
||||
active_page='sales',
|
||||
), 200
|
||||
|
||||
# 恢復欄位變數
|
||||
col_name = cols_map.get('name')
|
||||
@@ -1995,6 +1969,7 @@ def abc_analysis_detail():
|
||||
# 3. 執行 ABC 分類
|
||||
items = []
|
||||
total_revenue = 0
|
||||
current_factor = 0.0
|
||||
if col_amount and not target_df.empty:
|
||||
# V-Fix: 先針對商品進行聚合,確保 ABC 分析是基於「商品總銷量」而非「單筆訂單」
|
||||
agg_rules = {col_amount: 'sum'}
|
||||
@@ -2081,7 +2056,8 @@ def abc_analysis_detail():
|
||||
cols={'name': col_name, 'amount': col_amount, 'qty': col_qty, 'cat': col_category,
|
||||
'vendor': col_vendor, 'brand': col_brand, 'cost': col_cost, 'profit': col_profit, 'date': col_date, 'pid': col_pid},
|
||||
# 傳遞當前查詢參數以供匯出連結使用
|
||||
query_string=request.query_string.decode())
|
||||
query_string=request.query_string.decode(),
|
||||
active_page='sales')
|
||||
|
||||
except Exception as e:
|
||||
sys_log.error(f"ABC Detail Error: {e}")
|
||||
|
||||
@@ -63,8 +63,6 @@ vendor_db = VendorDatabaseManager()
|
||||
def index():
|
||||
"""廠商缺貨系統主頁"""
|
||||
sys_log.info("[VendorStockout] 進入廠商缺貨系統主頁")
|
||||
if request.args.get('ui') == 'legacy':
|
||||
return _render_vendor_template('vendor_stockout/index.html')
|
||||
return render_template(
|
||||
'vendor_stockout_index_v2.html',
|
||||
active_page='vendor_stockout',
|
||||
@@ -76,8 +74,6 @@ def index():
|
||||
def import_page():
|
||||
"""Excel 匯入頁面"""
|
||||
sys_log.info("[VendorStockout] 進入匯入頁面")
|
||||
if request.args.get('ui') == 'legacy':
|
||||
return _render_vendor_template('vendor_stockout/import.html')
|
||||
return render_template(
|
||||
'vendor_stockout_import_v2.html',
|
||||
active_page='vendor_stockout'
|
||||
@@ -88,8 +84,6 @@ def import_page():
|
||||
def list_page():
|
||||
"""缺貨清單頁面"""
|
||||
sys_log.info("[VendorStockout] 進入缺貨清單頁面")
|
||||
if request.args.get('ui') == 'legacy':
|
||||
return _render_vendor_template('vendor_stockout/list.html')
|
||||
return render_template(
|
||||
'vendor_stockout_list_v2.html',
|
||||
active_page='vendor_stockout',
|
||||
@@ -109,21 +103,21 @@ def list_page():
|
||||
def vendor_management_page():
|
||||
"""廠商管理頁面"""
|
||||
sys_log.info("[VendorStockout] 進入廠商管理頁面")
|
||||
return _render_vendor_template('vendor_stockout/vendor_management.html')
|
||||
return render_template('vendor_stockout_vendor_management_v2.html', active_page='vendor_stockout')
|
||||
|
||||
|
||||
@vendor_bp.route('/send-email')
|
||||
def send_email_page():
|
||||
"""郵件發送頁面"""
|
||||
sys_log.info("[VendorStockout] 進入郵件發送頁面")
|
||||
return _render_vendor_template('vendor_stockout/send_email.html')
|
||||
return render_template('vendor_stockout_send_email_v2.html', active_page='vendor_stockout')
|
||||
|
||||
|
||||
@vendor_bp.route('/history')
|
||||
def history_page():
|
||||
"""發送歷史頁面"""
|
||||
sys_log.info("[VendorStockout] 進入發送歷史頁面")
|
||||
return _render_vendor_template('vendor_stockout/history.html')
|
||||
return render_template('vendor_stockout_history_v2.html', active_page='vendor_stockout')
|
||||
|
||||
|
||||
# ==========================================
|
||||
|
||||
@@ -1,196 +1,184 @@
|
||||
<!-- cspell:ignore MOMO datatables -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ABC 分析詳情 - {{ info.title }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
|
||||
<style>
|
||||
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f4f6f9;
|
||||
padding-top: 70px;
|
||||
}
|
||||
.card { border: none; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.03); margin-bottom: 1.5rem; background: #fff; }
|
||||
.table th { font-weight: 600; color: #495057; background-color: #f8f9fa; }
|
||||
.text-truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
{% extends "ewoooc_base.html" %}
|
||||
{% block title %}ABC 分析詳情 - {{ info.title if info else '資料準備中' }} - EwoooC{% endblock %}
|
||||
|
||||
.navbar-dark.bg-primary {
|
||||
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-abc-analysis-detail.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
.navbar-dark .navbar-brand {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link {
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link:hover {
|
||||
color: #ffffff !important;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link.active {
|
||||
color: #ffffff !important;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-text {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-body-tertiary">
|
||||
{% include 'components/_navbar.html' %}
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 mt-4">
|
||||
<div>
|
||||
<h4 class="mb-1 fw-bold text-{{ info.color }}">
|
||||
<i class="fas fa-layer-group me-2"></i>{{ info.title }}
|
||||
</h4>
|
||||
<span class="text-muted">{{ info.desc }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- V-New: 動態補貨係數調整 -->
|
||||
<div class="input-group input-group-sm" style="width: 200px;">
|
||||
<span class="input-group-text bg-white">補貨係數 x</span>
|
||||
<input type="number" id="restockFactor" class="form-control text-center" value="{{ current_factor }}" step="0.1" min="0">
|
||||
<button class="btn btn-outline-primary" onclick="applyFactor()">應用</button>
|
||||
</div>
|
||||
|
||||
<!-- V-Fix: 匯出連結需包含 factor 參數 -->
|
||||
<a href="/api/export/excel/abc?class={{ target_class }}&factor={{ current_factor }}&{{ query_string }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-file-excel me-2"></i>匯出此類別報表
|
||||
</a>
|
||||
<button onclick="window.close()" class="btn btn-outline-secondary ms-2">關閉</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table id="dataTable" class="table table-hover align-middle mb-0" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 60px;">排名</th>
|
||||
{% if cols.pid %}<th>商品ID</th>{% endif %}
|
||||
<th style="width: 30%;">商品名稱</th>
|
||||
{% if cols.brand %}<th>品牌</th>{% endif %}
|
||||
{% if cols.vendor %}<th>廠商名稱</th>{% endif %}
|
||||
{% if cols.cat %}<th>分類</th>{% endif %}
|
||||
{% if cols.cost or cols.profit %}<th>毛利率</th>{% endif %}
|
||||
{% if cols.qty %}<th>平均單價</th>{% endif %}
|
||||
{% if cols.qty %}<th class="text-end">銷售數量</th>{% endif %}
|
||||
{% if cols.qty %}<th class="text-end table-info">建議補貨 (x{{ current_factor }})</th>{% endif %}
|
||||
<th class="text-end">銷售金額</th>
|
||||
<th class="text-end">累積營收佔比</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td class="text-center fw-bold text-muted">{{ loop.index }}</td>
|
||||
{% if cols.pid %}
|
||||
<td class="small text-muted">{{ item[cols.pid] }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<div class="text-truncate-2" title="{{ item[cols.name] | e }}">
|
||||
{{ item[cols.name] | e }}
|
||||
</div>
|
||||
</td>
|
||||
{% if cols.brand %}
|
||||
<td class="small text-muted">{{ item[cols.brand] }}</td>
|
||||
{% endif %}
|
||||
{% if cols.vendor %}
|
||||
<td class="small text-muted">{{ item[cols.vendor] }}</td>
|
||||
{% endif %}
|
||||
{% if cols.cat %}
|
||||
<td><span class="badge bg-light text-dark border">{{ item[cols.cat] }}</span></td>
|
||||
{% endif %}
|
||||
{% if cols.cost or cols.profit %}
|
||||
<td>
|
||||
{% set margin = item['calculated_margin_rate'] %}
|
||||
<span class="{{ 'text-success' if margin >= 30 else ('text-danger' if margin < 10 else 'text-dark') }} fw-bold">
|
||||
{{ "{:.1f}%".format(margin) }}
|
||||
</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if cols.qty %}
|
||||
<td class="small">${{ "{:,.0f}".format(item['avg_unit_price']) }}</td>
|
||||
{% endif %}
|
||||
{% if cols.qty %}
|
||||
<td class="text-end">{{ "{:,.0f}".format(item[cols.qty]) }}</td>
|
||||
{% endif %}
|
||||
{% if cols.qty %}
|
||||
<td class="text-end table-info fw-bold">
|
||||
{% if item['suggested_restock'] > 0 %}
|
||||
{{ "{:,.0f}".format(item['suggested_restock']) }}
|
||||
{% else %}
|
||||
<span class="text-muted small">建議清倉</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-end fw-bold text-danger">
|
||||
${{ "{:,.0f}".format(item[cols.amount]) }}
|
||||
</td>
|
||||
<td class="text-end text-muted small">
|
||||
{{ "{:.2f}%".format(item['cumulative_pct']) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}
|
||||
<section class="abc-detail-page">
|
||||
{% if loading_state|default(false) %}
|
||||
<div class="abc-loading-card">
|
||||
<div class="abc-loading-spinner" aria-hidden="true"></div>
|
||||
<div>
|
||||
<h1 class="h4 mb-2">數據準備中</h1>
|
||||
<p class="text-muted mb-0">正在重新載入業績分析快取,稍後會自動重試。</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="/sales_analysis">回業績分析</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<header class="abc-detail-header">
|
||||
<div>
|
||||
<span class="abc-detail-kicker">
|
||||
<i class="fas fa-layer-group" aria-hidden="true"></i>
|
||||
ABC Analysis
|
||||
</span>
|
||||
<h1>{{ info.title }}</h1>
|
||||
<p>{{ info.desc }}</p>
|
||||
<div class="abc-detail-statline">
|
||||
<span class="abc-detail-pill">
|
||||
<i class="fas fa-coins" aria-hidden="true"></i>
|
||||
總營收 ${{ "{:,.0f}".format(total_revenue|default(0)) }}
|
||||
</span>
|
||||
<span class="abc-detail-pill">
|
||||
<i class="fas fa-cubes" aria-hidden="true"></i>
|
||||
{{ items|length }} 個商品
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script>
|
||||
// V-Fix: 提取排序索引變數,避免在物件實字中混用模板語法導致編輯器誤判
|
||||
const sortColIndex = parseInt("{{ sort_col_index }}");
|
||||
<div class="abc-detail-actions">
|
||||
<div class="abc-factor-control input-group input-group-sm">
|
||||
<span class="input-group-text">補貨 x</span>
|
||||
<input type="number" id="restockFactor" class="form-control text-center" value="{{ current_factor }}" step="0.1" min="0" aria-label="補貨係數">
|
||||
<button class="btn btn-outline-primary" type="button" onclick="applyFactor()">套用</button>
|
||||
</div>
|
||||
<a href="/api/export/excel/abc?class={{ target_class }}&factor={{ current_factor }}&{{ query_string }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-file-excel me-2" aria-hidden="true"></i>匯出報表
|
||||
</a>
|
||||
<a href="/sales_analysis" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left me-2" aria-hidden="true"></i>回分析頁
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#dataTable').DataTable({
|
||||
"language": {
|
||||
"url": "//cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json",
|
||||
"paginate": {
|
||||
"previous": "上一頁",
|
||||
"next": "下一頁"
|
||||
}
|
||||
},
|
||||
"pageLength": 25, // V-Adj: 預設每頁 25 筆,避免過長
|
||||
"lengthMenu": [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], // V-New: 允許使用者選擇每頁筆數
|
||||
"order": [[ sortColIndex, "desc" ]], // 預設按金額排序
|
||||
"paging": true, // 確保分頁開啟
|
||||
"info": true // 顯示 "第 x 至 x 筆,共 x 筆"
|
||||
});
|
||||
});
|
||||
<div class="abc-detail-card">
|
||||
<div class="abc-detail-card__head">
|
||||
<h2>商品明細</h2>
|
||||
<span class="abc-detail-pill">{{ target_class }} 類排序</span>
|
||||
</div>
|
||||
<div class="abc-detail-table">
|
||||
<div class="table-responsive">
|
||||
<table id="dataTable" class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">排名</th>
|
||||
{% if cols.pid %}<th>商品 ID</th>{% endif %}
|
||||
<th>商品名稱</th>
|
||||
{% if cols.brand %}<th>品牌</th>{% endif %}
|
||||
{% if cols.vendor %}<th>廠商名稱</th>{% endif %}
|
||||
{% if cols.cat %}<th>分類</th>{% endif %}
|
||||
{% if cols.cost or cols.profit %}<th>毛利率</th>{% endif %}
|
||||
{% if cols.qty %}<th>平均單價</th>{% endif %}
|
||||
{% if cols.qty %}<th class="text-end">銷售數量</th>{% endif %}
|
||||
{% if cols.qty %}<th class="text-end">建議補貨</th>{% endif %}
|
||||
<th class="text-end">銷售金額</th>
|
||||
<th class="text-end">累積營收佔比</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td class="text-center fw-bold text-muted">{{ loop.index }}</td>
|
||||
{% if cols.pid %}
|
||||
<td class="small text-muted">{{ item[cols.pid] }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<div class="abc-product-name" title="{{ item[cols.name] | e }}">
|
||||
{{ item[cols.name] | e }}
|
||||
</div>
|
||||
</td>
|
||||
{% if cols.brand %}
|
||||
<td class="small text-muted">{{ item[cols.brand] }}</td>
|
||||
{% endif %}
|
||||
{% if cols.vendor %}
|
||||
<td class="small text-muted">{{ item[cols.vendor] }}</td>
|
||||
{% endif %}
|
||||
{% if cols.cat %}
|
||||
<td><span class="badge bg-light text-dark border">{{ item[cols.cat] }}</span></td>
|
||||
{% endif %}
|
||||
{% if cols.cost or cols.profit %}
|
||||
<td>
|
||||
{% set margin = item['calculated_margin_rate'] %}
|
||||
<span class="{{ 'text-success' if margin >= 30 else ('text-danger' if margin < 10 else 'text-dark') }} fw-bold">
|
||||
{{ "{:.1f}%".format(margin) }}
|
||||
</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if cols.qty %}
|
||||
<td class="small">${{ "{:,.0f}".format(item['avg_unit_price']) }}</td>
|
||||
<td class="text-end">{{ "{:,.0f}".format(item[cols.qty]) }}</td>
|
||||
<td class="text-end fw-bold">
|
||||
{% if item['suggested_restock'] > 0 %}
|
||||
{{ "{:,.0f}".format(item['suggested_restock']) }}
|
||||
{% else %}
|
||||
<span class="text-muted small">建議清倉</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-end fw-bold text-danger">
|
||||
${{ "{:,.0f}".format(item[cols.amount]) }}
|
||||
</td>
|
||||
<td class="text-end text-muted small">
|
||||
{{ "{:.2f}%".format(item['cumulative_pct']) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
// V-New: 應用新的補貨係數
|
||||
function applyFactor() {
|
||||
const factor = document.getElementById('restockFactor').value;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('factor', factor);
|
||||
window.location.href = url.toString();
|
||||
{% block extra_js %}
|
||||
{% if loading_state|default(false) %}
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
|
||||
const retryCount = parseInt(sessionStorage.getItem('abc_retry') || '0', 10);
|
||||
if (retryCount > 3) {
|
||||
sessionStorage.removeItem('abc_retry');
|
||||
window.location.href = '/sales_analysis';
|
||||
} else {
|
||||
sessionStorage.setItem('abc_retry', String(retryCount + 1));
|
||||
}
|
||||
</script>
|
||||
{% else %}
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script>
|
||||
const sortColIndex = parseInt("{{ sort_col_index }}", 10);
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#dataTable').DataTable({
|
||||
language: {
|
||||
url: '//cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json',
|
||||
paginate: {
|
||||
previous: '上一頁',
|
||||
next: '下一頁'
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
},
|
||||
pageLength: 25,
|
||||
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, '全部']],
|
||||
order: [[sortColIndex, 'desc']],
|
||||
paging: true,
|
||||
info: true
|
||||
});
|
||||
});
|
||||
|
||||
function applyFactor() {
|
||||
const factor = document.getElementById('restockFactor').value;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('factor', factor);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,224 +3,325 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登入 - WOOO TECH</title>
|
||||
<title>登入 - EwoooC</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&family=Noto+Sans+TC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-tokens.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-tokens-v2-alias.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-dotmatrix.css') }}">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
color: var(--momo-text-primary);
|
||||
font-family: var(--momo-font-family);
|
||||
background:
|
||||
radial-gradient(circle at 14px 14px, rgba(172, 92, 58, 0.14) 1.2px, transparent 1.4px) 0 0 / 18px 18px,
|
||||
var(--momo-bg-page);
|
||||
}
|
||||
|
||||
.login-page {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 440px);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: clamp(2rem, 5vw, 4.5rem);
|
||||
border-right: 1px solid var(--momo-border-light);
|
||||
background:
|
||||
radial-gradient(circle at 18px 18px, rgba(42, 37, 32, 0.08) 1.1px, transparent 1.3px) 0 0 / 22px 22px,
|
||||
rgba(255, 248, 239, 0.55);
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 70px;
|
||||
gap: 12px;
|
||||
color: var(--momo-text-primary);
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-mark__icon {
|
||||
display: inline-grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border: 1px solid var(--momo-text-primary);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-text-primary);
|
||||
color: var(--momo-bg-paper);
|
||||
}
|
||||
|
||||
.login-brand h1 {
|
||||
max-width: 720px;
|
||||
margin: clamp(4rem, 10vw, 8rem) 0 var(--momo-space-3);
|
||||
color: var(--momo-text-primary);
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: clamp(2.2rem, 5vw, 4.6rem);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.login-brand p {
|
||||
max-width: 620px;
|
||||
margin: 0;
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.login-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: var(--momo-space-6);
|
||||
}
|
||||
|
||||
.login-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-bg-elevated);
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: var(--momo-text-label);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-panel-wrap {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding: clamp(1rem, 4vw, 3rem);
|
||||
background: var(--momo-bg-page);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-lg);
|
||||
background: var(--momo-bg-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
.login-card__head {
|
||||
padding: var(--momo-space-5);
|
||||
border-bottom: 1px solid var(--momo-border-light);
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
.login-card__head span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--momo-space-2);
|
||||
color: var(--momo-page-accent-dark, var(--momo-accent-caramel));
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: var(--momo-text-label);
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.login-card__head h2 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--momo-text-primary);
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: 1.55rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
padding: 2rem;
|
||||
padding-top: 70px;
|
||||
.login-card__body {
|
||||
padding: var(--momo-space-5);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--momo-text-primary);
|
||||
font-family: var(--momo-font-display);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 46px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-md);
|
||||
background: var(--momo-bg-elevated);
|
||||
color: var(--momo-text-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
color: white;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
border-color: var(--momo-page-accent-line, rgba(172, 92, 58, 0.35));
|
||||
box-shadow: 0 0 0 3px rgba(172, 92, 58, 0.12);
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 14px;
|
||||
color: var(--momo-text-tertiary);
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
border: 1px solid var(--momo-text-primary);
|
||||
border-radius: var(--momo-radius-md);
|
||||
background: var(--momo-text-primary);
|
||||
color: var(--momo-bg-paper);
|
||||
font-family: var(--momo-font-display);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.btn-login:hover,
|
||||
.btn-login:focus {
|
||||
border-color: var(--momo-page-accent-dark, #7a4a18);
|
||||
background: var(--momo-page-accent-dark, #7a4a18);
|
||||
color: var(--momo-bg-paper);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
border: 1px solid var(--momo-danger-border);
|
||||
border-radius: var(--momo-radius-md);
|
||||
background: var(--momo-danger-bg);
|
||||
color: var(--momo-danger-text);
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
padding: 1rem 2rem 2rem;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
padding: 0 var(--momo-space-5) var(--momo-space-5);
|
||||
color: var(--momo-text-tertiary);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: var(--momo-text-label);
|
||||
}
|
||||
|
||||
.security-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: #f0f0f0;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
margin-top: 1rem;
|
||||
@media (max-width: 900px) {
|
||||
.login-page {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
min-height: auto;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--momo-border-light);
|
||||
}
|
||||
|
||||
.login-brand h1 {
|
||||
margin-top: var(--momo-space-6);
|
||||
}
|
||||
|
||||
.login-panel-wrap {
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dark.bg-primary {
|
||||
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.login-brand,
|
||||
.login-panel-wrap {
|
||||
padding: var(--momo-space-4);
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-brand {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link {
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link:hover {
|
||||
color: #ffffff !important;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link.active {
|
||||
color: #ffffff !important;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-text {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
.login-card__head,
|
||||
.login-card__body {
|
||||
padding: var(--momo-space-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<i class="fas fa-shield-alt fa-2x mb-2"></i>
|
||||
<h1>WOOO TECH</h1>
|
||||
<p>請輸入密碼以繼續</p>
|
||||
</div>
|
||||
|
||||
<div class="login-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock me-1"></i> 密碼
|
||||
</label>
|
||||
<div class="password-wrapper">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="請輸入密碼"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
>
|
||||
<i class="fas fa-eye password-toggle" onclick="togglePassword()"></i>
|
||||
</div>
|
||||
<main class="login-page">
|
||||
<section class="login-brand" aria-label="EwoooC 登入品牌區">
|
||||
<div>
|
||||
<a class="brand-mark" href="/login">
|
||||
<span class="brand-mark__icon"><i class="fas fa-grip"></i></span>
|
||||
<span>EwoooC</span>
|
||||
</a>
|
||||
<h1>營運數據工作台</h1>
|
||||
<p>商品、活動、廠商缺貨與 AI 觀測台都在同一個控制面。登入後會回到正式資料頁面。</p>
|
||||
<div class="login-meta">
|
||||
<span class="login-pill"><i class="fas fa-shield-halved"></i>CSRF 防護</span>
|
||||
<span class="login-pill"><i class="fas fa-clock"></i>Session 2h</span>
|
||||
<span class="login-pill"><i class="fas fa-database"></i>PostgreSQL</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login">
|
||||
<i class="fas fa-sign-in-alt me-2"></i> 登入
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<div class="security-badge">
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
<span>安全連線 • CSRF 防護已啟用</span>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<small>© 2024 WOOO TECH V9.4</small>
|
||||
<div class="login-footer d-none d-md-block">
|
||||
EwoooC V{{ config.SYSTEM_VERSION if config is defined else '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="login-panel-wrap" aria-label="登入表單">
|
||||
<div class="login-card">
|
||||
<div class="login-card__head">
|
||||
<span><i class="fas fa-lock"></i> Secure Access</span>
|
||||
<h2>登入系統</h2>
|
||||
</div>
|
||||
<div class="login-card__body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-circle-exclamation me-2"></i>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">密碼</label>
|
||||
<div class="password-wrapper">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="請輸入密碼"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
>
|
||||
<i class="fas fa-eye password-toggle" onclick="togglePassword()" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login">
|
||||
<i class="fas fa-right-to-bracket me-2"></i>登入
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="login-footer d-md-none">
|
||||
EwoooC V{{ config.SYSTEM_VERSION if config is defined else '' }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function togglePassword() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const toggleIcon = document.querySelector('.password-toggle');
|
||||
if (!passwordInput || !toggleIcon) return;
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
@@ -233,9 +334,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-focus on password field
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('password').focus();
|
||||
document.getElementById('password')?.focus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
36
templates/vendor_stockout_history_v2.html
Normal file
36
templates/vendor_stockout_history_v2.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "ewoooc_base.html" %}
|
||||
{% block title %}發送歷史 - EwoooC{% endblock %}
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-vendor-tools.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="vendor-tools-page" data-vendor-tool="history">
|
||||
<header class="vendor-tools-header">
|
||||
<div>
|
||||
<span class="vendor-tools-kicker"><i class="fas fa-boxes-stacked"></i> Vendor Stockout</span>
|
||||
<h1>發送歷史</h1>
|
||||
<p>保留廠商缺貨通知的操作歷程入口;目前進階查詢、統計圖表與重試機制仍在後續階段。</p>
|
||||
</div>
|
||||
<div class="vendor-tools-nav" aria-label="廠商缺貨工具導覽">
|
||||
<a href="/vendor-stockout/" ><i class="fas fa-table-columns"></i>總覽</a>
|
||||
<a href="/vendor-stockout/list" ><i class="fas fa-list-check"></i>缺貨清單</a>
|
||||
<a href="/vendor-stockout/import" ><i class="fas fa-file-arrow-up"></i>匯入</a>
|
||||
<a href="/vendor-stockout/vendor-management" ><i class="fas fa-building"></i>廠商</a>
|
||||
<a href="/vendor-stockout/send-email" ><i class="fas fa-paper-plane"></i>郵件記錄</a>
|
||||
<a href="/vendor-stockout/history" class="is-active" aria-current="page"><i class="fas fa-clock-rotate-left"></i>歷史</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="vendor-tools-body">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="fas fa-circle-info me-2"></i>
|
||||
此頁面功能將在 Phase 6 實作:記錄查詢、統計圖表與重試機制。現階段請先使用「郵件記錄」查看正式發送狀態。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
328
templates/vendor_stockout_send_email_v2.html
Normal file
328
templates/vendor_stockout_send_email_v2.html
Normal file
@@ -0,0 +1,328 @@
|
||||
{% extends "ewoooc_base.html" %}
|
||||
{% block title %}郵件發送記錄 - EwoooC{% endblock %}
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-vendor-tools.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="vendor-tools-page" data-vendor-tool="email">
|
||||
<header class="vendor-tools-header">
|
||||
<div>
|
||||
<span class="vendor-tools-kicker"><i class="fas fa-boxes-stacked"></i> Vendor Stockout</span>
|
||||
<h1>郵件發送記錄</h1>
|
||||
<p>追蹤廠商缺貨通知的發送狀態、成功率與錯誤訊息,方便營運快速補救。</p>
|
||||
</div>
|
||||
<div class="vendor-tools-nav" aria-label="廠商缺貨工具導覽">
|
||||
<a href="/vendor-stockout/" ><i class="fas fa-table-columns"></i>總覽</a>
|
||||
<a href="/vendor-stockout/list" ><i class="fas fa-list-check"></i>缺貨清單</a>
|
||||
<a href="/vendor-stockout/import" ><i class="fas fa-file-arrow-up"></i>匯入</a>
|
||||
<a href="/vendor-stockout/vendor-management" ><i class="fas fa-building"></i>廠商</a>
|
||||
<a href="/vendor-stockout/send-email" class="is-active" aria-current="page"><i class="fas fa-paper-plane"></i>郵件記錄</a>
|
||||
<a href="/vendor-stockout/history" ><i class="fas fa-clock-rotate-left"></i>歷史</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="vendor-tools-body">
|
||||
<div class="container-fluid" style="padding: 2rem;">
|
||||
<!-- 標題 -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">
|
||||
<i class="fas fa-paper-plane text-primary me-3"></i>郵件發送記錄
|
||||
</h2>
|
||||
<a href="/vendor-stockout" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-home me-2"></i>返回主頁
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 統計卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="text-muted small">總發送數</div>
|
||||
<h3 class="mb-0" id="statTotal">0</h3>
|
||||
</div>
|
||||
<div class="fs-1 text-primary">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="text-muted small">成功</div>
|
||||
<h3 class="mb-0 text-success" id="statSuccess">0</h3>
|
||||
</div>
|
||||
<div class="fs-1 text-success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="text-muted small">失敗</div>
|
||||
<h3 class="mb-0 text-danger" id="statFailed">0</h3>
|
||||
</div>
|
||||
<div class="fs-1 text-danger">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="text-muted small">成功率</div>
|
||||
<h3 class="mb-0 text-info" id="statRate">0%</h3>
|
||||
</div>
|
||||
<div class="fs-1 text-info">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 篩選區 -->
|
||||
<div class="card mb-3" style="border-radius: 12px;">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">狀態</label>
|
||||
<select class="form-select" id="filterStatus">
|
||||
<option value="">全部</option>
|
||||
<option value="sent">成功</option>
|
||||
<option value="failed">失敗</option>
|
||||
<option value="pending">待發送</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">廠商代碼</label>
|
||||
<input type="text" class="form-control" id="filterVendor" placeholder="輸入廠商代碼">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">開始日期</label>
|
||||
<input type="date" class="form-control" id="filterDateFrom">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">結束日期</label>
|
||||
<input type="date" class="form-control" id="filterDateTo">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-primary w-100" onclick="loadData()">
|
||||
<i class="fas fa-search me-2"></i>查詢
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 資料表格 -->
|
||||
<div class="table-container">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>批次編號</th>
|
||||
<th>廠商</th>
|
||||
<th>收件者</th>
|
||||
<th>主旨</th>
|
||||
<th>商品數</th>
|
||||
<th>狀態</th>
|
||||
<th>發送時間</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>載入中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分頁 -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div class="text-muted">
|
||||
共 <span id="totalCount">0</span> 筆記錄
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination mb-0" id="pagination"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
// 載入統計資料
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch('/vendor-stockout/api/email/stats');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
document.getElementById('statTotal').textContent = data.total_sent;
|
||||
document.getElementById('statSuccess').textContent = data.success_count;
|
||||
document.getElementById('statFailed').textContent = data.failed_count;
|
||||
document.getElementById('statRate').textContent = data.success_rate + '%';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入統計資料失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 載入郵件記錄
|
||||
async function loadData(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
const status = document.getElementById('filterStatus').value;
|
||||
const vendor = document.getElementById('filterVendor').value;
|
||||
const dateFrom = document.getElementById('filterDateFrom').value;
|
||||
const dateTo = document.getElementById('filterDateTo').value;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
});
|
||||
|
||||
if (status) params.append('status', status);
|
||||
if (vendor) params.append('vendor_code', vendor);
|
||||
if (dateFrom) params.append('date_from', dateFrom);
|
||||
if (dateTo) params.append('date_to', dateTo);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/vendor-stockout/api/email/logs?${params}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderTable(result.data);
|
||||
renderPagination(result.total, page);
|
||||
document.getElementById('totalCount').textContent = result.total;
|
||||
} else {
|
||||
alert('查詢失敗: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入資料失敗:', error);
|
||||
alert('載入資料失敗: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染表格
|
||||
function renderTable(data) {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted">
|
||||
<i class="fas fa-inbox me-2"></i>沒有記錄
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.map(item => {
|
||||
const statusClass = item.status === 'sent' ? 'status-sent' :
|
||||
item.status === 'failed' ? 'status-failed' : 'status-pending';
|
||||
const statusText = item.status === 'sent' ? '成功' :
|
||||
item.status === 'failed' ? '失敗' : '待發送';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${item.id}</td>
|
||||
<td><small>${item.batch_id}</small></td>
|
||||
<td>
|
||||
<div><strong>${item.vendor_name}</strong></div>
|
||||
<small class="text-muted">${item.vendor_code}</small>
|
||||
</td>
|
||||
<td><small>${item.recipient_email}</small></td>
|
||||
<td style="max-width: 300px;">
|
||||
<small>${item.subject}</small>
|
||||
${item.error_message ? `<div class="text-danger small mt-1">❌ ${item.error_message}</div>` : ''}
|
||||
</td>
|
||||
<td>${item.product_count}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td><small>${item.sent_at || item.created_at}</small></td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染分頁
|
||||
function renderPagination(total, current) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
pagination.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// 上一頁
|
||||
html += `
|
||||
<li class="page-item ${current === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadData(${current - 1}); return false;">上一頁</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 頁碼
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === 1 || i === totalPages || (i >= current - 2 && i <= current + 2)) {
|
||||
html += `
|
||||
<li class="page-item ${i === current ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadData(${i}); return false;">${i}</a>
|
||||
</li>
|
||||
`;
|
||||
} else if (i === current - 3 || i === current + 3) {
|
||||
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
}
|
||||
|
||||
// 下一頁
|
||||
html += `
|
||||
<li class="page-item ${current === totalPages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadData(${current + 1}); return false;">下一頁</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
pagination.innerHTML = html;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadStats();
|
||||
loadData(1);
|
||||
|
||||
// 每 30 秒自動重新整理統計資料
|
||||
setInterval(loadStats, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
838
templates/vendor_stockout_vendor_management_v2.html
Normal file
838
templates/vendor_stockout_vendor_management_v2.html
Normal file
@@ -0,0 +1,838 @@
|
||||
{% extends "ewoooc_base.html" %}
|
||||
{% block title %}廠商管理 - EwoooC{% endblock %}
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-vendor-tools.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="vendor-tools-page" data-vendor-tool="management">
|
||||
<header class="vendor-tools-header">
|
||||
<div>
|
||||
<span class="vendor-tools-kicker"><i class="fas fa-boxes-stacked"></i> Vendor Stockout</span>
|
||||
<h1>廠商管理</h1>
|
||||
<p>維護來源供應商、收件信箱與 Excel 匯入,所有異動直接接正式廠商缺貨資料。</p>
|
||||
</div>
|
||||
<div class="vendor-tools-nav" aria-label="廠商缺貨工具導覽">
|
||||
<a href="/vendor-stockout/" ><i class="fas fa-table-columns"></i>總覽</a>
|
||||
<a href="/vendor-stockout/list" ><i class="fas fa-list-check"></i>缺貨清單</a>
|
||||
<a href="/vendor-stockout/import" ><i class="fas fa-file-arrow-up"></i>匯入</a>
|
||||
<a href="/vendor-stockout/vendor-management" class="is-active" aria-current="page"><i class="fas fa-building"></i>廠商</a>
|
||||
<a href="/vendor-stockout/send-email" ><i class="fas fa-paper-plane"></i>郵件記錄</a>
|
||||
<a href="/vendor-stockout/history" ><i class="fas fa-clock-rotate-left"></i>歷史</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="vendor-tools-body">
|
||||
<div class="container-fluid" style="padding: 2rem;">
|
||||
<h2 class="mb-4">
|
||||
<i class="fas fa-building me-2" style="color: var(--momo-page-accent-dark);"></i>廠商管理
|
||||
</h2>
|
||||
|
||||
<!-- 分頁標籤 -->
|
||||
<div class="section-tabs">
|
||||
<ul class="nav nav-tabs" id="vendorTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="list-tab" data-bs-toggle="tab" data-bs-target="#list" type="button">
|
||||
<i class="fas fa-list me-2"></i>廠商清單
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="import-tab" data-bs-toggle="tab" data-bs-target="#import" type="button">
|
||||
<i class="fas fa-file-upload me-2"></i>Excel 匯入
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="vendorTabContent">
|
||||
<!-- 廠商清單 Tab -->
|
||||
<div class="tab-pane fade show active" id="list" role="tabpanel">
|
||||
<!-- 統計卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card">
|
||||
<div class="stats-number text-primary" style="font-size: 2rem; font-weight: bold;" id="totalVendors">0</div>
|
||||
<div class="stats-label text-muted">總廠商數</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card">
|
||||
<div class="stats-number" style="font-size: 2rem; font-weight: bold; color: var(--momo-page-accent-dark);" id="totalEmails">0</div>
|
||||
<div class="stats-label text-muted">郵件地址總數</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card">
|
||||
<div class="stats-number text-success" style="font-size: 2rem; font-weight: bold;" id="avgEmails">0</div>
|
||||
<div class="stats-label text-muted">平均每廠商郵件數</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 篩選與操作 -->
|
||||
<div class="card mb-3" style="border-radius: 12px;">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">搜尋廠商</label>
|
||||
<input type="text" class="form-control" id="searchVendor" placeholder="輸入廠商代碼或名稱">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-success" id="searchBtn">
|
||||
<i class="fas fa-search me-2"></i>搜尋
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="resetBtn">
|
||||
<i class="fas fa-redo me-2"></i>重置
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-5 text-end">
|
||||
<button class="btn btn-primary" id="addVendorBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||||
<i class="fas fa-plus me-2"></i>新增廠商
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 廠商列表 -->
|
||||
<div class="table-container">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 150px;">廠商代碼</th>
|
||||
<th>廠商名稱</th>
|
||||
<th>郵件地址</th>
|
||||
<th style="width: 120px;">郵件數量</th>
|
||||
<th style="width: 180px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vendorTable">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted mb-3"></i>
|
||||
<p class="text-muted">載入中...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分頁 -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<div class="text-muted">
|
||||
顯示 <span id="pageInfo">0-0 / 0</span> 筆
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination mb-0" id="pagination"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Excel 匯入 Tab -->
|
||||
<div class="tab-pane fade" id="import" role="tabpanel">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<!-- 上傳區域 -->
|
||||
<div class="card mb-4" style="border-radius: 16px;">
|
||||
<div class="card-body">
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<input type="file" id="fileInput" accept=".xlsx,.xls" style="display: none;">
|
||||
<div class="upload-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">拖曳檔案到此處或點擊選擇檔案</h4>
|
||||
<p class="text-muted">支援格式: Excel (.xlsx, .xls)</p>
|
||||
<p class="text-muted small">
|
||||
<strong>必要欄位:</strong>來源供應商編號、來源供應商名稱<br>
|
||||
<strong>選填欄位:</strong>Mail(支援多個郵件用逗號或分號分隔)
|
||||
</p>
|
||||
<button type="button" class="btn btn-primary btn-lg mt-3" id="selectFileBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||||
<i class="fas fa-folder-open me-2"></i>選擇檔案
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="/vendor-stockout/api/vendor/import/template" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-download me-2"></i>下載 Excel 範本
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 檔案資訊 -->
|
||||
<div id="fileInfo" class="mt-4" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-file me-2"></i>
|
||||
<strong>已選擇檔案:</strong> <span id="fileName"></span>
|
||||
<span class="badge bg-primary ms-2" id="fileSize"></span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success w-100 btn-lg" id="uploadBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||||
<i class="fas fa-upload me-2"></i>開始匯入
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 進度條 -->
|
||||
<div id="progressArea" class="mt-4" style="display: none;">
|
||||
<div class="progress" style="height: 30px; border-radius: 15px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 100%; background: var(--momo-page-accent-dark);">
|
||||
匯入中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 匯入結果 -->
|
||||
<div id="resultArea" style="display: none;">
|
||||
<div class="card" style="border-radius: 16px; border-left: 5px solid var(--momo-page-accent-dark);">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-check-circle text-success me-2"></i>匯入完成
|
||||
</h5>
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3 class="text-primary" id="totalCount">0</h3>
|
||||
<small class="text-muted">總筆數</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3 class="text-success" id="successCount">0</h3>
|
||||
<small class="text-muted">新增廠商</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3 class="text-info" id="updatedCount">0</h3>
|
||||
<small class="text-muted">更新廠商</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3 style="color: var(--momo-page-accent-dark);" id="emailCount">0</h3>
|
||||
<small class="text-muted">郵件地址</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<div class="text-center">
|
||||
<h3 class="text-danger" id="failedCount">0</h3>
|
||||
<small class="text-muted">失敗</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<button type="button" class="btn btn-outline-secondary" id="importAgainBtn">
|
||||
<i class="fas fa-redo me-2"></i>再次匯入
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);" onclick="refreshVendorList()">
|
||||
<i class="fas fa-list me-2"></i>查看廠商清單
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 錯誤訊息 -->
|
||||
<div id="errorArea" style="display: none;">
|
||||
<div class="alert alert-danger">
|
||||
<h5><i class="fas fa-exclamation-circle me-2"></i>匯入失敗</h5>
|
||||
<p class="mb-0" id="errorMessage"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/編輯廠商 Modal -->
|
||||
<div class="modal fade" id="vendorModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background: var(--momo-page-accent-dark); color: white;">
|
||||
<h5 class="modal-title" id="vendorModalTitle">
|
||||
<i class="fas fa-building me-2"></i>新增廠商
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editVendorId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">廠商代碼 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="vendorCode" placeholder="輸入廠商代碼">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">廠商名稱 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="vendorName" placeholder="輸入廠商名稱">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">郵件地址 <small class="text-muted">(每行一個郵件地址)</small></label>
|
||||
<textarea class="form-control" id="vendorEmails" rows="5" placeholder="輸入郵件地址,每行一個"></textarea>
|
||||
<small class="text-muted">提示:可以從 Excel 複製貼上多個郵件地址</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="saveVendorBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||||
<i class="fas fa-save me-2"></i>儲存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理郵件 Modal -->
|
||||
<div class="modal fade" id="emailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background: var(--momo-page-accent-dark); color: white;">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-envelope me-2"></i>管理郵件地址
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="emailVendorCode">
|
||||
<h6 class="mb-3">
|
||||
<strong id="emailVendorName"></strong> (<span id="emailVendorCode2"></span>)
|
||||
</h6>
|
||||
|
||||
<!-- 新增郵件 -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h6><i class="fas fa-plus-circle me-2"></i>新增郵件地址</h6>
|
||||
<div class="input-group">
|
||||
<input type="email" class="form-control" id="newEmail" placeholder="輸入新的郵件地址">
|
||||
<button class="btn btn-success" id="addEmailBtn">
|
||||
<i class="fas fa-plus me-1"></i>新增
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 現有郵件列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6><i class="fas fa-list me-2"></i>現有郵件地址</h6>
|
||||
<div id="emailList" class="mt-3">
|
||||
<p class="text-center text-muted">載入中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">關閉</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// ====== Global Variables ======
|
||||
const vendorCsrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
let currentPage = 1;
|
||||
let pageSize = 20;
|
||||
let totalVendors = 0;
|
||||
let searchKeyword = '';
|
||||
let selectedFile = null;
|
||||
let vendorModal, emailModal;
|
||||
|
||||
// ====== Initialize ======
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
vendorModal = new bootstrap.Modal(document.getElementById('vendorModal'));
|
||||
emailModal = new bootstrap.Modal(document.getElementById('emailModal'));
|
||||
loadVendorList();
|
||||
bindEvents();
|
||||
});
|
||||
|
||||
// ====== Bind Events ======
|
||||
function bindEvents() {
|
||||
// 廠商清單相關
|
||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||
searchKeyword = document.getElementById('searchVendor').value;
|
||||
currentPage = 1;
|
||||
loadVendorList();
|
||||
});
|
||||
|
||||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||
document.getElementById('searchVendor').value = '';
|
||||
searchKeyword = '';
|
||||
currentPage = 1;
|
||||
loadVendorList();
|
||||
});
|
||||
|
||||
document.getElementById('addVendorBtn').addEventListener('click', () => {
|
||||
openVendorModal();
|
||||
});
|
||||
|
||||
document.getElementById('saveVendorBtn').addEventListener('click', saveVendor);
|
||||
|
||||
// 郵件管理相關
|
||||
document.getElementById('addEmailBtn').addEventListener('click', addEmail);
|
||||
|
||||
// Excel 匯入相關
|
||||
bindImportEvents();
|
||||
}
|
||||
|
||||
// ====== Vendor List Functions ======
|
||||
async function loadVendorList() {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage,
|
||||
page_size: pageSize
|
||||
});
|
||||
|
||||
if (searchKeyword) {
|
||||
params.append('search', searchKeyword);
|
||||
}
|
||||
|
||||
const response = await fetch(`/vendor-stockout/api/vendor/list?${params}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderVendorTable(result.data.vendors);
|
||||
updateVendorStats(result.data.stats);
|
||||
updatePagination(result.data.total);
|
||||
totalVendors = result.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入廠商清單失敗:', error);
|
||||
document.getElementById('vendorTable').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-danger">
|
||||
<i class="fas fa-exclamation-circle fa-2x mb-3"></i>
|
||||
<p>載入失敗: ${error.message}</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderVendorTable(vendors) {
|
||||
const tbody = document.getElementById('vendorTable');
|
||||
|
||||
if (vendors.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">暫無廠商資料</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = vendors.map(vendor => `
|
||||
<tr>
|
||||
<td><strong>${vendor.vendor_code}</strong></td>
|
||||
<td>${vendor.vendor_name}</td>
|
||||
<td>
|
||||
${vendor.emails.slice(0, 2).map(email =>
|
||||
`<span class="email-badge">${email}</span>`
|
||||
).join('')}
|
||||
${vendor.email_count > 2 ? `<span class="badge bg-secondary">+${vendor.email_count - 2} 個</span>` : ''}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-info">${vendor.email_count} 個</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary me-1" onclick="editVendor('${vendor.vendor_code}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info me-1" onclick="manageEmails('${vendor.vendor_code}', '${vendor.vendor_name}')">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteVendor('${vendor.vendor_code}', '${vendor.vendor_name}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateVendorStats(stats) {
|
||||
document.getElementById('totalVendors').textContent = stats.total_vendors || 0;
|
||||
document.getElementById('totalEmails').textContent = stats.total_emails || 0;
|
||||
document.getElementById('avgEmails').textContent = stats.avg_emails || '0.0';
|
||||
}
|
||||
|
||||
function updatePagination(total) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const start = (currentPage - 1) * pageSize + 1;
|
||||
const end = Math.min(currentPage * pageSize, total);
|
||||
|
||||
document.getElementById('pageInfo').textContent = `${start}-${end} / ${total}`;
|
||||
|
||||
const pagination = document.getElementById('pagination');
|
||||
let html = '';
|
||||
|
||||
html += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="changePage(${currentPage - 1}); return false;">上一頁</a>
|
||||
</li>`;
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
|
||||
html += `<li class="page-item ${i === currentPage ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="changePage(${i}); return false;">${i}</a>
|
||||
</li>`;
|
||||
} else if (i === currentPage - 3 || i === currentPage + 3) {
|
||||
html += `<li class="page-item disabled"><a class="page-link">...</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="changePage(${currentPage + 1}); return false;">下一頁</a>
|
||||
</li>`;
|
||||
|
||||
pagination.innerHTML = html;
|
||||
}
|
||||
|
||||
function changePage(page) {
|
||||
const totalPages = Math.ceil(totalVendors / pageSize);
|
||||
if (page < 1 || page > totalPages) return;
|
||||
currentPage = page;
|
||||
loadVendorList();
|
||||
}
|
||||
|
||||
// ====== Vendor CRUD Functions ======
|
||||
function openVendorModal(vendorCode = null) {
|
||||
document.getElementById('editVendorId').value = vendorCode || '';
|
||||
document.getElementById('vendorModalTitle').innerHTML = vendorCode
|
||||
? '<i class="fas fa-edit me-2"></i>編輯廠商'
|
||||
: '<i class="fas fa-plus me-2"></i>新增廠商';
|
||||
|
||||
if (vendorCode) {
|
||||
loadVendorDetail(vendorCode);
|
||||
} else {
|
||||
document.getElementById('vendorCode').value = '';
|
||||
document.getElementById('vendorCode').readOnly = false;
|
||||
document.getElementById('vendorName').value = '';
|
||||
document.getElementById('vendorEmails').value = '';
|
||||
}
|
||||
|
||||
vendorModal.show();
|
||||
}
|
||||
|
||||
async function loadVendorDetail(vendorCode) {
|
||||
try {
|
||||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const vendor = result.data;
|
||||
document.getElementById('vendorCode').value = vendor.vendor_code;
|
||||
document.getElementById('vendorCode').readOnly = true;
|
||||
document.getElementById('vendorName').value = vendor.vendor_name;
|
||||
document.getElementById('vendorEmails').value = vendor.emails.join('\n');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('載入廠商資料失敗: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function editVendor(vendorCode) {
|
||||
openVendorModal(vendorCode);
|
||||
}
|
||||
|
||||
async function saveVendor() {
|
||||
const vendorCode = document.getElementById('vendorCode').value.trim();
|
||||
const vendorName = document.getElementById('vendorName').value.trim();
|
||||
const emailsText = document.getElementById('vendorEmails').value;
|
||||
|
||||
if (!vendorCode || !vendorName) {
|
||||
alert('廠商代碼和名稱為必填欄位!');
|
||||
return;
|
||||
}
|
||||
|
||||
const emails = emailsText.split('\n')
|
||||
.map(e => e.trim())
|
||||
.filter(e => e && e.includes('@'));
|
||||
|
||||
const data = {
|
||||
vendor_code: vendorCode,
|
||||
vendor_name: vendorName,
|
||||
emails: emails
|
||||
};
|
||||
|
||||
try {
|
||||
const isEdit = document.getElementById('editVendorId').value;
|
||||
const url = isEdit
|
||||
? `/vendor-stockout/api/vendor/${vendorCode}`
|
||||
: '/vendor-stockout/api/vendor';
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': vendorCsrfToken
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
vendorModal.hide();
|
||||
await loadVendorList();
|
||||
alert(isEdit ? '更新成功!' : '新增成功!');
|
||||
} else {
|
||||
alert(result.message || '操作失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('操作失敗: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVendor(vendorCode, vendorName) {
|
||||
if (!confirm(`確定要刪除廠商「${vendorName}」嗎?這將同時刪除所有相關郵件地址。`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': vendorCsrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
await loadVendorList();
|
||||
alert('刪除成功!');
|
||||
} else {
|
||||
alert(result.message || '刪除失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('刪除失敗: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Email Management Functions ======
|
||||
async function manageEmails(vendorCode, vendorName) {
|
||||
document.getElementById('emailVendorCode').value = vendorCode;
|
||||
document.getElementById('emailVendorCode2').textContent = vendorCode;
|
||||
document.getElementById('emailVendorName').textContent = vendorName;
|
||||
document.getElementById('newEmail').value = '';
|
||||
|
||||
await loadEmailList(vendorCode);
|
||||
emailModal.show();
|
||||
}
|
||||
|
||||
async function loadEmailList(vendorCode) {
|
||||
try {
|
||||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}/emails`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const emailList = document.getElementById('emailList');
|
||||
if (result.data.length === 0) {
|
||||
emailList.innerHTML = '<p class="text-muted text-center">暫無郵件地址</p>';
|
||||
} else {
|
||||
emailList.innerHTML = result.data.map(email => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 p-2 border rounded">
|
||||
<div>
|
||||
<i class="fas fa-envelope me-2" style="color: var(--momo-page-accent-dark);"></i>
|
||||
<strong>${email.email}</strong>
|
||||
<span class="badge bg-secondary ms-2">${email.email_type}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteEmail(${email.id})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('emailList').innerHTML =
|
||||
'<p class="text-danger text-center">載入失敗: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function addEmail() {
|
||||
const vendorCode = document.getElementById('emailVendorCode').value;
|
||||
const email = document.getElementById('newEmail').value.trim();
|
||||
|
||||
if (!email) {
|
||||
alert('請輸入郵件地址!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
alert('請輸入有效的郵件地址!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}/emails`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': vendorCsrfToken
|
||||
},
|
||||
body: JSON.stringify({ email: email, email_type: 'primary' })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('newEmail').value = '';
|
||||
await loadEmailList(vendorCode);
|
||||
await loadVendorList();
|
||||
alert('新增成功!');
|
||||
} else {
|
||||
alert(result.message || '新增失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('新增失敗: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEmail(emailId) {
|
||||
if (!confirm('確定要刪除這個郵件地址嗎?')) return;
|
||||
|
||||
const vendorCode = document.getElementById('emailVendorCode').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/vendor-stockout/api/vendor/email/${emailId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': vendorCsrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
await loadEmailList(vendorCode);
|
||||
await loadVendorList();
|
||||
alert('刪除成功!');
|
||||
} else {
|
||||
alert(result.message || '刪除失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('刪除失敗: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Excel Import Functions ======
|
||||
function bindImportEvents() {
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const selectFileBtn = document.getElementById('selectFileBtn');
|
||||
|
||||
uploadArea.addEventListener('click', () => fileInput.click());
|
||||
selectFileBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
handleFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
document.getElementById('uploadBtn').addEventListener('click', uploadExcel);
|
||||
document.getElementById('importAgainBtn').addEventListener('click', resetImport);
|
||||
}
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file) return;
|
||||
|
||||
if (!file.name.match(/\.(xlsx|xls)$/i)) {
|
||||
alert('請選擇 Excel 檔案 (.xlsx 或 .xls)');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile = file;
|
||||
document.getElementById('fileName').textContent = file.name;
|
||||
document.getElementById('fileSize').textContent = formatFileSize(file.size);
|
||||
document.getElementById('fileInfo').style.display = 'block';
|
||||
document.getElementById('resultArea').style.display = 'none';
|
||||
document.getElementById('errorArea').style.display = 'none';
|
||||
}
|
||||
|
||||
async function uploadExcel() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
|
||||
document.getElementById('fileInfo').style.display = 'none';
|
||||
document.getElementById('progressArea').style.display = 'block';
|
||||
document.getElementById('errorArea').style.display = 'none';
|
||||
document.getElementById('resultArea').style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/vendor-stockout/api/vendor/import/excel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': vendorCsrfToken
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
document.getElementById('progressArea').style.display = 'none';
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('totalCount').textContent = result.data.total_count;
|
||||
document.getElementById('successCount').textContent = result.data.success_count;
|
||||
document.getElementById('updatedCount').textContent = result.data.updated_count;
|
||||
document.getElementById('emailCount').textContent = result.data.email_count;
|
||||
document.getElementById('failedCount').textContent = result.data.failed_count;
|
||||
document.getElementById('resultArea').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('errorMessage').textContent = result.message;
|
||||
document.getElementById('errorArea').style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('progressArea').style.display = 'none';
|
||||
document.getElementById('errorMessage').textContent = '網路錯誤: ' + error.message;
|
||||
document.getElementById('errorArea').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function resetImport() {
|
||||
selectedFile = null;
|
||||
document.getElementById('fileInput').value = '';
|
||||
document.getElementById('fileInfo').style.display = 'none';
|
||||
document.getElementById('resultArea').style.display = 'none';
|
||||
document.getElementById('errorArea').style.display = 'none';
|
||||
}
|
||||
|
||||
function refreshVendorList() {
|
||||
const listTab = new bootstrap.Tab(document.getElementById('list-tab'));
|
||||
listTab.show();
|
||||
loadVendorList();
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
189
web/static/css/page-abc-analysis-detail.css
Normal file
189
web/static/css/page-abc-analysis-detail.css
Normal file
@@ -0,0 +1,189 @@
|
||||
.abc-detail-page {
|
||||
display: grid;
|
||||
gap: var(--momo-space-5);
|
||||
color: var(--momo-text-primary);
|
||||
}
|
||||
|
||||
.abc-detail-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--momo-space-4);
|
||||
align-items: end;
|
||||
padding: var(--momo-space-5);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-lg);
|
||||
background:
|
||||
radial-gradient(circle at 14px 14px, rgba(172, 92, 58, 0.15) 1.2px, transparent 1.4px) 0 0 / 18px 18px,
|
||||
var(--momo-bg-surface);
|
||||
}
|
||||
|
||||
.abc-detail-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--momo-space-2);
|
||||
color: var(--momo-page-accent-dark);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: var(--momo-text-label);
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.abc-detail-header h1 {
|
||||
margin: 0;
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: clamp(1.55rem, 2.1vw, 2.15rem);
|
||||
font-weight: 820;
|
||||
letter-spacing: 0;
|
||||
color: var(--momo-text-primary);
|
||||
}
|
||||
|
||||
.abc-detail-header p {
|
||||
max-width: 760px;
|
||||
margin: var(--momo-space-2) 0 0;
|
||||
color: var(--momo-text-secondary);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.abc-detail-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.abc-factor-control {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: min(100%, 260px);
|
||||
}
|
||||
|
||||
.abc-factor-control .input-group-text {
|
||||
border-color: var(--momo-border-light);
|
||||
background: var(--momo-bg-paper);
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.abc-detail-statline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: var(--momo-space-3);
|
||||
}
|
||||
|
||||
.abc-detail-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-bg-elevated);
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: var(--momo-text-label);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.abc-detail-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-lg);
|
||||
background: var(--momo-bg-surface);
|
||||
}
|
||||
|
||||
.abc-detail-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--momo-space-3);
|
||||
padding: var(--momo-space-4);
|
||||
border-bottom: 1px solid var(--momo-border-light);
|
||||
}
|
||||
|
||||
.abc-detail-card__head h2 {
|
||||
margin: 0;
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: 1.08rem;
|
||||
font-weight: 780;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.abc-detail-table {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.abc-detail-page .table-responsive {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.abc-detail-page .table {
|
||||
min-width: 1180px;
|
||||
}
|
||||
|
||||
.abc-detail-page .table td,
|
||||
.abc-detail-page .table th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.abc-detail-page .abc-product-name {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.abc-loading-card {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--momo-space-3);
|
||||
padding: clamp(2rem, 7vw, 4rem);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-lg);
|
||||
background:
|
||||
radial-gradient(circle at 14px 14px, rgba(172, 92, 58, 0.14) 1.2px, transparent 1.4px) 0 0 / 18px 18px,
|
||||
var(--momo-bg-surface);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.abc-loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid var(--momo-border-light);
|
||||
border-top-color: var(--momo-page-accent-dark);
|
||||
border-radius: 50%;
|
||||
animation: abc-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes abc-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.abc-detail-header {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
padding: var(--momo-space-4);
|
||||
}
|
||||
|
||||
.abc-detail-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.abc-detail-actions,
|
||||
.abc-factor-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.abc-detail-actions .btn,
|
||||
.abc-factor-control .btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
370
web/static/css/page-vendor-tools.css
Normal file
370
web/static/css/page-vendor-tools.css
Normal file
@@ -0,0 +1,370 @@
|
||||
.vendor-tools-page {
|
||||
display: grid;
|
||||
gap: var(--momo-space-5);
|
||||
color: var(--momo-text-primary);
|
||||
}
|
||||
|
||||
.vendor-tools-page .container,
|
||||
.vendor-tools-page .container-fluid {
|
||||
max-width: 100%;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.vendor-tools-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--momo-space-2);
|
||||
color: var(--momo-page-accent-dark);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: var(--momo-text-label);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.vendor-tools-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--momo-space-4);
|
||||
align-items: end;
|
||||
padding: var(--momo-space-5);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-lg);
|
||||
background:
|
||||
radial-gradient(circle at 14px 14px, rgba(172, 92, 58, 0.16) 1.2px, transparent 1.4px) 0 0 / 18px 18px,
|
||||
var(--momo-bg-surface);
|
||||
}
|
||||
|
||||
.vendor-tools-header h1 {
|
||||
margin: 0;
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: clamp(1.55rem, 2.2vw, 2.2rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
color: var(--momo-text-primary);
|
||||
}
|
||||
|
||||
.vendor-tools-header p {
|
||||
max-width: 760px;
|
||||
margin: var(--momo-space-2) 0 0;
|
||||
color: var(--momo-text-secondary);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.vendor-tools-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vendor-tools-nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 36px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-bg-elevated);
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-display);
|
||||
font-size: var(--momo-text-body-sm);
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vendor-tools-nav a:hover,
|
||||
.vendor-tools-nav a.is-active {
|
||||
border-color: var(--momo-page-accent-line);
|
||||
background: var(--momo-page-accent-soft);
|
||||
color: var(--momo-page-accent-dark);
|
||||
}
|
||||
|
||||
.vendor-tools-body {
|
||||
display: grid;
|
||||
gap: var(--momo-space-4);
|
||||
}
|
||||
|
||||
.vendor-tools-page h2,
|
||||
.vendor-tools-page h3,
|
||||
.vendor-tools-page h4,
|
||||
.vendor-tools-page h5,
|
||||
.vendor-tools-page h6 {
|
||||
color: var(--momo-text-primary);
|
||||
font-family: var(--momo-font-display);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.vendor-tools-page h2 {
|
||||
font-size: 1.28rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.vendor-tools-page .section-tabs,
|
||||
.vendor-tools-page .table-container,
|
||||
.vendor-tools-page .stats-card,
|
||||
.vendor-tools-page .card {
|
||||
border: 1px solid var(--momo-border-light) !important;
|
||||
border-radius: var(--momo-radius-md) !important;
|
||||
background: var(--momo-bg-surface) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.vendor-tools-page .section-tabs {
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--momo-space-4);
|
||||
}
|
||||
|
||||
.vendor-tools-page .section-tabs .nav-link {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0.82rem 1rem;
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-display);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vendor-tools-page .section-tabs .nav-link.active {
|
||||
background: var(--momo-text-primary) !important;
|
||||
color: var(--momo-bg-paper) !important;
|
||||
}
|
||||
|
||||
.vendor-tools-page .section-tabs .nav-link:hover:not(.active) {
|
||||
background: var(--momo-bg-paper);
|
||||
color: var(--momo-text-primary);
|
||||
}
|
||||
|
||||
.vendor-tools-page .stats-card {
|
||||
padding: var(--momo-space-4);
|
||||
min-height: 118px;
|
||||
}
|
||||
|
||||
.vendor-tools-page .stats-number,
|
||||
.vendor-tools-page h3[id^="stat"] {
|
||||
color: var(--momo-text-primary) !important;
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: clamp(1.45rem, 2vw, 1.95rem) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container .table-responsive {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table {
|
||||
min-width: 860px;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table tbody tr:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.vendor-tools-page .email-badge,
|
||||
.vendor-tools-page .status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
margin: 2px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-bg-paper);
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vendor-tools-page .status-sent {
|
||||
border-color: var(--momo-success-border);
|
||||
background: var(--momo-success-bg);
|
||||
color: var(--momo-success-text);
|
||||
}
|
||||
|
||||
.vendor-tools-page .status-failed {
|
||||
border-color: var(--momo-danger-border);
|
||||
background: var(--momo-danger-bg);
|
||||
color: var(--momo-danger-text);
|
||||
}
|
||||
|
||||
.vendor-tools-page .status-pending {
|
||||
border-color: var(--momo-warning-border);
|
||||
background: var(--momo-warning-bg);
|
||||
color: var(--momo-warning-text);
|
||||
}
|
||||
|
||||
.vendor-tools-page .upload-area {
|
||||
border: 1px dashed var(--momo-page-accent-line) !important;
|
||||
border-radius: var(--momo-radius-md) !important;
|
||||
padding: clamp(1.2rem, 4vw, 2.6rem) !important;
|
||||
background:
|
||||
radial-gradient(circle at 10px 10px, rgba(172, 92, 58, 0.13) 1.1px, transparent 1.3px) 0 0 / 16px 16px,
|
||||
var(--momo-bg-elevated) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vendor-tools-page .upload-area:hover,
|
||||
.vendor-tools-page .upload-area.dragover {
|
||||
border-color: var(--momo-page-accent-dark) !important;
|
||||
background-color: var(--momo-page-accent-soft) !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.vendor-tools-page .upload-icon {
|
||||
color: var(--momo-page-accent-dark) !important;
|
||||
}
|
||||
|
||||
.vendor-tools-page .modal-header {
|
||||
border-bottom: 1px solid var(--momo-border-light);
|
||||
background: var(--momo-text-primary) !important;
|
||||
color: var(--momo-bg-paper) !important;
|
||||
}
|
||||
|
||||
.vendor-tools-page .modal-content {
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-md);
|
||||
background: var(--momo-bg-surface);
|
||||
}
|
||||
|
||||
.vendor-tools-page .alert-info {
|
||||
border-color: var(--momo-info-border);
|
||||
background: var(--momo-info-bg);
|
||||
color: var(--momo-info-text);
|
||||
}
|
||||
|
||||
.vendor-tools-page .alert-danger {
|
||||
border-color: var(--momo-danger-border);
|
||||
background: var(--momo-danger-bg);
|
||||
color: var(--momo-danger-text);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.vendor-tools-header {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
padding: var(--momo-space-4);
|
||||
}
|
||||
|
||||
.vendor-tools-nav {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.vendor-tools-page {
|
||||
gap: var(--momo-space-4);
|
||||
}
|
||||
|
||||
.vendor-tools-header {
|
||||
border-radius: var(--momo-radius-md);
|
||||
}
|
||||
|
||||
.vendor-tools-nav a {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vendor-tools-page .row {
|
||||
row-gap: var(--momo-space-3);
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container .table-responsive {
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container table,
|
||||
.vendor-tools-page .table-container tbody,
|
||||
.vendor-tools-page .table-container tr,
|
||||
.vendor-tools-page .table-container td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody tr {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-bg-elevated);
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody td {
|
||||
display: grid;
|
||||
grid-template-columns: 5.8rem minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
min-height: 40px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--momo-border-light);
|
||||
text-align: left !important;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody td::before {
|
||||
color: var(--momo-text-tertiary);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody td[colspan] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vendor-tools-page .table-container tbody td[colspan]::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.vendor-tools-page[data-vendor-tool="management"] .table-container tbody td:nth-child(1)::before { content: "代碼"; }
|
||||
.vendor-tools-page[data-vendor-tool="management"] .table-container tbody td:nth-child(2)::before { content: "廠商"; }
|
||||
.vendor-tools-page[data-vendor-tool="management"] .table-container tbody td:nth-child(3)::before { content: "郵件"; }
|
||||
.vendor-tools-page[data-vendor-tool="management"] .table-container tbody td:nth-child(4)::before { content: "數量"; }
|
||||
.vendor-tools-page[data-vendor-tool="management"] .table-container tbody td:nth-child(5)::before { content: "操作"; }
|
||||
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(1)::before { content: "ID"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(2)::before { content: "批次"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(3)::before { content: "廠商"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(4)::before { content: "收件"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(5)::before { content: "主旨"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(6)::before { content: "商品"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(7)::before { content: "狀態"; }
|
||||
.vendor-tools-page[data-vendor-tool="email"] .table-container tbody td:nth-child(8)::before { content: "時間"; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.vendor-tools-nav a {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.vendor-tools-page .d-flex.justify-content-between {
|
||||
align-items: stretch !important;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user