Files
ewoooc/templates/notification_templates.html
OoO 2c869edcb1
All checks were successful
CD Pipeline / deploy (push) Successful in 1m0s
統一系統管理頁新版殼層
2026-05-14 00:43:55 +08:00

415 lines
16 KiB
HTML

{% extends "ewoooc_base.html" %}
{% block title %}通知模板管理{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<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">
<h5 class="mb-0"><i class="fas fa-bell me-2"></i>通知模板管理</h5>
<div>
<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="table-light">
<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="text-center py-4 text-muted">載入中...</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="border rounded p-3 bg-dark text-light" id="previewArea" style="white-space: pre-wrap; font-family: monospace;">
點擊「預覽」按鈕查看效果
</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>
.template-preview {
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #6c757d;
font-size: 0.875rem;
}
@media (max-width: 760px) {
.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="text-center py-4 text-muted">沒有模板</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="badge ${t.is_active ? 'bg-success' : 'bg-secondary'}">
${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>${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 = `alert alert-${type} position-fixed top-0 end-0 m-3`;
toast.style.zIndex = '9999';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
</script>
{% endblock %}