fix: 收斂系統管理頁新版樣式
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-17 21:41:05 +08:00
parent 14d645b4b1
commit 5690d79a40
4 changed files with 313 additions and 32 deletions

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.154"
SYSTEM_VERSION = "V10.155"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -3,13 +3,13 @@
{% block title %}通知模板管理 - EwoooC{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="container-fluid py-4 notification-templates-page">
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-primary bg-opacity-10 d-flex justify-content-between align-items-center">
<div class="card notification-card">
<div class="card-header notification-card__header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-bell me-2"></i>通知模板管理</h5>
<div>
<div class="notification-card__tools">
<select class="form-select form-select-sm d-inline-block w-auto me-2" id="categoryFilter">
<option value="">全部分類</option>
</select>
@@ -21,7 +21,7 @@
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 notification-template-table">
<thead class="table-light">
<thead class="notification-table-head">
<tr>
<th style="width: 40px;">狀態</th>
<th style="width: 150px;">代碼</th>
@@ -33,7 +33,7 @@
</tr>
</thead>
<tbody id="templateList">
<tr><td colspan="7" class="text-center py-4 text-muted">載入中...</td></tr>
<tr><td colspan="7" class="notification-empty">載入中...</td></tr>
</tbody>
</table>
</div>
@@ -118,6 +118,58 @@
</div>
<style>
.notification-templates-page {
color: var(--momo-text-primary);
}
.notification-card {
overflow: hidden;
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
box-shadow: none;
}
.notification-card__header {
gap: var(--momo-space-3);
flex-wrap: wrap;
background: var(--momo-dot-grid), var(--momo-bg-paper);
border-bottom: 1px solid var(--momo-border-light);
color: var(--momo-text-primary);
}
.notification-card__header h5 {
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-title);
font-weight: 800;
letter-spacing: 0;
}
.notification-card__tools {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: var(--momo-space-2);
}
.notification-table-head th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
border-bottom: 1px solid var(--momo-border-light);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
letter-spacing: 0.04em;
}
.notification-empty {
padding: var(--momo-space-5) !important;
color: var(--momo-text-secondary) !important;
text-align: center;
}
.template-preview {
max-height: 60px;
overflow: hidden;
@@ -155,7 +207,81 @@
min-width: 0;
}
.notification-status-badge,
.notification-category-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 3px 8px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-sm);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
line-height: 1;
}
.notification-status-badge.is-active {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.notification-status-badge.is-inactive {
background: var(--momo-tag-muted-bg);
border-color: var(--momo-tag-muted-border);
color: var(--momo-tag-muted-text);
}
.notification-category-badge {
background: var(--momo-info-bg);
border-color: var(--momo-info-border);
color: var(--momo-info-text);
}
.notification-toast {
z-index: 9999;
max-width: min(340px, calc(100vw - 24px));
padding: 12px 14px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
background: var(--momo-bg-elevated);
color: var(--momo-text-primary);
font-weight: 700;
}
.notification-toast--success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.notification-toast--danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.notification-toast--warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
.notification-toast--info {
background: var(--momo-info-bg);
border-color: var(--momo-info-border);
color: var(--momo-info-text);
}
@media (max-width: 760px) {
.notification-card__tools,
.notification-card__tools .form-select,
.notification-card__tools .btn {
width: 100% !important;
}
.notification-template-table {
min-width: 0;
border-collapse: separate;
@@ -285,7 +411,7 @@ function renderTemplates() {
const tbody = document.getElementById('templateList');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4 text-muted">沒有模板</td></tr>';
tbody.innerHTML = '<tr><td colspan="7" class="notification-empty">沒有模板</td></tr>';
return;
}
@@ -295,13 +421,13 @@ function renderTemplates() {
return `
<tr>
<td>
<span class="badge ${t.is_active ? 'bg-success' : 'bg-secondary'}">
<span class="notification-status-badge ${t.is_active ? 'is-active' : 'is-inactive'}">
${t.is_active ? 'ON' : 'OFF'}
</span>
</td>
<td><code>${t.code}</code></td>
<td>${t.name}</td>
<td><span class="badge bg-info bg-opacity-75">${catName}</span></td>
<td><span class="notification-category-badge">${catName}</span></td>
<td>${t.channel === 'telegram' ? '📱 TG' : t.channel === 'line' ? '💬 LINE' : '📱💬'}</td>
<td class="template-preview">${escapeHtml(preview)}...</td>
<td>
@@ -432,8 +558,7 @@ function getCSRFToken() {
function showToast(message, type = 'info') {
// 簡單的 toast 顯示
const toast = document.createElement('div');
toast.className = `alert alert-${type} position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '9999';
toast.className = `notification-toast notification-toast--${type} position-fixed top-0 end-0 m-3`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);

View File

@@ -57,6 +57,21 @@
border-radius: var(--momo-radius-md);
}
.system-import-page .system-version-pill {
display: inline-flex;
align-items: center;
align-self: flex-start;
min-height: 24px;
padding: 4px 10px;
background: var(--momo-tag-terra-bg);
border: 1px solid var(--momo-tag-terra-border);
border-radius: var(--momo-radius-sm);
color: var(--momo-tag-terra-text);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
}
@media (max-width: 760px) {
.system-import-page .system-import-head,
.system-import-page .table-container .d-flex {
@@ -77,7 +92,7 @@
<h1><i class="fas fa-cogs me-2"></i>系統設定與資料匯入</h1>
<p>集中管理備份、月總表、即時業績與一般 Excel 匯入。</p>
</div>
<span class="badge bg-primary">版本 {{ system_version }}</span>
<span class="system-version-pill">版本 {{ system_version }}</span>
</header>
<div class="table-container">
@@ -143,7 +158,7 @@
<input class="form-control" type="file" id="excelFile" accept=".xlsx, .xls">
</div>
<div class="col-md-4">
<button class="btn btn-success w-100" onclick="uploadExcel()">
<button class="btn btn-primary w-100" onclick="uploadExcel()">
<i class="fas fa-table me-2"></i>匯入並建立通用資料表
</button>
</div>

View File

@@ -4,7 +4,148 @@
{% block extra_css %}
<style>
.user-management-page {
color: var(--momo-text-primary);
}
.user-management-head {
padding: var(--momo-space-4) var(--momo-space-5);
background: var(--momo-dot-grid), var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
}
.user-management-head h2 {
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-headline);
font-weight: 800;
letter-spacing: 0;
}
.user-management-page .text-muted {
color: var(--momo-text-secondary) !important;
}
.user-table-head th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
letter-spacing: 0.04em;
}
.user-role-badge,
.user-status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 3px 8px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-sm);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
line-height: 1;
}
.user-role-badge--danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.user-role-badge--warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
.user-role-badge--muted,
.user-status-badge.is-muted {
background: var(--momo-tag-muted-bg);
border-color: var(--momo-tag-muted-border);
color: var(--momo-tag-muted-text);
}
.user-status-badge.is-active,
.user-status-badge.is-info {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.user-status-badge.is-inactive {
background: var(--momo-tag-muted-bg);
border-color: var(--momo-tag-muted-border);
color: var(--momo-tag-muted-text);
}
.user-permission-toolbar {
background: var(--momo-bg-paper);
border-color: var(--momo-border-light);
}
.user-modal-alert {
background: var(--momo-bg-paper);
border-bottom: 1px solid var(--momo-border-light);
color: var(--momo-text-primary);
}
.user-modal-alert--danger {
background: var(--momo-danger-bg);
color: var(--momo-danger-text);
}
.user-modal-alert--info {
background: var(--momo-info-bg);
color: var(--momo-info-text);
}
.user-toast {
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
background: var(--momo-bg-elevated);
color: var(--momo-text-primary);
font-weight: 700;
}
.user-toast--success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.user-toast--danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.user-toast--warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
.user-toast--info {
background: var(--momo-info-bg);
border-color: var(--momo-info-border);
color: var(--momo-info-text);
}
@media (max-width: 760px) {
.user-management-head {
padding: var(--momo-space-4);
}
.user-management-head .col-auto,
.user-management-head .btn {
width: 100%;
}
#usersTable {
min-width: 0;
border-collapse: separate;
@@ -84,8 +225,8 @@
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="container-fluid py-4 user-management-page">
<div class="row mb-4 user-management-head">
<div class="col">
<h2 class="mb-0">
<i class="fas fa-users-cog me-2"></i>用戶管理
@@ -111,7 +252,7 @@
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="usersTable">
<thead class="table-light">
<thead class="user-table-head">
<tr>
<th>帳號</th>
<th>顯示名稱</th>
@@ -272,9 +413,9 @@
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<div class="modal-header user-modal-alert user-modal-alert--danger">
<h5 class="modal-title"><i class="fas fa-exclamation-triangle me-2"></i>確認停用</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>確定要停用用戶 <strong id="deleteUsername"></strong> 嗎?</p>
@@ -295,17 +436,17 @@
<div class="modal fade" id="permissionsModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<div class="modal-header user-modal-alert user-modal-alert--info">
<h5 class="modal-title">
<i class="fas fa-shield-alt me-2"></i>編輯權限: <span id="permissionsUsername"></span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="permissionsUserId">
<!-- 快速設定區 -->
<div class="card mb-3 bg-light">
<div class="card mb-3 user-permission-toolbar">
<div class="card-body py-2">
<div class="row align-items-center">
<div class="col-auto">
@@ -429,14 +570,14 @@
<td>${escapeHtml(user.display_name || '-')}</td>
<td>${escapeHtml(user.email || '-')}</td>
<td>
<span class="badge bg-${getRoleBadgeColor(user.role)}">
<span class="user-role-badge user-role-badge--${getRoleBadgeTone(user.role)}">
${escapeHtml(user.role_label)}
</span>
</td>
<td>
${user.is_active
? '<span class="badge bg-success">啟用</span>'
: '<span class="badge bg-secondary">停用</span>'
? '<span class="user-status-badge is-active">啟用</span>'
: '<span class="user-status-badge is-inactive">停用</span>'
}
</td>
<td>${formatDateTime(user.created_at)}</td>
@@ -724,11 +865,11 @@
input.type = input.type === 'password' ? 'text' : 'password';
}
function getRoleBadgeColor(role) {
function getRoleBadgeTone(role) {
switch (role) {
case 'admin': return 'danger';
case 'manager': return 'warning';
default: return 'secondary';
default: return 'muted';
}
}
@@ -755,12 +896,12 @@
// 簡單的 toast 實現
const toastContainer = document.querySelector('.toast-container') || createToastContainer();
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.className = `toast align-items-center user-toast user-toast--${type}`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${escapeHtml(message)}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
toastContainer.appendChild(toast);
@@ -845,7 +986,7 @@
data-bs-toggle="collapse" data-bs-target="#${collapseId}">
<i class="${icon} me-2"></i>
<span class="flex-grow-1">${escapeHtml(category)}</span>
<span class="badge bg-${grantedCount > 0 ? 'info' : 'secondary'} me-2">
<span class="user-status-badge ${grantedCount > 0 ? 'is-info' : 'is-muted'} me-2">
${grantedCount} / ${permissions.length}
</span>
</button>
@@ -907,10 +1048,10 @@
accordion.querySelectorAll('.accordion-item').forEach(item => {
const checkboxes = item.querySelectorAll('.permission-checkbox');
const checkedCount = item.querySelectorAll('.permission-checkbox:checked').length;
const badge = item.querySelector('.badge');
const badge = item.querySelector('.user-status-badge');
if (badge) {
badge.textContent = `${checkedCount} / ${checkboxes.length}`;
badge.className = `badge bg-${checkedCount > 0 ? 'info' : 'secondary'} me-2`;
badge.className = `user-status-badge ${checkedCount > 0 ? 'is-info' : 'is-muted'} me-2`;
}
});
}