568 lines
20 KiB
HTML
568 lines
20 KiB
HTML
{% extends "ewoooc_base.html" %}
|
|
|
|
{% block title %}通知模板管理 - EwoooC{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4 notification-templates-page">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<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 class="notification-card__tools">
|
|
<select class="form-select form-select-sm d-inline-block w-auto me-2" id="categoryFilter">
|
|
<option value="">全部分類</option>
|
|
</select>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="initTemplates()">
|
|
<i class="fas fa-sync me-1"></i>初始化預設
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 notification-template-table">
|
|
<thead class="notification-table-head">
|
|
<tr>
|
|
<th style="width: 40px;">狀態</th>
|
|
<th style="width: 150px;">代碼</th>
|
|
<th style="width: 200px;">名稱</th>
|
|
<th style="width: 100px;">分類</th>
|
|
<th style="width: 80px;">渠道</th>
|
|
<th>預覽</th>
|
|
<th style="width: 100px;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="templateList">
|
|
<tr><td colspan="7" class="notification-empty">載入中...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 編輯 Modal -->
|
|
<div class="modal fade" id="editModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-edit me-2"></i>編輯通知模板</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editForm">
|
|
<input type="hidden" id="editCode">
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">模板代碼</label>
|
|
<input type="text" class="form-control" id="editCodeDisplay" readonly>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">名稱</label>
|
|
<input type="text" class="form-control" id="editName" required>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Emoji 前綴</label>
|
|
<input type="text" class="form-control" id="editEmoji" maxlength="4">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">渠道</label>
|
|
<select class="form-select" id="editChannel">
|
|
<option value="telegram">Telegram</option>
|
|
<option value="line">LINE</option>
|
|
<option value="both">兩者</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">狀態</label>
|
|
<select class="form-select" id="editActive">
|
|
<option value="true">啟用</option>
|
|
<option value="false">停用</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">標題</label>
|
|
<input type="text" class="form-control" id="editTitle">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">訊息內容</label>
|
|
<textarea class="form-control font-monospace" id="editBody" rows="8" required></textarea>
|
|
<small class="text-muted">
|
|
支援變數: <code>{variable_name}</code>。常用變數會根據模板類型自動提供。
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">預覽</label>
|
|
<div class="notification-preview" id="previewArea">
|
|
點擊「預覽」按鈕查看效果
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="previewTemplate()">
|
|
<i class="fas fa-eye me-1"></i>預覽
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveTemplate()">
|
|
<i class="fas fa-save me-1"></i>儲存
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--momo-text-secondary);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.notification-preview {
|
|
min-height: 116px;
|
|
padding: 0.85rem 1rem;
|
|
border: 1px solid var(--momo-border-light);
|
|
border-radius: var(--momo-radius-sm);
|
|
background: var(--momo-bg-paper);
|
|
color: var(--momo-text-primary);
|
|
font-family: var(--momo-font-mono, monospace);
|
|
font-size: 0.88rem;
|
|
line-height: 1.65;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.notification-template-table {
|
|
table-layout: fixed;
|
|
width: 100%;
|
|
}
|
|
|
|
.notification-template-table th,
|
|
.notification-template-table td {
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.notification-template-table .template-preview {
|
|
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;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
.notification-template-table,
|
|
.notification-template-table tbody,
|
|
.notification-template-table tr,
|
|
.notification-template-table td {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
.notification-template-table thead {
|
|
display: none;
|
|
}
|
|
|
|
.notification-template-table tbody {
|
|
display: grid;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.notification-template-table tbody tr {
|
|
overflow: hidden;
|
|
border: 1px solid var(--momo-border-light, #e5dccd);
|
|
border-radius: 8px;
|
|
background: var(--momo-bg-surface, #fffaf1);
|
|
}
|
|
|
|
.notification-template-table tbody td {
|
|
display: grid;
|
|
grid-template-columns: 5.6rem minmax(0, 1fr);
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--momo-border-light, #e5dccd);
|
|
text-align: left !important;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.notification-template-table tbody td:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.notification-template-table tbody td::before {
|
|
color: var(--momo-text-tertiary, #9a8f80);
|
|
font-family: var(--momo-font-mono, monospace);
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.notification-template-table tbody td:nth-child(1)::before { content: "狀態"; }
|
|
.notification-template-table tbody td:nth-child(2)::before { content: "代碼"; }
|
|
.notification-template-table tbody td:nth-child(3)::before { content: "名稱"; }
|
|
.notification-template-table tbody td:nth-child(4)::before { content: "分類"; }
|
|
.notification-template-table tbody td:nth-child(5)::before { content: "渠道"; }
|
|
.notification-template-table tbody td:nth-child(6)::before { content: "預覽"; }
|
|
.notification-template-table tbody td:nth-child(7)::before { content: "操作"; }
|
|
|
|
.notification-template-table tbody td[colspan] {
|
|
display: block;
|
|
}
|
|
|
|
.notification-template-table tbody td[colspan]::before {
|
|
content: none;
|
|
}
|
|
|
|
.notification-template-table .template-preview {
|
|
max-height: none;
|
|
white-space: normal;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let templates = [];
|
|
let categories = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadTemplates();
|
|
|
|
document.getElementById('categoryFilter').addEventListener('change', function() {
|
|
renderTemplates();
|
|
});
|
|
|
|
// 即時預覽
|
|
['editEmoji', 'editTitle', 'editBody'].forEach(id => {
|
|
document.getElementById(id).addEventListener('input', debounce(previewTemplate, 500));
|
|
});
|
|
});
|
|
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function(...args) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
};
|
|
}
|
|
|
|
async function loadTemplates() {
|
|
try {
|
|
const resp = await fetch('/api/notification/templates');
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
templates = data.templates;
|
|
categories = data.categories;
|
|
|
|
// 填充分類下拉選單
|
|
const filter = document.getElementById('categoryFilter');
|
|
filter.innerHTML = '<option value="">全部分類</option>';
|
|
categories.forEach(cat => {
|
|
filter.innerHTML += `<option value="${cat.code}">${cat.name}</option>`;
|
|
});
|
|
|
|
renderTemplates();
|
|
}
|
|
} catch (e) {
|
|
console.error('載入模板失敗:', e);
|
|
showToast('載入模板失敗', 'danger');
|
|
}
|
|
}
|
|
|
|
function renderTemplates() {
|
|
const category = document.getElementById('categoryFilter').value;
|
|
const filtered = category ? templates.filter(t => t.category === category) : templates;
|
|
|
|
const tbody = document.getElementById('templateList');
|
|
if (filtered.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="notification-empty">沒有模板</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = filtered.map(t => {
|
|
const catName = categories.find(c => c.code === t.category)?.name || t.category;
|
|
const preview = `${t.emoji_prefix || ''} ${t.title || ''}\n${t.body || ''}`.substring(0, 80);
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<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="notification-category-badge">${catName}</span></td>
|
|
<td>${t.channel === 'telegram' ? '📱 TG' : t.channel === 'line' ? '💬 LINE' : '📱💬'}</td>
|
|
<td class="template-preview">${escapeHtml(preview)}...</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="editTemplate('${t.code}')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function editTemplate(code) {
|
|
const template = templates.find(t => t.code === code);
|
|
if (!template) return;
|
|
|
|
document.getElementById('editCode').value = template.code;
|
|
document.getElementById('editCodeDisplay').value = template.code;
|
|
document.getElementById('editName').value = template.name || '';
|
|
document.getElementById('editEmoji').value = template.emoji_prefix || '';
|
|
document.getElementById('editChannel').value = template.channel || 'telegram';
|
|
document.getElementById('editActive').value = template.is_active ? 'true' : 'false';
|
|
document.getElementById('editTitle').value = template.title || '';
|
|
document.getElementById('editBody').value = template.body || '';
|
|
document.getElementById('previewArea').textContent = '點擊「預覽」按鈕查看效果';
|
|
|
|
new bootstrap.Modal(document.getElementById('editModal')).show();
|
|
}
|
|
|
|
async function previewTemplate() {
|
|
const code = document.getElementById('editCode').value;
|
|
const emoji = document.getElementById('editEmoji').value;
|
|
const title = document.getElementById('editTitle').value;
|
|
const body = document.getElementById('editBody').value;
|
|
|
|
// 本地預覽(使用範例變數)
|
|
const sampleVars = {
|
|
usage_percent: '87.5', free_gb: '12.3', total_gb: '100',
|
|
new_usage_percent: '75.2', results: '• Docker 清理完成\n• 日誌輪轉完成',
|
|
status: 'unhealthy', database: 'disconnected', deployment: 'momo-app',
|
|
issues: '⏳ mo.wooo.work: 10 天後到期', error: '找不到備份檔案',
|
|
project: 'momo-pro-system', branch: 'main', pipeline_id: '123',
|
|
commit_message: 'feat: 新增功能', author: 'Developer', duration: '2m 30s',
|
|
url: 'http://192.168.0.110:8929/...',
|
|
date: '2026-01-25', app_status: '✅ 正常', backup_status: '✅ 正常',
|
|
crawler_status: '⚠️ 需檢查', last_backup: 'backup_20260125.zip',
|
|
week_start: '2026-01-20', week_end: '2026-01-26', total_sales: '1,234,567',
|
|
order_count: '456', growth_rate: '15.2',
|
|
month: '二月', prev_month: '一月'
|
|
};
|
|
|
|
let preview = `${emoji} *${title}*\n\n${body}`;
|
|
|
|
// 替換變數
|
|
for (const [key, value] of Object.entries(sampleVars)) {
|
|
preview = preview.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
|
}
|
|
|
|
document.getElementById('previewArea').textContent = preview;
|
|
}
|
|
|
|
async function saveTemplate() {
|
|
const code = document.getElementById('editCode').value;
|
|
const data = {
|
|
name: document.getElementById('editName').value,
|
|
emoji_prefix: document.getElementById('editEmoji').value,
|
|
channel: document.getElementById('editChannel').value,
|
|
is_active: document.getElementById('editActive').value === 'true',
|
|
title: document.getElementById('editTitle').value,
|
|
body: document.getElementById('editBody').value
|
|
};
|
|
|
|
try {
|
|
const resp = await fetch(`/api/notification/templates/${code}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCSRFToken()
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await resp.json();
|
|
if (result.success) {
|
|
showToast('模板已更新', 'success');
|
|
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
|
loadTemplates();
|
|
} else {
|
|
showToast(result.error || '更新失敗', 'danger');
|
|
}
|
|
} catch (e) {
|
|
console.error('儲存失敗:', e);
|
|
showToast('儲存失敗', 'danger');
|
|
}
|
|
}
|
|
|
|
async function initTemplates() {
|
|
if (!confirm('確定要初始化預設模板?這不會覆蓋已存在的模板。')) return;
|
|
|
|
try {
|
|
const resp = await fetch('/api/notification/init', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRFToken': getCSRFToken() }
|
|
});
|
|
const result = await resp.json();
|
|
if (result.success) {
|
|
showToast('預設模板已初始化', 'success');
|
|
loadTemplates();
|
|
} else {
|
|
showToast(result.error || '初始化失敗', 'danger');
|
|
}
|
|
} catch (e) {
|
|
console.error('初始化失敗:', e);
|
|
showToast('初始化失敗', 'danger');
|
|
}
|
|
}
|
|
|
|
function getCSRFToken() {
|
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
// 簡單的 toast 顯示
|
|
const toast = document.createElement('div');
|
|
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);
|
|
}
|
|
</script>
|
|
{% endblock %}
|