This commit is contained in:
@@ -81,6 +81,8 @@
|
||||
<input type="text" id="searchQuery" class="search-input" placeholder="请输入搜索关键词...">
|
||||
<select id="keyFilter" class="search-input"></select>
|
||||
<button class="btn" onclick="clearKeyFilter()">清空Key筛查</button>
|
||||
<select id="typeFilter" class="search-input"></select>
|
||||
<button class="btn" onclick="clearTypeFilter()">清空类型筛查</button>
|
||||
<button class="btn btn-primary" onclick="performSearch('exact')">关键词搜索</button>
|
||||
<button class="btn btn-secondary" onclick="performSearch('fuzzy')">模糊搜索</button>
|
||||
<button class="btn" onclick="loadAllData()">显示全部</button>
|
||||
@@ -93,12 +95,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_admin or has_manage_key %}
|
||||
<div class="search-container" style="margin-top: 12px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">统计报表</div>
|
||||
<div class="search-controls" style="flex-wrap: wrap;">
|
||||
<input type="datetime-local" id="reportFrom" class="search-input" placeholder="开始时间">
|
||||
<input type="datetime-local" id="reportTo" class="search-input" placeholder="结束时间">
|
||||
<select id="reportInterval" class="search-input">
|
||||
<option value="day">按天</option>
|
||||
<option value="week">按周</option>
|
||||
<option value="month">按月</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" onclick="generateReport()">生成报表</button>
|
||||
<button class="btn" onclick="downloadReportCsv()">下载CSV</button>
|
||||
</div>
|
||||
<div id="reportBox" class="search-result" style="display: none; margin-top: 10px;"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table id="dataTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>图片</th>
|
||||
<th>数据</th>
|
||||
<th>时间</th>
|
||||
<th>录入人</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@@ -153,6 +174,11 @@ function getCookie(name) {
|
||||
// DOM元素引用
|
||||
const searchQueryInput = document.getElementById('searchQuery');
|
||||
const keyFilterSelect = document.getElementById('keyFilter');
|
||||
const typeFilterSelect = document.getElementById('typeFilter');
|
||||
const reportFromInput = document.getElementById('reportFrom');
|
||||
const reportToInput = document.getElementById('reportTo');
|
||||
const reportIntervalSelect = document.getElementById('reportInterval');
|
||||
const reportBox = document.getElementById('reportBox');
|
||||
const searchResultDiv = document.getElementById('searchResult');
|
||||
const searchStatus = document.getElementById('searchStatus');
|
||||
const searchCount = document.getElementById('searchCount');
|
||||
@@ -173,6 +199,9 @@ const zoomOutBtn = document.getElementById('zoomOutBtn');
|
||||
const resetZoomBtn = document.getElementById('resetZoomBtn');
|
||||
const zoomValue = document.getElementById('zoomValue');
|
||||
|
||||
const IS_ADMIN = {{ is_admin|yesno:"true,false" }};
|
||||
const HAS_MANAGE_KEY = {{ has_manage_key|yesno:"true,false" }};
|
||||
|
||||
// 全局变量
|
||||
let currentId = '';
|
||||
let currentWriter = '';
|
||||
@@ -182,6 +211,7 @@ let currentSearchQuery = ''; // 记录当前搜索查询
|
||||
let isFuzzySearch = false; // 记录当前是否为模糊搜索
|
||||
let isDeleting = false; // 标记是否正在删除
|
||||
let currentKeyFilter = '';
|
||||
let currentTypeFilter = '';
|
||||
|
||||
// 图片缩放相关变量
|
||||
let currentScale = 1;
|
||||
@@ -205,6 +235,10 @@ async function performSearch(type) {
|
||||
currentKeyFilter = '';
|
||||
if (keyFilterSelect) keyFilterSelect.value = '';
|
||||
}
|
||||
if (currentTypeFilter) {
|
||||
currentTypeFilter = '';
|
||||
if (typeFilterSelect) typeFilterSelect.value = '';
|
||||
}
|
||||
|
||||
currentSearchQuery = query;
|
||||
isFuzzySearch = type === 'fuzzy';
|
||||
@@ -270,11 +304,11 @@ async function loadAllData() {
|
||||
showSearchLoading();
|
||||
|
||||
try {
|
||||
if (currentKeyFilter) {
|
||||
const response = await fetch(`/elastic/filter-by-key/?key=${encodeURIComponent(currentKeyFilter)}`);
|
||||
if (currentKeyFilter || currentTypeFilter) {
|
||||
const response = await fetch(`/elastic/filter/?key=${encodeURIComponent(currentKeyFilter)}&type=${encodeURIComponent(currentTypeFilter)}`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
displayAllData(data.data || [], currentKeyFilter);
|
||||
displayAllData(data.data || [], currentKeyFilter, currentTypeFilter);
|
||||
} else {
|
||||
showSearchMessage(`加载数据失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
@@ -306,7 +340,10 @@ async function loadAllData() {
|
||||
function displayAllData(data, key) {
|
||||
searchResultDiv.style.display = 'block';
|
||||
searchResultDiv.className = 'search-result';
|
||||
searchStatus.textContent = key ? `按Key筛查:${key}` : '显示全部数据';
|
||||
const labels = [];
|
||||
if (key) labels.push(`Key:${key}`);
|
||||
if (currentTypeFilter) labels.push(`类型:${currentTypeFilter}`);
|
||||
searchStatus.textContent = labels.length ? `筛查:${labels.join(',')}` : '显示全部数据';
|
||||
searchCount.textContent = `共 ${data.length} 条记录`;
|
||||
|
||||
renderTable(data);
|
||||
@@ -318,7 +355,7 @@ function clearSearch() {
|
||||
searchResultDiv.style.display = 'none';
|
||||
currentSearchQuery = '';
|
||||
|
||||
if (currentKeyFilter) {
|
||||
if (currentKeyFilter || currentTypeFilter) {
|
||||
loadAllData();
|
||||
return;
|
||||
}
|
||||
@@ -359,13 +396,119 @@ function clearKeyFilter() {
|
||||
loadAllData();
|
||||
}
|
||||
|
||||
async function initTypeFilter() {
|
||||
if (!typeFilterSelect) return;
|
||||
typeFilterSelect.innerHTML = '<option value="">全部类型</option>';
|
||||
try {
|
||||
const resp = await fetch('/elastic/types-for-filter/', { credentials: 'same-origin' });
|
||||
const data = await resp.json();
|
||||
if (data.status !== 'success') return;
|
||||
const types = data.data || [];
|
||||
types.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = String(t || '');
|
||||
opt.textContent = String(t || '');
|
||||
typeFilterSelect.appendChild(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
typeFilterSelect.addEventListener('change', () => {
|
||||
currentTypeFilter = (typeFilterSelect.value || '').trim();
|
||||
loadAllData();
|
||||
});
|
||||
}
|
||||
|
||||
function clearTypeFilter() {
|
||||
currentTypeFilter = '';
|
||||
if (typeFilterSelect) typeFilterSelect.value = '';
|
||||
loadAllData();
|
||||
}
|
||||
|
||||
function buildReportParams() {
|
||||
const params = new URLSearchParams();
|
||||
if (currentKeyFilter) params.set('key', currentKeyFilter);
|
||||
if (currentTypeFilter) params.set('type', currentTypeFilter);
|
||||
const iv = (reportIntervalSelect && reportIntervalSelect.value) ? reportIntervalSelect.value : 'day';
|
||||
params.set('interval', iv);
|
||||
const fromVal = reportFromInput ? (reportFromInput.value || '').trim() : '';
|
||||
const toVal = reportToInput ? (reportToInput.value || '').trim() : '';
|
||||
if (fromVal) params.set('from', fromVal);
|
||||
if (toVal) params.set('to', toVal);
|
||||
return params;
|
||||
}
|
||||
|
||||
async function generateReport() {
|
||||
if (!reportBox) return;
|
||||
reportBox.style.display = 'block';
|
||||
reportBox.className = 'search-result';
|
||||
reportBox.innerHTML = '<div>正在生成报表...</div>';
|
||||
try {
|
||||
const params = buildReportParams();
|
||||
const resp = await fetch(`/elastic/report/?${params.toString()}`, { credentials: 'same-origin' });
|
||||
const data = await resp.json();
|
||||
if (data.status !== 'success') {
|
||||
reportBox.className = 'search-result error';
|
||||
reportBox.innerHTML = `<div>生成失败:${data.message || '未知错误'}</div>`;
|
||||
return;
|
||||
}
|
||||
const r = data.data || {};
|
||||
const total = r.total || 0;
|
||||
const byType = r.by_type || [];
|
||||
const byTime = r.by_time || [];
|
||||
const rng = r.range || {};
|
||||
const flt = r.filters || {};
|
||||
const lines = [];
|
||||
const filterParts = [];
|
||||
if (flt.key) filterParts.push(`Key:${flt.key}`);
|
||||
if (flt.type) filterParts.push(`类型:${flt.type}`);
|
||||
if (flt.interval) filterParts.push(`粒度:${flt.interval}`);
|
||||
lines.push(`<div style="font-weight: 600;">总数:${total}</div>`);
|
||||
if (rng.from || rng.to) {
|
||||
lines.push(`<div class="muted" style="margin-top: 4px;">时间范围:${(rng.from || '')} ~ ${(rng.to || '')}</div>`);
|
||||
}
|
||||
if (filterParts.length) {
|
||||
lines.push(`<div class="muted" style="margin-top: 4px;">筛查:${filterParts.join(',')}</div>`);
|
||||
}
|
||||
|
||||
const typeRows = byType.map(it => `<tr><td style="padding:4px 8px;">${it.type || ''}</td><td style="padding:4px 8px; text-align:right;">${it.count || 0}</td></tr>`).join('');
|
||||
const timeRows = byTime.map(it => `<tr><td style="padding:4px 8px;">${it.bucket || ''}</td><td style="padding:4px 8px; text-align:right;">${it.count || 0}</td></tr>`).join('');
|
||||
|
||||
lines.push(`<div style="display:flex; gap:12px; flex-wrap: wrap; margin-top: 10px;">
|
||||
<div style="min-width: 260px; flex: 1;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">按成果类型</div>
|
||||
<table style="width:100%; border-collapse: collapse;">
|
||||
<thead><tr><th style="text-align:left; padding:4px 8px; border-bottom:1px solid #eee;">类型</th><th style="text-align:right; padding:4px 8px; border-bottom:1px solid #eee;">数量</th></tr></thead>
|
||||
<tbody>${typeRows || '<tr><td colspan="2" class="muted" style="padding:6px 8px;">暂无</td></tr>'}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="min-width: 260px; flex: 1;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">按时间</div>
|
||||
<table style="width:100%; border-collapse: collapse;">
|
||||
<thead><tr><th style="text-align:left; padding:4px 8px; border-bottom:1px solid #eee;">时间</th><th style="text-align:right; padding:4px 8px; border-bottom:1px solid #eee;">数量</th></tr></thead>
|
||||
<tbody>${timeRows || '<tr><td colspan="2" class="muted" style="padding:6px 8px;">暂无</td></tr>'}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
reportBox.innerHTML = lines.join('');
|
||||
} catch (e) {
|
||||
reportBox.className = 'search-result error';
|
||||
reportBox.innerHTML = `<div>生成失败:${e.message || '未知错误'}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadReportCsv() {
|
||||
const params = buildReportParams();
|
||||
window.location.href = `/elastic/report/csv/?${params.toString()}`;
|
||||
}
|
||||
|
||||
// 渲染表格
|
||||
function renderTable(data) {
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = '<td colspan="4" style="text-align: center; color: #999;">暂无数据</td>';
|
||||
row.innerHTML = '<td colspan="5" style="text-align: center; color: #999;">暂无数据</td>';
|
||||
tableBody.appendChild(row);
|
||||
return;
|
||||
}
|
||||
@@ -422,6 +565,7 @@ function renderTable(data) {
|
||||
<td>
|
||||
${displayData}
|
||||
</td>
|
||||
<td style="font-size: 12px; white-space: nowrap;">${formatDateTime(item.time)}</td>
|
||||
<td style="font-size: 12px;">${item.writer_name || item.writer_id || ''}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" onclick="openEdit('${item._id || item.id}')">编辑</button>
|
||||
@@ -432,6 +576,18 @@ function renderTable(data) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateTime(t) {
|
||||
if (!t) return '';
|
||||
try {
|
||||
const d = new Date(t);
|
||||
if (String(d) === 'Invalid Date') return String(t);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
} catch (e) {
|
||||
return String(t);
|
||||
}
|
||||
}
|
||||
|
||||
function buildImageCell(item) {
|
||||
const urls = Array.isArray(item.image_urls) ? item.image_urls : (item.image_url ? [item.image_url] : []);
|
||||
if (!urls.length) return '无图片';
|
||||
@@ -667,6 +823,18 @@ async function doDelete(id){
|
||||
// 页面加载时自动加载所有数据
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initKeyFilter();
|
||||
initTypeFilter();
|
||||
if (reportFromInput && reportToInput && reportIntervalSelect) {
|
||||
const now = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const toLocal = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
if (!reportToInput.value) reportToInput.value = toLocal(now);
|
||||
if (!reportFromInput.value) {
|
||||
const from = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
reportFromInput.value = toLocal(from);
|
||||
}
|
||||
if (!reportIntervalSelect.value) reportIntervalSelect.value = 'day';
|
||||
}
|
||||
loadAllData();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user