Files
ewoooc/templates/notification_templates.html
2026-06-26 07:28:15 +08:00

582 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: 220px;">通知名稱</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="6" 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-12">
<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[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 = [];
const SAMPLE_TEMPLATE_VALUES = {
usage_percent: '87.5', free_gb: '12.3', total_gb: '100',
new_usage_percent: '75.2', results: '已完成清理並釋放空間',
status: '需確認', database: '資料連線需確認', deployment: '主系統',
issues: '網站憑證將於 10 天後到期', error: '找不到最新備份檔案',
project: 'PChome 業績成長系統', branch: '正式版', deploy_id: '123',
update_summary: '更新業績流程', author: '系統管理員', duration: '2 分 30 秒',
url: 'https://mo.wooo.work',
date: '2026-01-25', app_status: '正常', backup_status: '正常',
crawler_status: '需檢查', last_backup: '最新備份已建立',
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: '一月'
};
function publicTemplateText(text) {
let output = String(text || '');
for (const [key, value] of Object.entries(SAMPLE_TEMPLATE_VALUES)) {
output = output.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
return output
.replace(/CI\/CD Pipeline SUCCESS/g, '部署流程成功')
.replace(/CI\/CD Pipeline FAILED/g, '部署流程失敗')
.replace(/K8s Pod/g, '服務健康')
.replace(/Pod 重啟/g, '服務重啟')
.replace(/Pipeline/g, '部署流程')
.replace(/Commit/g, '更新內容')
.replace(/備份資料庫/g, '建立營運資料備份')
.replace(/資料庫狀態/g, '資料連線狀態')
.replace(/資料庫大小/g, '資料容量')
.replace(/資料庫:/g, '資料連線:')
.replace(/資料庫:/g, '資料連線:')
.replace(/資料庫/g, '資料連線');
}
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="6" class="notification-empty">沒有模板</td></tr>';
return;
}
tbody.innerHTML = filtered.map(t => {
const catName = categories.find(c => c.code === t.category)?.name || t.category;
const displayName = publicTemplateText(t.name || '未命名通知');
const preview = publicTemplateText(`${t.emoji_prefix || ''} ${t.title || ''}\n${t.body || ''}`).substring(0, 96);
const channelLabel = t.channel === 'telegram' ? 'Telegram' : t.channel === 'line' ? 'LINE' : '雙渠道';
return `
<tr>
<td>
<span class="notification-status-badge ${t.is_active ? 'is-active' : 'is-inactive'}">
${t.is_active ? '啟用' : '停用'}
</span>
</td>
<td>${escapeHtml(displayName)}</td>
<td><span class="notification-category-badge">${catName}</span></td>
<td>${channelLabel}</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('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;
// 本地預覽(使用範例變數)
let preview = `${emoji} *${title}*\n\n${body}`;
// 替換變數
for (const [key, value] of Object.entries(SAMPLE_TEMPLATE_VALUES)) {
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 %}