Files
weixin-holiday-message/static/contacts_manager.html

843 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信联系人祝福管理</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container-main {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.stats-bar {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
padding: 15px 25px;
background: #f8f9fa;
border-radius: 10px;
}
.stat-number {
font-size: 28px;
font-weight: bold;
color: #667eea;
}
.search-bar {
margin-bottom: 20px;
}
.filter-bar {
margin-bottom: 20px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.filter-btn {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
}
.contact-table {
font-size: 14px;
}
.contact-table th {
background: #f8f9fa;
font-weight: 600;
white-space: nowrap;
}
.contact-name {
cursor: pointer;
color: #0d6efd;
font-weight: 500;
}
.contact-name:hover {
text-decoration: underline;
}
.category-badge {
cursor: pointer;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
display: inline-block;
background: #e9ecef;
color: #6c757d;
border: 1px solid transparent;
transition: all 0.15s;
margin: 2px;
}
.category-badge:hover {
transform: scale(1.05);
}
.category-badge.active {
color: #fff;
font-weight: 500;
}
.category-badge.同事.active { background: #0dcaf0; border-color: #0dcaf0; }
.category-badge.好友.active { background: #198754; border-color: #198754; }
.category-badge.同学.active { background: #17a2b8; border-color: #17a2b8; }
.category-badge.老师.active { background: #ffc107; border-color: #ffc107; color: #212529; }
.category-badge.亲戚.active { background: #dc3545; border-color: #dc3545; }
.category-badge.客户.active { background: #6f42c1; border-color: #6f42c1; }
.category-badge.供应商.active { background: #20c997; border-color: #20c997; }
.category-badge.其他.active { background: #fd7e14; border-color: #fd7e14; }
.category-tags {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.send-badge {
cursor: pointer;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
display: inline-block;
transition: all 0.15s;
border: 2px solid #dee2e6;
background: #fff;
color: #6c757d;
}
.send-badge:hover {
transform: scale(1.05);
}
.send-badge.active {
background: #198754;
border-color: #198754;
color: #fff;
font-weight: 500;
}
.blessing-text {
cursor: pointer;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
color: #198754;
}
.blessing-text:hover {
text-decoration: underline;
}
.pagination-container {
margin-top: 20px;
justify-content: center;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
/* Modal styles */
.modal-body input, .modal-body textarea, .modal-body select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.modal-body textarea {
resize: vertical;
min-height: 80px;
}
</style>
</head>
<body>
<div class="container-main">
<div class="header">
<h1><i class="bi bi-wechat"></i> 微信联系人祝福管理</h1>
<p class="subtitle">点击姓名/分类/祝福语可编辑,勾选发送选择框</p>
</div>
<!-- 统计栏 -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-number" id="totalCount">0</div>
<div>总联系人</div>
</div>
<div class="stat-item">
<div class="stat-number" id="selectedCount">0</div>
<div>已选择</div>
</div>
</div>
<!-- 新增联系人表单 -->
<div class="add-contact-form mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-person-plus"></i> 新增联系人</span>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#addContactCollapse">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div class="collapse show" id="addContactCollapse">
<div class="card-body">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">姓名 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newNameInput" placeholder="联系人姓名">
</div>
<div class="col-md-4">
<label class="form-label">搜索姓名</label>
<input type="text" class="form-control" id="newSearchNameInput" placeholder="用于搜索的简化姓名(可选)">
</div>
<div class="col-md-2">
<label class="form-label">分类</label>
<select class="form-select" id="newCategorySelect">
<option value="">未分类</option>
<option value="同事">同事</option>
<option value="好友">好友</option>
<option value="同学">同学</option>
<option value="老师">老师</option>
<option value="亲戚">亲戚</option>
<option value="客户">客户</option>
<option value="供应商">供应商</option>
<option value="其他">其他</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-primary w-100" onclick="addContact()">
<i class="bi bi-plus-lg"></i> 添加
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="searchInput" placeholder="搜索联系人...">
<button class="btn btn-primary" onclick="loadContacts()">搜索</button>
<select class="form-select" style="max-width: 120px;" id="pageSizeSelect" onchange="changePageSize()">
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100" selected>100条/页</option>
</select>
</div>
</div>
<!-- 分类筛选 -->
<div class="filter-bar">
<button class="btn btn-outline-primary filter-btn active" data-category="" onclick="setFilter('')">全部</button>
<button class="btn btn-outline-primary filter-btn" data-category="同事" onclick="setFilter('同事')">同事</button>
<button class="btn btn-outline-primary filter-btn" data-category="好友" onclick="setFilter('好友')">好友</button>
<button class="btn btn-outline-primary filter-btn" data-category="同学" onclick="setFilter('同学')">同学</button>
<button class="btn btn-outline-primary filter-btn" data-category="老师" onclick="setFilter('老师')">老师</button>
<button class="btn btn-outline-primary filter-btn" data-category="亲戚" onclick="setFilter('亲戚')">亲戚</button>
<button class="btn btn-outline-primary filter-btn" data-category="客户" onclick="setFilter('客户')">客户</button>
<button class="btn btn-outline-primary filter-btn" data-category="供应商" onclick="setFilter('供应商')">供应商</button>
<button class="btn btn-outline-primary filter-btn" data-category="其他" onclick="setFilter('其他')">其他</button>
<button class="btn btn-danger ms-3" id="batchDeleteBtn" onclick="batchDelete()" style="display:none;">
<i class="bi bi-trash"></i> 批量删除 (<span id="selectedCount2">0</span>)
</button>
</div>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-hover contact-table">
<thead>
<tr>
<th style="width: 50px;"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
<th style="width: 50px;">ID</th>
<th style="min-width: 150px;">姓名</th>
<th style="min-width: 120px;">搜索姓名</th>
<th style="min-width: 100px;">分类</th>
<th style="min-width: 100px;">自定义内容</th>
<th>祝福语</th>
<th style="width: 60px;">发送</th>
<th style="width: 50px;">操作</th>
</tr>
</thead>
<tbody id="contactTable">
<tr>
<td colspan="9" class="loading">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav>
<ul class="pagination pagination-container" id="pagination"></ul>
</nav>
</div>
<!-- 编辑姓名 Modal -->
<div class="modal fade" id="nameModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑姓名</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editNameId">
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" id="editNameInput">
</div>
<div class="mb-3">
<label class="form-label">搜索姓名</label>
<input type="text" id="editSearchNameInput" placeholder="用于搜索的简化姓名">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveName()">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑祝福语 Modal -->
<div class="modal fade" id="blessingModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑祝福语</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editBlessingId">
<div class="mb-3">
<label class="form-label">祝福语</label>
<textarea id="editBlessingInput" rows="4">马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveBlessing()">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑自定义内容 Modal -->
<div class="modal fade" id="customContentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑自定义内容</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editCustomContentId">
<div class="mb-3">
<label class="form-label">自定义内容</label>
<input type="text" id="editCustomContentInput" placeholder="输入自定义内容,默认为分类值">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveCustomContent()">保存</button>
</div>
</div>
</div>
</div>
<!-- Toast 提示 -->
<div class="toast-container">
<div class="toast" id="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body" id="toastBody"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
let currentPage = 1;
let pageSize = 100;
let currentFilter = '';
let contacts = [];
// 页面加载
document.addEventListener('DOMContentLoaded', function() {
loadContacts();
updateStats();
});
// 搜索回车事件
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadContacts(1);
}
});
let totalPages = 1;
let totalCount = 0;
// 加载联系人
async function loadContacts(page = 1) {
currentPage = page;
const search = document.getElementById('searchInput').value;
let url = `${API_BASE}/contacts?page=${page}&page_size=${pageSize}`;
if (currentFilter) url += `&category=${encodeURIComponent(currentFilter)}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
try {
const response = await fetch(url);
const data = await response.json();
contacts = data.contacts;
totalPages = data.total_pages;
totalCount = data.total;
renderTable();
renderPagination();
} catch (error) {
console.error('加载失败:', error);
showToast('加载失败,请检查后端服务是否运行', 'danger');
}
}
// 渲染表格
function renderTable() {
const tbody = document.getElementById('contactTable');
const allCategories = ['同事', '好友', '同学', '老师', '亲戚', '客户', '供应商', '其他'];
if (contacts.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center py-4">暂无数据</td></tr>';
updateBatchDeleteBtn();
return;
}
tbody.innerHTML = contacts.map(c => {
// 解析当前分类(支持逗号分隔的多标签)
const currentCats = (c.category || '').split(',').map(s => s.trim()).filter(s => s);
// 生成分类标签
const catTags = allCategories.map(cat => {
const isActive = currentCats.includes(cat);
return `<span class="category-badge ${cat} ${isActive ? 'active' : ''}"
onclick="toggleCategory(${c.id}, '${cat}')">${cat}</span>`;
}).join('');
// 自定义内容:如果没有则默认显示分类
const customContent = c.custom_content || c.category || '';
// 发送状态
const isSelected = c.selected;
const sendBadge = `<span class="send-badge ${isSelected ? 'active' : ''}"
onclick="toggleSend(${c.id})">${isSelected ? '✓ 发送' : '发送'}</span>`;
return `
<tr>
<td><input type="checkbox" class="contact-checkbox" data-id="${c.id}" ${c.selected ? 'checked' : ''} onchange="toggleSelect(${c.id}); updateBatchDeleteBtn()"></td>
<td>${c.id}</td>
<td><span class="contact-name" onclick="openNameModal(${c.id}, '${escapeHtml(c.name)}', '${escapeHtml(c.search_name || '')}')">${escapeHtml(c.name)}</span></td>
<td><span class="contact-name" onclick="openNameModal(${c.id}, '${escapeHtml(c.name)}', '${escapeHtml(c.search_name || '')}')">${escapeHtml(c.search_name || '')}</span></td>
<td><div class="category-tags">${catTags}</div></td>
<td><span class="contact-name" onclick="openCustomContentModal(${c.id}, '${escapeHtml(customContent)}')">${escapeHtml(customContent)}</span></td>
<td><span class="blessing-text" onclick="openBlessingModal(${c.id}, '${escapeHtml(c.blessing)}')" title="${escapeHtml(c.blessing)}">${escapeHtml(c.blessing)}</span></td>
<td>${sendBadge}</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteContact(${c.id})"><i class="bi bi-trash"></i></button>
</td>
</tr>
`;
}).join('');
updateBatchDeleteBtn();
}
// 渲染分页
function renderPagination() {
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = `
<li class="page-item ${currentPage <= 1 ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadContacts(${currentPage - 1})">上一页</a>
</li>
`;
// 显示页码
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
html += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadContacts(${i})">${i}</a>
</li>
`;
}
html += `
<li class="page-item ${currentPage >= totalPages ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadContacts(${currentPage + 1})">下一页</a>
</li>
`;
pagination.innerHTML = html;
}
// 设置筛选
function setFilter(category) {
currentFilter = category;
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.category === category) {
btn.classList.add('active');
}
});
loadContacts(1);
}
// 改变每页显示数量
function changePageSize() {
pageSize = parseInt(document.getElementById('pageSizeSelect').value);
loadContacts(1);
}
// 新增联系人
async function addContact() {
const name = document.getElementById('newNameInput').value.trim();
const searchName = document.getElementById('newSearchNameInput').value.trim();
const category = document.getElementById('newCategorySelect').value;
if (!name) {
showToast('请输入联系人姓名', 'warning');
document.getElementById('newNameInput').focus();
return;
}
try {
const response = await fetch(`${API_BASE}/contacts`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: name,
search_name: searchName || name,
category: category,
blessing: '马年新春快乐!愿您在新的一年里,事业腾飞,马到成功!',
selected: false
})
});
if (response.ok) {
// 清空表单
document.getElementById('newNameInput').value = '';
document.getElementById('newSearchNameInput').value = '';
document.getElementById('newCategorySelect').value = '';
showToast('添加成功', 'success');
loadContacts(1);
updateStats();
} else {
showToast('添加失败', 'danger');
}
} catch (error) {
showToast('添加失败,请检查后端服务', 'danger');
}
}
// 回车快捷添加
document.getElementById('newNameInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addContact();
}
});
document.getElementById('newSearchNameInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addContact();
}
});
// 更新统计
async function updateStats() {
try {
const response = await fetch(`${API_BASE}/stats`);
const stats = await response.json();
document.getElementById('totalCount').textContent = stats.total;
document.getElementById('selectedCount').textContent = stats.selected;
} catch (error) {
console.error('统计加载失败:', error);
}
}
// 全选/取消全选
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll').checked;
document.querySelectorAll('.contact-checkbox').forEach(cb => {
cb.checked = selectAll;
const id = parseInt(cb.dataset.id);
updateSelect(id, selectAll);
});
updateStats();
updateBatchDeleteBtn();
}
// 单个选择
async function toggleSelect(id) {
const checkbox = document.querySelector(`.contact-checkbox[data-id="${id}"]`);
await updateSelect(id, checkbox.checked);
updateStats();
}
// 更新选择状态
async function updateSelect(id, selected) {
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected: selected})
});
} catch (error) {
console.error('更新失败:', error);
}
}
// 打开编辑姓名Modal
function openNameModal(id, name, searchName) {
document.getElementById('editNameId').value = id;
document.getElementById('editNameInput').value = name;
document.getElementById('editSearchNameInput').value = searchName || '';
new bootstrap.Modal(document.getElementById('nameModal')).show();
}
// 保存姓名
async function saveName() {
const id = document.getElementById('editNameId').value;
const name = document.getElementById('editNameInput').value.trim();
const searchName = document.getElementById('editSearchNameInput').value.trim();
if (!name) {
showToast('姓名不能为空', 'warning');
return;
}
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name, search_name: searchName})
});
bootstrap.Modal.getInstance(document.getElementById('nameModal')).hide();
showToast('保存成功', 'success');
loadContacts(currentPage);
} catch (error) {
showToast('保存失败', 'danger');
}
}
// 快速切换分类标签
async function toggleCategory(id, category) {
// 找到当前联系人
const contact = contacts.find(c => c.id === id);
if (!contact) return;
// 解析当前分类
let currentCats = (contact.category || '').split(',').map(s => s.trim()).filter(s => s);
// 切换分类
const index = currentCats.indexOf(category);
if (index > -1) {
currentCats.splice(index, 1); // 移除
} else {
currentCats.push(category); // 添加
}
const newCategory = currentCats.join(',');
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({category: newCategory})
});
// 更新本地数据
contact.category = newCategory;
// 重新渲染表格
renderTable();
showToast('分类已更新', 'success');
updateStats();
} catch (error) {
showToast('更新失败', 'danger');
}
}
// 快速切换发送状态
async function toggleSend(id) {
const contact = contacts.find(c => c.id === id);
if (!contact) return;
const newSelected = !contact.selected;
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected: newSelected})
});
contact.selected = newSelected;
renderTable();
showToast(newSelected ? '已标记发送' : '已取消发送', 'success');
updateStats();
} catch (error) {
showToast('更新失败', 'danger');
}
}
// 打开编辑祝福语Modal
function openBlessingModal(id, blessing) {
document.getElementById('editBlessingId').value = id;
document.getElementById('editBlessingInput').value = blessing;
new bootstrap.Modal(document.getElementById('blessingModal')).show();
}
// 保存祝福语
async function saveBlessing() {
const id = document.getElementById('editBlessingId').value;
const blessing = document.getElementById('editBlessingInput').value.trim();
if (!blessing) {
showToast('祝福语不能为空', 'warning');
return;
}
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({blessing: blessing})
});
bootstrap.Modal.getInstance(document.getElementById('blessingModal')).hide();
showToast('保存成功', 'success');
loadContacts(currentPage);
} catch (error) {
showToast('保存失败', 'danger');
}
}
// 打开编辑自定义内容Modal
function openCustomContentModal(id, customContent) {
document.getElementById('editCustomContentId').value = id;
document.getElementById('editCustomContentInput').value = customContent;
new bootstrap.Modal(document.getElementById('customContentModal')).show();
}
// 保存自定义内容
async function saveCustomContent() {
const id = document.getElementById('editCustomContentId').value;
const customContent = document.getElementById('editCustomContentInput').value.trim();
try {
await fetch(`${API_BASE}/contacts/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({custom_content: customContent})
});
bootstrap.Modal.getInstance(document.getElementById('customContentModal')).hide();
showToast('保存成功', 'success');
loadContacts(currentPage);
} catch (error) {
showToast('保存失败', 'danger');
}
}
// 删除联系人
async function deleteContact(id) {
if (!confirm('确定要删除这个联系人吗?')) return;
try {
await fetch(`${API_BASE}/contacts/${id}`, {method: 'DELETE'});
showToast('删除成功', 'success');
loadContacts(currentPage);
updateStats();
} catch (error) {
showToast('删除失败', 'danger');
}
}
// 批量删除
async function batchDelete() {
const checkedIds = [];
document.querySelectorAll('.contact-checkbox:checked').forEach(cb => {
checkedIds.push(parseInt(cb.dataset.id));
});
if (checkedIds.length === 0) {
showToast('请先选择要删除的联系人', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${checkedIds.length} 个联系人吗?`)) return;
try {
let success = 0;
for (const id of checkedIds) {
await fetch(`${API_BASE}/contacts/${id}`, {method: 'DELETE'});
success++;
}
showToast(`成功删除 ${success} 个联系人`, 'success');
loadContacts(currentPage);
updateStats();
updateBatchDeleteBtn();
} catch (error) {
showToast('删除失败', 'danger');
}
}
// 更新批量删除按钮状态
function updateBatchDeleteBtn() {
const checkedCount = document.querySelectorAll('.contact-checkbox:checked').length;
const btn = document.getElementById('batchDeleteBtn');
const countSpan = document.getElementById('selectedCount2');
if (checkedCount > 0) {
btn.style.display = 'inline-block';
countSpan.textContent = checkedCount;
} else {
btn.style.display = 'none';
}
}
// 显示Toast提示
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
const toastBody = document.getElementById('toastBody');
toastBody.textContent = message;
toast.className = `toast show bg-${type} text-white`;
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>