This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user