Files
ns2.0/templates/admin_wishlist.html

694 lines
20 KiB
HTML
Executable File

{% extends "base.html" %}
{% block content %}
{% include 'admin_nav.html' %}
<div class="admin-container">
<div class="admin-content">
<!-- 标签页导航 -->
<div class="tab-nav">
<button class="tab-btn active" onclick="switchTab('unnotified')">
<i class="fas fa-bell-slash"></i>
未通知应用
</button>
<button class="tab-btn" onclick="switchTab('notified')">
<i class="fas fa-bell"></i>
已通知应用
</button>
</div>
<!-- 未通知应用 -->
<div class="admin-card" id="unnotified-tab">
<div class="card-header">
<div class="header-left">
<i class="fas fa-bell-slash"></i>
<h3>未通知应用</h3>
</div>
<div class="filter-sort">
<select class="sort-select" onchange="sortTable(this, 'unnotified')">
<option value="last_added_desc">最近添加时间 ↓</option>
<option value="last_added_asc">最近添加时间 ↑</option>
<option value="count_desc">关注人数 ↓</option>
<option value="count_asc">关注人数 ↑</option>
</select>
<div class="search-box">
<input type="text" class="search-input"
placeholder="搜索未通知应用..."
onkeyup="filterTable(this, 'unnotified')">
<i class="fas fa-search"></i>
</div>
</div>
</div>
<div class="wishlist-stats">
<table class="wishlist-table">
<thead>
<tr>
<th>应用名称</th>
<th>关注人数</th>
<th>最早添加时间</th>
<th>最近添加时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in wishlist_items %}
{% if not item.notified %}
<tr class="wishlist-row">
<td class="app-name">{{ item.app_name }}</td>
<td>{{ item.count }}</td>
<td>{{ item.first_added }}</td>
<td>{{ item.last_added }}</td>
<td>
<div class="action-buttons">
<button class="btn-view" onclick="viewUsers('{{ item.app_name }}')" title="查看用户">
<i class="fas fa-users"></i>
</button>
<button class="btn-notify large" onclick="notifyUsers('{{ item.app_name }}')" title="发送通知">
<i class="fas fa-envelope"></i>
<span>发送通知</span>
</button>
<button class="btn-delete" onclick="deleteWishlistItem('{{ item.app_name }}')" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 已通知应用 -->
<div class="admin-card" id="notified-tab" style="display: none;">
<div class="card-header">
<div class="header-left">
<i class="fas fa-bell"></i>
<h3>已通知应用</h3>
</div>
<div class="filter-sort">
<select class="sort-select" onchange="sortTable(this, 'notified')">
<option value="last_added_desc">最近添加时间 ↓</option>
<option value="last_added_asc">最近添加时间 ↑</option>
<option value="count_desc">关注人数 ↓</option>
<option value="count_asc">关注人数 ↑</option>
</select>
<div class="search-box">
<input type="text" class="search-input"
placeholder="搜索已通知应用..."
onkeyup="filterTable(this, 'notified')">
<i class="fas fa-search"></i>
</div>
</div>
</div>
<div class="wishlist-stats">
<table class="wishlist-table notified">
<thead>
<tr>
<th>应用名称</th>
<th>关注人数</th>
<th>最早添加时间</th>
<th>最近添加时间</th>
<th>通知时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in wishlist_items %}
{% if item.notified %}
<tr class="wishlist-row">
<td class="app-name">{{ item.app_name }}</td>
<td>{{ item.count }}</td>
<td>{{ item.first_added }}</td>
<td>{{ item.last_added }}</td>
<td>{{ item.notified_at }}</td>
<td>
<div class="action-buttons">
<button class="btn-view" onclick="viewUsers('{{ item.app_name }}')" title="查看用户">
<i class="fas fa-users"></i>
</button>
<button class="btn-notify" onclick="notifyUsers('{{ item.app_name }}')" title="重新发送通知">
<i class="fas fa-paper-plane"></i>
<span>重新发送</span>
</button>
<button class="btn-delete" onclick="deleteWishlistItem('{{ item.app_name }}')" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 用户列表弹窗 -->
<div id="usersModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>关注用户列表</h3>
<button class="close-btn" onclick="closeUsersModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="usersList" class="users-list"></div>
</div>
</div>
<!-- Toast 容器 -->
<div id="toast" class="toast"></div>
<style>
.wishlist-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.wishlist-table th,
.wishlist-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.wishlist-table th {
background: #f5f5f5;
font-weight: 600;
}
.action-buttons {
display: flex;
gap: 8px;
}
.btn-view,
.btn-notify,
.btn-delete {
padding: 6px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-view {
background: #1890ff;
color: white;
}
.btn-notify {
background: #52c41a;
color: white;
}
.btn-notify:hover {
background: #73d13d;
}
.btn-delete {
background: #ff4d4f;
color: white;
}
.btn-view:hover {
background: #40a9ff;
}
.btn-delete:hover {
background: #ff7875;
}
.users-list {
max-height: 400px;
overflow-y: auto;
padding: 20px;
}
.user-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: 500;
margin-bottom: 4px;
}
.user-email {
font-size: 12px;
color: #666;
}
/* 暗色模式适配 */
[data-theme="dark"] .wishlist-table th {
background: #1f1f1f;
}
[data-theme="dark"] .wishlist-table td {
border-color: #333;
}
[data-theme="dark"] .user-item {
border-color: #333;
}
[data-theme="dark"] .user-email {
color: #999;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
position: relative;
}
[data-theme="dark"] .modal-content {
background: #242424;
}
.admin-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
.search-box {
position: relative;
width: 300px;
}
.search-input {
width: 100%;
padding: 8px 12px;
padding-right: 35px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
outline: none;
}
.search-box i {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
}
/* 暗色模式适配 */
[data-theme="dark"] .admin-card {
background: #242424;
}
[data-theme="dark"] .card-header {
border-color: #333;
}
[data-theme="dark"] .search-input {
background: #333;
border-color: #444;
color: #fff;
}
[data-theme="dark"] .search-input:focus {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23,125,220,0.2);
}
[data-theme="dark"] .search-box i {
color: #666;
}
/* 标签页样式 */
.tab-nav {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
background: white;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.tab-btn.active {
background: #1890ff !important;
color: white !important;
}
.tab-btn:hover:not(.active) {
background: #f5f5f5;
}
/* 暗色模式适配 */
[data-theme="dark"] .tab-btn {
background: #242424;
color: #999;
}
[data-theme="dark"] .tab-btn.active {
background: #177ddc !important;
color: white !important;
}
[data-theme="dark"] .tab-btn:hover:not(.active) {
background: #333;
}
/* Toast 样式 */
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 4px;
z-index: 1000;
display: none;
animation: fadeInOut 3s ease;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(-20px); }
10% { opacity: 1; transform: translateY(0); }
90% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-20px); }
}
/* 暗色模式适配 */
[data-theme="dark"] .toast {
background: rgba(255, 255, 255, 0.9);
color: #000;
}
/* Add styles for filter and sort */
.filter-sort {
display: flex;
gap: 12px;
align-items: center;
}
.filter-select,
.sort-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.filter-select:hover,
.sort-select:hover {
border-color: #40a9ff;
}
.filter-select:focus,
.sort-select:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
outline: none;
}
/* Dark mode styles for filter and sort */
[data-theme="dark"] .filter-select,
[data-theme="dark"] .sort-select {
background: #333;
border-color: #444;
color: #fff;
}
[data-theme="dark"] .filter-select:hover,
[data-theme="dark"] .sort-select:hover {
border-color: #177ddc;
}
[data-theme="dark"] .filter-select:focus,
[data-theme="dark"] .sort-select:focus {
border-color: #177ddc;
box-shadow: 0 0 0 2px rgba(23,125,220,0.2);
}
</style>
<script>
function viewUsers(appName) {
fetch(`/admin/wishlist/users/${encodeURIComponent(appName)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const usersList = document.getElementById('usersList');
usersList.innerHTML = data.users.map(user => `
<div class="user-item">
<img src="${user.avatar || '/static/images/default-avatar.png'}"
alt="${user.name}"
class="user-avatar">
<div class="user-info">
<div class="user-name">${user.name}</div>
<div class="user-email">${user.email || '未设置邮箱'}</div>
</div>
</div>
`).join('');
document.getElementById('usersModal').style.display = 'flex';
} else {
showToast(data.error || '获取用户列表失败');
}
})
.catch(error => {
console.error('Failed to get users:', error);
showToast('获取用户列表失败');
});
}
function closeUsersModal() {
document.getElementById('usersModal').style.display = 'none';
}
function deleteWishlistItem(appName) {
if (!confirm(`确定要删除应用"${appName}"的所有心愿记录吗?`)) return;
fetch(`/admin/wishlist/delete/${encodeURIComponent(appName)}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
showToast(data.error || '删除失败');
}
})
.catch(error => {
console.error('Delete failed:', error);
showToast('删除失败,请稍后重试');
});
}
function notifyUsers(appName) {
const row = event.target.closest('tr');
const isNotified = row.closest('table').classList.contains('notified');
const message = isNotified ?
`确定要重新发送通知给所有关注"${appName}"的用户吗?` :
`确定要通知所有关注"${appName}"的用户吗?`;
if (!confirm(message)) return;
fetch(`/admin/wishlist/notify/${encodeURIComponent(appName)}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message || '通知发送成功');
// 移动行到已通知表格
if (!isNotified) {
const notifiedTable = document.querySelector('.wishlist-table.notified tbody');
const newRow = row.cloneNode(true);
// 修改按钮样式和文本
const notifyBtn = newRow.querySelector('.btn-notify');
notifyBtn.classList.remove('large');
notifyBtn.innerHTML = '<i class="fas fa-paper-plane"></i><span>重新发送</span>';
// 添加通知时间列
const timeCell = document.createElement('td');
timeCell.textContent = new Date().toLocaleString();
newRow.insertBefore(timeCell, newRow.lastElementChild);
notifiedTable.insertBefore(newRow, notifiedTable.firstChild);
row.remove();
// 如果未通知列表为空,自动切换到已通知标签页
const unnotifiedRows = document.querySelector('.wishlist-table:not(.notified) tbody').children.length;
if (unnotifiedRows === 0) {
switchTab('notified');
}
}
} else {
showToast(data.error || '发送失败');
}
})
.catch(error => {
console.error('Notification failed:', error);
showToast('发送失败,请稍后重试');
});
}
// 点击遮罩层关闭弹窗
document.getElementById('usersModal').addEventListener('click', function(e) {
if (e.target === this) {
closeUsersModal();
}
});
function filterTable(input, type) {
const searchText = input.value.toLowerCase();
const table = type === 'notified' ?
document.querySelector('.wishlist-table.notified') :
document.querySelector('.wishlist-table:not(.notified)');
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const appName = row.querySelector('.app-name').textContent.toLowerCase();
const matchesSearch = appName.includes(searchText);
row.style.display = matchesSearch ? '' : 'none';
});
}
function switchTab(tabId) {
// 更新标签按钮状态
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// 获取当前点击的按钮
const activeBtn = document.querySelector(`.tab-btn[onclick*="${tabId}"]`);
activeBtn.classList.add('active');
// 切换内容显示
document.getElementById('unnotified-tab').style.display = tabId === 'unnotified' ? 'block' : 'none';
document.getElementById('notified-tab').style.display = tabId === 'notified' ? 'block' : 'none';
}
// Toast 通知函数
function showToast(message, duration = 3000) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.display = 'block';
// 重置动画
toast.style.animation = 'none';
toast.offsetHeight; // 触发重排
toast.style.animation = 'fadeInOut 3s ease';
setTimeout(() => {
toast.style.display = 'none';
}, duration);
}
function sortTable(select, type) {
const sortBy = select.value;
const table = type === 'notified' ?
document.querySelector('.wishlist-table.notified') :
document.querySelector('.wishlist-table:not(.notified)');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
let aValue, bValue;
if (sortBy.startsWith('count')) {
aValue = parseInt(a.querySelectorAll('td')[1].textContent);
bValue = parseInt(b.querySelectorAll('td')[1].textContent);
} else if (sortBy.startsWith('last_added')) {
const aIndex = type === 'notified' ? 3 : 3;
const bIndex = type === 'notified' ? 3 : 3;
aValue = new Date(a.querySelectorAll('td')[aIndex].textContent);
bValue = new Date(b.querySelectorAll('td')[bIndex].textContent);
}
if (sortBy.endsWith('asc')) {
return aValue - bValue;
} else {
return bValue - aValue;
}
});
rows.forEach(row => tbody.appendChild(row));
}
</script>
{% endblock %}