ebe191b06d
- 会员改为永久激活方案;云端文件不再拦截非会员;预览弹窗内导出/打印才校验 - 首页最近文件支持本地记录,登录后与云端合并;移除独立会员页与订阅页 - 模板库入库管理:soon_templates 表、Admin 上传 CRUD、/templates 轻量列表与按需下载 Co-authored-by: Cursor <cursoragent@cursor.com>
335 lines
14 KiB
JavaScript
335 lines
14 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
var Api = function () { return window.AdminApi; };
|
|
var Ui = function () { return window.AdminUi; };
|
|
var I = function () { return window.AdminI18n; };
|
|
var State = function () { return window.AdminState; };
|
|
var Filter = function () { return window.AdminFilter; };
|
|
var Menu = function () { return window.AdminMenu; };
|
|
var Router = function () { return window.AdminRouter; };
|
|
|
|
function buildQuery() {
|
|
var st = State().orders;
|
|
var q = 'orders?page=' + st.page + '&size=' + st.size;
|
|
if (st.q) q += '&q=' + encodeURIComponent(st.q);
|
|
if (st.status) q += '&status=' + encodeURIComponent(st.status);
|
|
if (st.refund_status) q += '&refund_status=' + encodeURIComponent(st.refund_status);
|
|
if (st.channel) q += '&channel=' + encodeURIComponent(st.channel);
|
|
if (st.from) q += '&from=' + encodeURIComponent(st.from);
|
|
if (st.to) q += '&to=' + encodeURIComponent(st.to);
|
|
return q;
|
|
}
|
|
|
|
function statusOptions(selected) {
|
|
var opts = [{ v: '', l: '全部状态' }, { v: 'pending', l: '待支付' }, { v: 'paid', l: '已支付' },
|
|
{ v: 'cancelled', l: '已取消' }, { v: 'refunded', l: '已退款' }];
|
|
return opts.map(function (o) {
|
|
return '<option value="' + o.v + '"' + (selected === o.v ? ' selected' : '') + '>' + o.l + '</option>';
|
|
}).join('');
|
|
}
|
|
|
|
function refundStatusOptions(selected) {
|
|
var opts = [{ v: '', l: '全部退款' }, { v: 'pending', l: '待审核' }, { v: 'none', l: '无申请' },
|
|
{ v: 'approved', l: '已通过' }, { v: 'rejected', l: '已拒绝' }];
|
|
return opts.map(function (o) {
|
|
return '<option value="' + o.v + '"' + (selected === o.v ? ' selected' : '') + '>' + o.l + '</option>';
|
|
}).join('');
|
|
}
|
|
|
|
function channelOptions(selected) {
|
|
var opts = [{ v: '', l: '全部渠道' }, { v: 'alipay', l: '支付宝' }, { v: 'wechat', l: '微信支付' }];
|
|
return opts.map(function (o) {
|
|
return '<option value="' + o.v + '"' + (selected === o.v ? ' selected' : '') + '>' + o.l + '</option>';
|
|
}).join('');
|
|
}
|
|
|
|
function renderFilterBar(st) {
|
|
return Filter().bar(
|
|
Filter().field('关键词', Filter().searchInput('orderQ', st.q, '订单号或邮箱')) +
|
|
Filter().field('状态', Filter().select('orderStatus', statusOptions(st.status))) +
|
|
Filter().field('退款', Filter().select('orderRefundStatus', refundStatusOptions(st.refund_status))) +
|
|
Filter().field('渠道', Filter().select('orderChannel', channelOptions(st.channel))) +
|
|
Ui().filterDateField('orderFrom', '从', st.from) +
|
|
Ui().filterDateField('orderTo', '至', st.to),
|
|
'<button type="button" class="soon-btn soon-btn--sm" id="orderSearchBtn">查询</button>' +
|
|
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="orderResetBtn">重置</button>'
|
|
);
|
|
}
|
|
|
|
function orderActionMenu(o) {
|
|
var items = [{ label: '详情', action: 'detail', dataset: { id: String(o.id) } }];
|
|
if (o.status === 'pending') {
|
|
items.push({ label: '取消', action: 'cancel', dataset: { id: String(o.id) } });
|
|
}
|
|
if (o.status === 'paid' && o.refund_status === 'pending') {
|
|
items.push({ label: '通过退款', action: 'refund_approve', dataset: { id: String(o.id) } });
|
|
items.push({ label: '拒绝退款', action: 'refund_reject', dataset: { id: String(o.id) } });
|
|
}
|
|
if (o.status === 'paid' && (o.refund_status === 'none' || !o.refund_status)) {
|
|
items.push({ label: '退款', action: 'refund_direct', dataset: { id: String(o.id) } });
|
|
}
|
|
return Menu().actionMenu('order-' + o.id, items);
|
|
}
|
|
|
|
function bindOrderMenus(root) {
|
|
Menu().bind(root, function (action, btn) {
|
|
var id = parseInt(btn.dataset.id, 10);
|
|
if (action === 'detail') showOrderDetail(id);
|
|
if (action === 'cancel') {
|
|
Ui().confirm('确认取消该待支付订单?', function () {
|
|
Api().postJson('orders/' + id + '/cancel', {}).then(function (r) {
|
|
if (r) { soonToast('订单已取消', 'success'); Ui().closeDrawer(); render(); }
|
|
});
|
|
});
|
|
}
|
|
if (action === 'refund_approve' || action === 'refund_direct') {
|
|
var title = action === 'refund_direct' ? '管理员退款' : '通过退款申请';
|
|
openNoteModal(title, '可选:填写审核备注', function (note) {
|
|
return Api().postJson('orders/' + id + '/refund/approve', { note: note }).then(function (r) {
|
|
if (r) { soonToast('退款已处理', 'success'); Ui().closeDrawer(); render(); }
|
|
});
|
|
});
|
|
}
|
|
if (action === 'refund_reject') {
|
|
openNoteModal('拒绝退款', '请填写拒绝原因', function (note) {
|
|
if (!note) {
|
|
soonToast('请填写拒绝原因', 'warn');
|
|
return false;
|
|
}
|
|
return Api().postJson('orders/' + id + '/refund/reject', { note: note }).then(function (r) {
|
|
if (r) { soonToast('已拒绝退款', 'success'); Ui().closeDrawer(); render(); }
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyFilters() {
|
|
var st = State().orders;
|
|
st.q = Ui().el('orderQ').value.trim();
|
|
st.status = Ui().el('orderStatus').value;
|
|
st.refund_status = Ui().el('orderRefundStatus').value;
|
|
st.channel = Ui().el('orderChannel').value;
|
|
st.from = Ui().el('orderFrom').value;
|
|
st.to = Ui().el('orderTo').value;
|
|
if (st.from && st.to && st.from > st.to) {
|
|
soonToast('开始日期不能晚于结束日期', 'warn');
|
|
return false;
|
|
}
|
|
st.page = 1;
|
|
return true;
|
|
}
|
|
|
|
function bindFilters() {
|
|
Ui().el('orderSearchBtn').onclick = function () {
|
|
if (applyFilters()) {
|
|
Router().syncQuery();
|
|
render();
|
|
}
|
|
};
|
|
Ui().el('orderResetBtn').onclick = function () {
|
|
State().orders = {
|
|
page: 1, size: 20, q: '', status: '', refund_status: '', channel: '', from: '', to: '',
|
|
};
|
|
Router().syncQuery();
|
|
render();
|
|
};
|
|
}
|
|
|
|
function rowActions(o) {
|
|
var btns = '<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost order-detail-btn" data-id="' +
|
|
o.id + '">详情</button>';
|
|
if (o.status === 'pending') {
|
|
btns += '<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost order-cancel-btn" data-id="' +
|
|
o.id + '">取消</button>';
|
|
}
|
|
if (o.status === 'paid' && o.refund_status === 'pending') {
|
|
btns += '<button type="button" class="soon-btn soon-btn--xs order-refund-approve-btn" data-id="' + o.id +
|
|
'">通过退款</button>';
|
|
btns += '<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost order-refund-reject-btn" data-id="' +
|
|
o.id + '">拒绝</button>';
|
|
}
|
|
if (o.status === 'paid' && (o.refund_status === 'none' || !o.refund_status)) {
|
|
btns += '<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost order-refund-direct-btn" data-id="' +
|
|
o.id + '">退款</button>';
|
|
}
|
|
return '<div class="soon-admin-btn-group">' + btns + '</div>';
|
|
}
|
|
|
|
function openNoteModal(title, placeholder, onSave) {
|
|
var form = '<div class="soon-admin-form">' +
|
|
'<div class="soon-form-field"><label class="soon-form-label">备注</label>' +
|
|
'<textarea id="orderNote" class="soon-input soon-input--sm" rows="3" placeholder="' + I().esc(placeholder) +
|
|
'"></textarea></div>' +
|
|
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">确认</button></div>';
|
|
Ui().openFormModal(title, form, function () {
|
|
return onSave(Ui().el('orderNote').value.trim());
|
|
});
|
|
}
|
|
|
|
function showOrderDetail(id, opts) {
|
|
opts = opts || {};
|
|
if (!opts.fromRoute) Router().openDetail(id);
|
|
Api().get('orders/' + id).then(function (j) {
|
|
if (!j) return;
|
|
var o = j.data.order || {};
|
|
var sub = j.data.subscription;
|
|
|
|
var basic = Ui().detailRow('订单号', I().esc(o.order_no)) +
|
|
Ui().detailRow('用户', I().esc(o.email || '')) +
|
|
Ui().detailRow('套餐', I().esc(o.plan_name || '-')) +
|
|
Ui().detailRow('渠道', I().esc(I().tChannel(o.channel))) +
|
|
Ui().detailRow('金额', I().formatCents(o.amount_cents)) +
|
|
Ui().detailRow('订单状态', I().badgeStatus(o.status)) +
|
|
Ui().detailRow('退款状态', I().badgeRefundStatus(o.refund_status)) +
|
|
Ui().detailRow('交易号', I().esc(o.txn_id || '-')) +
|
|
Ui().detailRow('创建时间', I().esc(I().formatDateTime(o.created_at))) +
|
|
Ui().detailRow('支付时间', I().esc(I().formatDateTime(o.paid_at))) +
|
|
Ui().detailRow('取消时间', I().esc(I().formatDateTime(o.cancelled_at))) +
|
|
Ui().detailRow('退款时间', I().esc(I().formatDateTime(o.refunded_at)));
|
|
|
|
var refundHtml = '';
|
|
if (o.refund_reason) {
|
|
refundHtml += Ui().detailRow('退款原因', I().esc(o.refund_reason));
|
|
}
|
|
if (o.refund_note) {
|
|
refundHtml += Ui().detailRow('审核备注', I().esc(o.refund_note));
|
|
}
|
|
if (!refundHtml) {
|
|
refundHtml = '<p class="soon-admin-detail-empty">无退款记录</p>';
|
|
}
|
|
|
|
var subHtml;
|
|
if (sub) {
|
|
subHtml = Ui().detailRow('会员状态', I().badgeStatus(sub.status)) +
|
|
Ui().detailRow('开始', I().esc(I().formatDateTime(sub.started_at))) +
|
|
Ui().detailRow('到期', I().esc(I().formatDateTime(sub.expires_at)));
|
|
} else {
|
|
subHtml = '<p class="soon-admin-detail-empty">无关联会员记录</p>';
|
|
}
|
|
|
|
var drawerActions = rowActions(o).replace(/<button[^>]*order-detail-btn[^>]*>[\s\S]*?<\/button>/, '');
|
|
Ui().openDrawer('订单 ' + I().esc(o.order_no),
|
|
'<div class="soon-admin-drawer-actions">' + drawerActions + '</div>' +
|
|
Ui().detailSection('订单信息', basic) +
|
|
Ui().detailSection('退款信息', refundHtml) +
|
|
Ui().detailSection('关联会员', subHtml));
|
|
|
|
var drawer = document.getElementById('adminDrawer');
|
|
if (drawer) bindRowActions(drawer);
|
|
});
|
|
}
|
|
|
|
function bindRowActions(root) {
|
|
if (!root) return;
|
|
root.querySelectorAll('.order-detail-btn').forEach(function (btn) {
|
|
btn.onclick = function () { showOrderDetail(parseInt(btn.dataset.id, 10)); };
|
|
});
|
|
root.querySelectorAll('.order-cancel-btn').forEach(function (btn) {
|
|
btn.onclick = function () {
|
|
Ui().confirm('确认取消该待支付订单?', function () {
|
|
Api().postJson('orders/' + btn.dataset.id + '/cancel', {}).then(function (r) {
|
|
if (r) {
|
|
soonToast('订单已取消', 'success');
|
|
Ui().closeDrawer();
|
|
render();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
});
|
|
root.querySelectorAll('.order-refund-approve-btn, .order-refund-direct-btn').forEach(function (btn) {
|
|
btn.onclick = function () {
|
|
var isDirect = btn.classList.contains('order-refund-direct-btn');
|
|
var title = isDirect ? '管理员退款' : '通过退款申请';
|
|
openNoteModal(title, '可选:填写审核备注', function (note) {
|
|
return Api().postJson('orders/' + btn.dataset.id + '/refund/approve', { note: note }).then(function (r) {
|
|
if (r) {
|
|
layer.closeAll();
|
|
var gw = r.data && r.data.gateway;
|
|
var msg = '退款已完成';
|
|
if (gw && gw.indexOf('skipped') === 0) msg += '(未配置支付密钥,仅标记退款并撤销会员)';
|
|
else if (gw && gw.indexOf('fail') >= 0) msg += '(支付渠道退款失败,已标记退款并撤销会员)';
|
|
soonToast(msg, 'success');
|
|
Ui().closeDrawer();
|
|
render();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
});
|
|
root.querySelectorAll('.order-refund-reject-btn').forEach(function (btn) {
|
|
btn.onclick = function () {
|
|
openNoteModal('拒绝退款', '请填写拒绝原因', function (note) {
|
|
if (!note) {
|
|
soonToast('请填写拒绝原因', 'warn');
|
|
return false;
|
|
}
|
|
return Api().postJson('orders/' + btn.dataset.id + '/refund/reject', { note: note }).then(function (r) {
|
|
if (r) {
|
|
layer.closeAll();
|
|
soonToast('已拒绝退款申请', 'success');
|
|
Ui().closeDrawer();
|
|
render();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
});
|
|
}
|
|
|
|
function render() {
|
|
var panel = Ui().el('panel-orders');
|
|
if (!panel) return;
|
|
if (!Router().hasPendingDetail()) Ui().closeDrawer();
|
|
var st = State().orders;
|
|
panel.innerHTML = '<div class="soon-admin-page">' +
|
|
Ui().pageHeader('订单管理', '支付订单查询、取消与退款审核') +
|
|
renderFilterBar(st) + Ui().skeletonTable() + '</div>';
|
|
|
|
Api().get(buildQuery()).then(function (j) {
|
|
if (!j) {
|
|
panel.innerHTML = '<div class="soon-admin-page">' +
|
|
Ui().pageHeader('订单管理', '支付订单查询、取消与退款审核') +
|
|
renderFilterBar(st) + Ui().listError('订单列表加载失败', 'ordersListRetry') + '</div>';
|
|
bindFilters();
|
|
Ui().el('ordersListRetry').onclick = render;
|
|
var errDetailId = Router().consumeDetailId();
|
|
if (errDetailId) showOrderDetail(errDetailId, { fromRoute: true });
|
|
return;
|
|
}
|
|
var rows = (j.data.items || []).map(function (o) {
|
|
return '<tr><td>' + I().esc(o.order_no || o.id) + '</td><td>' + I().esc(o.email || '') + '</td><td>' +
|
|
I().esc(o.plan_name || '-') + '</td><td>' + I().esc(I().tChannel(o.channel)) + '</td><td>' +
|
|
I().formatCents(o.amount_cents) + '</td><td>' + I().badgeStatus(o.status) + '</td><td>' +
|
|
I().badgeRefundStatus(o.refund_status) + '</td><td>' + I().formatDateTime(o.created_at) + '</td><td>' +
|
|
I().formatDateTime(o.paid_at) + '</td><td>' + orderActionMenu(o) + '</td></tr>';
|
|
}).join('');
|
|
|
|
panel.innerHTML = '<div class="soon-admin-page">' +
|
|
Ui().pageHeader('订单管理', '支付订单查询、取消与退款审核') +
|
|
renderFilterBar(st) +
|
|
Ui().dataTable(
|
|
['订单号', '用户', '套餐', '渠道', '金额', '状态', '退款', '创建时间', '支付时间', '操作'],
|
|
rows
|
|
) +
|
|
Ui().pagerHtml(j.data.page, j.data.size, j.data.total) + '</div>';
|
|
|
|
bindFilters();
|
|
Ui().bindPager(panel, j.data.page, j.data.size, j.data.total, function (p, newSize) {
|
|
st.page = p;
|
|
if (newSize) st.size = newSize;
|
|
Router().syncQuery();
|
|
render();
|
|
});
|
|
bindOrderMenus(panel);
|
|
var detailId = Router().consumeDetailId();
|
|
if (detailId) showOrderDetail(detailId, { fromRoute: true });
|
|
});
|
|
}
|
|
|
|
window.AdminViews = window.AdminViews || {};
|
|
window.AdminViews.orders = { render: render };
|
|
})();
|