- 修复前端 energy.js api.post/get 回调调用为 Promise 链式调用,解决操作无响应 - 修复 AdminAdjust system_operation 模式错误扣减公户余额,现不碰公户 - 修复 AdminAdjust admin_transfer 模式公户按用户数量扣款(amount * len(userIDs)) - 修复 AdminAdjust 余额检查移入事务内并加行锁(SELECT FOR UPDATE),防 TOCTOU 竞态 - FundStore 接口及 FundRepo 新增 LockAndGetBalance 方法
207 lines
8.3 KiB
JavaScript
207 lines
8.3 KiB
JavaScript
// 域能管理页面 JS
|
||
(function () {
|
||
const path = window.location.pathname;
|
||
|
||
if (path === '/admin/energy') {
|
||
initEnergyPage();
|
||
} else if (path === '/admin/energy/logs') {
|
||
initEnergyLogPage();
|
||
} else if (path === '/admin/energy/fund-logs') {
|
||
initFundLogPage();
|
||
}
|
||
|
||
// ----- 域能管理首页:公户余额 + 调整表单 -----
|
||
function initEnergyPage() {
|
||
var adjustForm = document.getElementById('adjust-form');
|
||
if (!adjustForm) return;
|
||
|
||
adjustForm.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var form = e.target;
|
||
var submitBtn = form.querySelector('button[type="submit"]');
|
||
var msgEl = document.getElementById('adjust-message');
|
||
if (msgEl) msgEl.className = 'form-message';
|
||
|
||
var userIDsStr = form.querySelector('[name="user_ids"]').value.trim();
|
||
var amountStr = form.querySelector('[name="amount"]').value.trim();
|
||
var desc = form.querySelector('[name="description"]').value.trim();
|
||
var mode = form.querySelector('[name="mode"]').value;
|
||
|
||
if (!userIDsStr || !amountStr || !desc) {
|
||
showMsg(msgEl, '请填写完整信息', 'error');
|
||
return;
|
||
}
|
||
|
||
var userIDs = userIDsStr.split(',').map(function (s) { return parseInt(s.trim(), 10); }).filter(function (n) { return n > 0; });
|
||
var amount = parseInt(amountStr, 10);
|
||
if (userIDs.length === 0 || isNaN(amount) || amount === 0) {
|
||
showMsg(msgEl, '用户ID和金额格式不正确', 'error');
|
||
return;
|
||
}
|
||
|
||
if (submitBtn) submitBtn.disabled = true;
|
||
|
||
api.post('/api/admin/energy/adjust', {
|
||
user_ids: userIDs,
|
||
amount: amount,
|
||
description: desc,
|
||
mode: mode
|
||
}).then(function (res) {
|
||
if (submitBtn) submitBtn.disabled = false;
|
||
if (res.success) {
|
||
showMsg(msgEl, res.message || '调整成功', 'success');
|
||
setTimeout(function () {
|
||
api.get('/api/admin/energy/fund-balance').then(function (r) {
|
||
if (r.success && r.data) {
|
||
var balEl = document.getElementById('fund-balance');
|
||
if (balEl) {
|
||
balEl.textContent = r.data.balance_display.toFixed(1);
|
||
balEl.className = 'fund-balance' + (r.data.balance < 0 ? ' negative' : '');
|
||
}
|
||
}
|
||
});
|
||
}, 300);
|
||
} else {
|
||
showMsg(msgEl, res.message || '调整失败', 'error');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----- 域能日志页 -----
|
||
function initEnergyLogPage() {
|
||
var page = 1;
|
||
var typeFilter = document.getElementById('log-type-filter');
|
||
|
||
function loadLogs() {
|
||
var logType = typeFilter ? typeFilter.value : '';
|
||
api.get('/api/admin/energy/logs?page=' + page + '&page_size=20&type=' + encodeURIComponent(logType)).then(function (res) {
|
||
if (!res.success) return;
|
||
renderLogTable('energy-log-tbody', res.data.items, function (item) {
|
||
return [
|
||
item.id,
|
||
formatTime(item.created_at),
|
||
item.user_id,
|
||
'<span class="' + (item.amount >= 0 ? 'amount-positive' : 'amount-negative') + '">' + item.energy_display.toFixed(1) + '</span>',
|
||
'<span class="type-badge">' + (item.type_name || item.type) + '</span>',
|
||
escapeHtml(item.description || '')
|
||
];
|
||
}, 6);
|
||
renderPagination('energy-log-pagination', page, res.data.total_pages, function (p) {
|
||
page = p;
|
||
loadLogs();
|
||
});
|
||
});
|
||
}
|
||
|
||
if (typeFilter) {
|
||
typeFilter.addEventListener('change', function () {
|
||
page = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
loadLogs();
|
||
}
|
||
|
||
// ----- 公户流水页 -----
|
||
function initFundLogPage() {
|
||
var page = 1;
|
||
var typeFilter = document.getElementById('fund-log-type-filter');
|
||
|
||
function loadLogs() {
|
||
var logType = typeFilter ? typeFilter.value : '';
|
||
api.get('/api/admin/energy/fund-logs?page=' + page + '&page_size=20&type=' + encodeURIComponent(logType)).then(function (res) {
|
||
if (!res.success) return;
|
||
renderLogTable('fund-log-tbody', res.data.items, function (item) {
|
||
return [
|
||
item.id,
|
||
formatTime(item.created_at),
|
||
'<span class="' + (item.amount >= 0 ? 'amount-positive' : 'amount-negative') + '">' + item.amount_display.toFixed(1) + '</span>',
|
||
'<span class="type-badge">' + (item.type_name || item.type) + '</span>',
|
||
escapeHtml(item.description || '')
|
||
];
|
||
}, 5);
|
||
renderPagination('fund-log-pagination', page, res.data.total_pages, function (p) {
|
||
page = p;
|
||
loadLogs();
|
||
});
|
||
});
|
||
}
|
||
|
||
if (typeFilter) {
|
||
typeFilter.addEventListener('change', function () {
|
||
page = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
loadLogs();
|
||
}
|
||
|
||
// ----- 通用工具函数 -----
|
||
function renderLogTable(tbodyId, items, rowFn, colCount) {
|
||
var tbody = document.getElementById(tbodyId);
|
||
if (!tbody) return;
|
||
if (!items || items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="' + colCount + '" class="empty-state">暂无记录</td></tr>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
for (var i = 0; i < items.length; i++) {
|
||
var cells = rowFn(items[i]);
|
||
html += '<tr>';
|
||
for (var j = 0; j < cells.length; j++) {
|
||
html += '<td>' + cells[j] + '</td>';
|
||
}
|
||
html += '</tr>';
|
||
}
|
||
tbody.innerHTML = html;
|
||
}
|
||
|
||
function renderPagination(paginationId, current, total, onChange) {
|
||
var container = document.getElementById(paginationId);
|
||
if (!container || total <= 1) { if (container) container.innerHTML = ''; return; }
|
||
var html = '';
|
||
html += '<button' + (current <= 1 ? ' disabled' : '') + ' data-page="' + (current - 1) + '">‹ 上一页</button>';
|
||
var start = Math.max(1, current - 2);
|
||
var end = Math.min(total, current + 2);
|
||
for (var p = start; p <= end; p++) {
|
||
html += '<button class="' + (p === current ? 'active' : '') + '" data-page="' + p + '">' + p + '</button>';
|
||
}
|
||
html += '<button' + (current >= total ? ' disabled' : '') + ' data-page="' + (current + 1) + '">下一页 ›</button>';
|
||
container.innerHTML = html;
|
||
|
||
var buttons = container.querySelectorAll('button');
|
||
for (var i = 0; i < buttons.length; i++) {
|
||
(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
var p = parseInt(this.getAttribute('data-page'), 10);
|
||
if (p > 0 && p <= total && p !== current) {
|
||
onChange(p);
|
||
}
|
||
});
|
||
})(buttons[i]);
|
||
}
|
||
}
|
||
|
||
function formatTime(isoStr) {
|
||
if (!isoStr) return '';
|
||
var d = new Date(isoStr);
|
||
var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
function showMsg(msgEl, text, type) {
|
||
if (!msgEl) return;
|
||
msgEl.textContent = text;
|
||
msgEl.className = 'form-message ' + type;
|
||
setTimeout(function () { msgEl.className = 'form-message'; }, 4000);
|
||
}
|
||
})();
|