Files
SoonDesign/frontend-web/pages/admin/assets/js/views/orders.js
T
24kycj 88c6ce8ccc 重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 18:17:39 +08:00

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 };
})();