Files
jiapu/public/js/vip-pages.js
T
2026-08-29 19:06:07 +08:00

573 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.VipPages = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.VipPages.init();
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function confirmAction(message) {
if (root.ProfileUI && typeof root.ProfileUI.confirmAction === 'function') {
return root.ProfileUI.confirmAction(message);
}
return Promise.resolve(!root['confirm'] || root['confirm'](message));
}
var PACKAGE_TYPES = {
vip: '会员套餐',
storage: '存储扩容'
};
var DURATION_UNITS = {
permanent: '永久',
day: '天',
month: '个月',
year: '年'
};
var PAY_STATUS = {
'0': '待支付',
'1': '已支付',
'2': '已关闭',
'3': '已退款'
};
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function normalizeId(value) {
var text;
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
if (value === undefined || value === null) return '';
text = String(value).trim();
return /^[1-9][0-9]*$/.test(text) ? text : '';
}
function optionalText(value) {
if (value === undefined || value === null) return '';
return String(value).trim();
}
function moneyText(value, optional) {
var text;
if ((value === undefined || value === null || value === '') && optional) return '';
if (typeof value === 'number' && (!Number.isFinite(value) || value < 0)) return null;
text = String(value === undefined || value === null ? '' : value).trim();
return /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(text) ? text : null;
}
function validLimit(value) {
return value === undefined || value === null ||
(Number.isSafeInteger(Number(value)) && Number(value) >= 0);
}
function normalizeVipPackage(item) {
var source = item || {};
var packageId = normalizeId(source.packageId);
var packageName = optionalText(source.packageName);
var packageType = optionalText(source.packageType);
var durationUnit = optionalText(source.durationUnit);
var price = moneyText(source.price, false);
var originalPrice = moneyText(source.originalPrice, true);
var status = String(source.status === undefined || source.status === null ? '' : source.status);
if (!packageId || !packageName ||
!Object.prototype.hasOwnProperty.call(PACKAGE_TYPES, packageType) ||
!Object.prototype.hasOwnProperty.call(DURATION_UNITS, durationUnit) ||
price === null || originalPrice === null || status !== '0' ||
!validLimit(source.durationValue) || !validLimit(source.genealogyLimit) ||
!validLimit(source.memberLimit) || !validLimit(source.storageLimitMb)) {
return null;
}
return {
packageId: packageId,
packageName: packageName,
packageType: packageType,
packageDesc: optionalText(source.packageDesc),
price: price,
originalPrice: originalPrice,
durationValue: source.durationValue === undefined || source.durationValue === null ? null : Number(source.durationValue),
durationUnit: durationUnit,
genealogyLimit: source.genealogyLimit === undefined || source.genealogyLimit === null ? null : Number(source.genealogyLimit),
memberLimit: source.memberLimit === undefined || source.memberLimit === null ? null : Number(source.memberLimit),
storageLimitMb: source.storageLimitMb === undefined || source.storageLimitMb === null ? null : Number(source.storageLimitMb),
remark: optionalText(source.remark)
};
}
function normalizeVipPackages(data) {
if (!Array.isArray(data)) return [];
return data.map(normalizeVipPackage).filter(Boolean);
}
function normalizeGenealogyOption(item) {
var source = item || {};
var genealogyId = normalizeId(source.genealogyId);
var genealogyName = optionalText(source.genealogyName);
if (!genealogyId || !genealogyName) return null;
return {
genealogyId: genealogyId,
genealogyName: genealogyName,
surname: optionalText(source.surname)
};
}
function normalizeGenealogyOptions(data) {
if (!Array.isArray(data)) return [];
return data.map(normalizeGenealogyOption).filter(Boolean);
}
function resolveContextGenealogyId(options, contextId) {
var normalizedContextId = normalizeId(contextId);
if (!normalizedContextId || !Array.isArray(options)) return '';
return options.some(function (option) {
return option.genealogyId === normalizedContextId;
}) ? normalizedContextId : '';
}
function buildVipOrderBody(values) {
var source = values || {};
var body = {};
var packageId = normalizeId(source.packageId);
var genealogyId = normalizeId(source.genealogyId);
if (packageId) body.packageId = packageId;
else if (source.packageId !== undefined) body.invalidPackageId = true;
if (genealogyId) body.genealogyId = genealogyId;
else if (source.genealogyId) body.invalidGenealogyId = true;
return body;
}
function validateVipOrderBody(body) {
if (!body || !normalizeId(body.packageId) || body.invalidPackageId) return '请选择有效会员套餐';
if (body.invalidGenealogyId ||
(body.genealogyId !== undefined && body.genealogyId !== null &&
String(body.genealogyId).trim() !== '' && !normalizeId(body.genealogyId))) {
return '家谱选项无效';
}
return '';
}
function normalizeVipCapability(value) {
var source = value || {};
var methods = Array.isArray(source.paymentMethods) ? source.paymentMethods : [];
var wechat = methods.find(function (item) { return item && item.paymentMethod === 'WECHAT'; });
return {
enabled: source.enabled === true && !!wechat && wechat.enabled === true,
disabledReason: optionalText(source.disabledReason || wechat && wechat.disabledReason)
};
}
function normalizeVipOrder(item) {
var source = item || {};
var orderId = normalizeId(source.orderId);
var packageId = normalizeId(source.packageId);
var genealogyId = source.genealogyId === undefined || source.genealogyId === null
? ''
: normalizeId(source.genealogyId);
var orderAmount = moneyText(source.orderAmount, false);
var payAmount = moneyText(source.payAmount, false);
var payStatus = String(source.payStatus === undefined || source.payStatus === null ? '' : source.payStatus);
var status = String(source.status === undefined || source.status === null ? '' : source.status);
if (!orderId || !packageId || !optionalText(source.orderNo) ||
!optionalText(source.packageName) ||
(source.genealogyId !== undefined && source.genealogyId !== null && !genealogyId) ||
orderAmount === null || payAmount === null ||
!Object.prototype.hasOwnProperty.call(PAY_STATUS, payStatus) ||
['0', '1'].indexOf(status) < 0 ||
(source.transactionId !== undefined && source.transactionId !== null && !normalizeId(source.transactionId))) {
return null;
}
return {
orderId: orderId,
orderNo: optionalText(source.orderNo),
packageId: packageId,
packageName: optionalText(source.packageName),
genealogyId: genealogyId,
genealogyNo: optionalText(source.genealogyNo),
genealogyName: optionalText(source.genealogyName),
orderAmount: orderAmount,
payAmount: payAmount,
payType: optionalText(source.payType),
payStatus: payStatus,
payTime: optionalText(source.payTime),
expireTime: optionalText(source.expireTime),
status: status,
remark: optionalText(source.remark),
transactionId: source.transactionId === undefined || source.transactionId === null ? '' : normalizeId(source.transactionId),
canRefreshPayment: source.canRefreshPayment === true,
canClose: source.canClose === true
};
}
function normalizePaymentOrder(value) {
var source = value || {};
var order = normalizeVipOrder(source.order);
var transactionId = normalizeId(source.transactionId);
var codeUrl = optionalText(source.codeUrl);
if (!order || !transactionId || !codeUrl || source.tradeType !== 'NATIVE' || source.paymentMethod !== 'WECHAT') return null;
return {
order: order,
transactionId: transactionId,
codeUrl: codeUrl,
expiresAt: optionalText(source.expiresAt),
completed: source.completed === true
};
}
function normalizePaymentStatus(value) {
var source = value || {};
var transactionId = normalizeId(source.transactionId);
var status = optionalText(source.status);
if (!transactionId || ['CREATED', 'PAYING', 'SUCCESS', 'CLOSED', 'REFUNDING', 'REFUNDED'].indexOf(status) < 0) return null;
return { transactionId: transactionId, status: status, paidAt: optionalText(source.paidAt) };
}
function normalizeVipOrders(data) {
if (!Array.isArray(data)) return [];
return data.map(normalizeVipOrder).filter(Boolean);
}
function renderVipPackages(data, selectedPackageId) {
var items = Array.isArray(data) ? data : [];
var selectedId = normalizeId(selectedPackageId);
if (!items.length) return '<div class="api-empty">当前没有可用会员套餐</div>';
return items.map(function (item) {
var duration = item.durationUnit === 'permanent'
? DURATION_UNITS[item.durationUnit]
: (item.durationValue === null ? '' : item.durationValue) + DURATION_UNITS[item.durationUnit];
var limits = [
item.genealogyLimit === null ? '' : '家谱 ' + item.genealogyLimit + ' 部',
item.memberLimit === null ? '' : '成员 ' + item.memberLimit + ' 人',
item.storageLimitMb === null ? '' : '存储 ' + item.storageLimitMb + ' MB'
].filter(Boolean).join(' · ');
return '<button class="module-card' + (item.packageId === selectedId ? ' is-selected' : '') +
'" type="button" data-vip-package-id="' + escapeHtml(item.packageId) +
'" data-vip-package-name="' + escapeHtml(item.packageName) + '"><span class="icon">会</span><h3>' +
escapeHtml(item.packageName) + '</h3><p>' +
escapeHtml([PACKAGE_TYPES[item.packageType], item.packageDesc, duration, limits].filter(Boolean).join(' · ')) +
'</p><strong>¥' + escapeHtml(item.price) + '</strong></button>';
}).join('');
}
function renderGenealogyOptions(data) {
var items = normalizeGenealogyOptions(data);
return '<option value="">不关联具体家谱</option>' + items.map(function (item) {
return '<option value="' + escapeHtml(item.genealogyId) + '">' +
escapeHtml(item.genealogyName + (item.surname ? ' · ' + item.surname + '氏' : '')) +
'</option>';
}).join('');
}
function renderVipOrders(data) {
var items = normalizeVipOrders(data);
if (!items.length) return '<div class="api-empty">当前没有会员订单</div>';
return items.map(function (item) {
var details = [
item.orderNo,
item.genealogyName,
'订单金额 ¥' + item.orderAmount,
'应付 ¥' + item.payAmount,
item.payType ? '支付方式 ' + item.payType : '',
item.payTime ? '支付时间 ' + item.payTime : '',
item.expireTime ? '有效期至 ' + item.expireTime : '',
item.remark
].filter(Boolean).join(' · ');
return '<article class="module-row"><div><h3>' + escapeHtml(item.packageName) +
'</h3><p>' + escapeHtml(details) + '</p></div><span class="pill">' +
PAY_STATUS[item.payStatus] + '</span>' +
(item.canRefreshPayment && item.transactionId ? '<button class="btn outline" type="button" data-vip-payment-refresh="' + escapeHtml(item.transactionId) + '">查询支付</button>' : '') +
(item.canClose && item.transactionId ? '<button class="btn outline" type="button" data-vip-payment-close="' + escapeHtml(item.transactionId) + '">关闭支付</button>' : '') + '</article>';
}).join('');
}
function shouldRedirectToLogin(api, error) {
var status = error && (error.status || error.code);
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
}
function redirectToLogin(api) {
if (api && api.clearToken) api.clearToken();
if (!root.location) return;
if (typeof root.location.replace === 'function') root.location.replace('login.html');
else root.location.href = 'login.html';
}
async function init() {
var page = root.document && root.document.querySelector('[data-vip-page]');
var packageList = root.document && root.document.querySelector('[data-vip-package-list]');
var selectedText = root.document && root.document.querySelector('[data-vip-selected-package]');
var genealogySelect = root.document && root.document.querySelector('[data-vip-genealogy-options]');
var form = root.document && root.document.querySelector('[data-vip-order-form]');
var submit = root.document && root.document.querySelector('[data-vip-order-submit]');
var status = root.document && root.document.querySelector('[data-vip-order-status]');
var refresh = root.document && root.document.querySelector('[data-vip-order-refresh]');
var orderList = root.document && root.document.querySelector('[data-vip-order-list]');
var paymentPanel = root.document && root.document.querySelector('[data-vip-payment-panel]');
var paymentQr = root.document && root.document.querySelector('[data-vip-payment-qr]');
var paymentText = root.document && root.document.querySelector('[data-vip-payment-text]');
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
var packages = [];
var selectedPackageId = '';
var contextGenealogyId = root.ProfileUI && typeof root.ProfileUI.getGenealogyId === 'function'
? root.ProfileUI.getGenealogyId()
: '';
var writePending = false;
var purchaseEnabled = false;
if (!page) return;
if (shouldRedirectToLogin(api)) {
redirectToLogin(api);
return;
}
if (!packageList || !selectedText || !genealogySelect || !form || !orderList ||
!api.vipCapability || !api.vipPackages || !api.genealogiesMine || !api.vipOrders || !api.createVipOrder) {
setListState(packageList, 'error', '会员套餐服务初始化失败,请刷新后重试。');
setListState(orderList, 'error', '会员订单服务初始化失败,请刷新后重试。');
setStatus('error', '会员服务接口初始化失败,请刷新后重试。');
return;
}
function setApiState(target, type, message) {
if (!target) return;
if (root.ProfileUI && root.ProfileUI.setApiState && type) {
root.ProfileUI.setApiState(target, type, message);
return;
}
target.textContent = message || '';
}
function setStatus(type, message) {
setApiState(status, type, message);
}
function setListState(target, type, message) {
setApiState(target, type, message);
}
function setPendingButton(button, pending, label) {
if (!button) return;
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent;
button.disabled = pending;
button.textContent = pending ? label : button.dataset.defaultLabel;
}
function renderPackages() {
if (!packages.length) {
setListState(packageList, 'empty', '当前没有可用会员套餐。');
return;
}
packageList.innerHTML = renderVipPackages(packages, selectedPackageId);
}
function renderOrders(orders) {
var items = normalizeVipOrders(orders);
if (!items.length) {
setListState(orderList, 'empty', '当前没有会员订单。');
return;
}
orderList.innerHTML = renderVipOrders(items);
}
async function loadOrders() {
var orders = normalizeVipOrders(await api.vipOrders());
renderOrders(orders);
return orders;
}
function showPayment(payment) {
if (!paymentPanel || !paymentQr || !paymentText) return;
paymentPanel.hidden = false;
paymentQr.innerHTML = '';
if (typeof root.QRCode === 'function') {
new root.QRCode(paymentQr, { text: payment.codeUrl, width: 220, height: 220 });
} else {
paymentQr.textContent = payment.codeUrl;
}
paymentText.textContent = '请使用微信扫码支付' + (payment.expiresAt ? ',支付单有效期至 ' + payment.expiresAt : '');
paymentPanel.setAttribute('data-transaction-id', payment.transactionId);
}
setListState(packageList, 'loading', '正在读取会员套餐…');
setListState(orderList, 'loading', '正在读取会员订单…');
setStatus('loading', '正在读取会员服务…');
try {
var initial = await Promise.all([api.vipCapability(), api.vipPackages(), api.genealogiesMine(), api.vipOrders()]);
var capability = normalizeVipCapability(initial[0]);
var genealogyOptions = normalizeGenealogyOptions(initial[2]);
var selectedContextGenealogyId = resolveContextGenealogyId(genealogyOptions, contextGenealogyId);
purchaseEnabled = capability.enabled;
packages = normalizeVipPackages(initial[1]);
renderPackages();
genealogySelect.innerHTML = renderGenealogyOptions(genealogyOptions);
if (selectedContextGenealogyId) genealogySelect.value = selectedContextGenealogyId;
renderOrders(initial[3]);
submit.disabled = !purchaseEnabled;
setStatus(purchaseEnabled ? '' : 'forbidden', purchaseEnabled && packages.length
? (selectedContextGenealogyId ? '已关联当前家谱,请选择会员套餐' : '请选择会员套餐')
: (capability.disabledReason || '当前暂不能使用微信扫码支付'));
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setListState(packageList, 'error', '会员套餐读取失败,请稍后重试。');
setListState(orderList, 'error', '会员订单读取失败,请稍后重试。');
setStatus('error', error.message || '会员服务读取失败,请稍后重试。');
return;
}
packageList.addEventListener('click', function (event) {
var button = event.target.closest('[data-vip-package-id]');
var packageId;
var selected;
if (!button || !packageList.contains(button)) return;
packageId = normalizeId(button.getAttribute('data-vip-package-id'));
selected = packages.find(function (item) { return item.packageId === packageId; });
if (!selected) return;
selectedPackageId = selected.packageId;
selectedText.textContent = '已选择:' + selected.packageName + '(¥' + selected.price + '';
renderPackages();
setStatus('', '可以创建会员订单');
});
if (refresh) {
refresh.addEventListener('click', async function () {
if (writePending) return;
setPendingButton(refresh, true, '正在刷新…');
setStatus('loading', '正在刷新会员订单…');
try {
await loadOrders();
setStatus('', '会员订单已刷新');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus('error', error.message || '会员订单刷新失败,请稍后重试。');
} finally {
setPendingButton(refresh, false, '正在刷新…');
}
});
}
form.addEventListener('submit', async function (event) {
var body;
var validation;
var payment;
var created;
var refreshed;
var verified;
event.preventDefault();
if (writePending) return;
if (!purchaseEnabled) return setStatus('forbidden', '当前暂不能创建支付订单');
body = buildVipOrderBody({
packageId: selectedPackageId,
genealogyId: genealogySelect.value
});
validation = validateVipOrderBody(body);
if (validation) {
setStatus('error', validation);
return;
}
writePending = true;
setPendingButton(submit, true, '正在创建订单…');
setStatus('loading', '正在创建会员订单…');
try {
payment = normalizePaymentOrder(await api.createVipOrder(body));
if (!payment) throw new Error('创建响应缺少有效微信扫码支付信息');
created = payment.order;
showPayment(payment);
refreshed = normalizeVipOrders(await api.vipOrders());
verified = refreshed.find(function (item) { return item.orderId === created.orderId; });
if (!verified) throw new Error('订单已提交,但重新读取未找到对应记录');
renderOrders(refreshed);
setStatus('', '支付单已创建,请使用微信扫码支付。');
} catch (error) {
if (shouldRedirectToLogin(api, error)) {
redirectToLogin(api);
return;
}
setStatus('error', error.message || '会员订单创建失败,请稍后重试。');
} finally {
writePending = false;
setPendingButton(submit, false, '正在创建订单…');
}
});
orderList.addEventListener('click', async function (event) {
var refreshButton = event.target.closest('[data-vip-payment-refresh]');
var closeButton = event.target.closest('[data-vip-payment-close]');
var transactionId;
var paymentStatus;
if (writePending || !refreshButton && !closeButton) return;
transactionId = (refreshButton || closeButton).getAttribute(refreshButton ? 'data-vip-payment-refresh' : 'data-vip-payment-close');
writePending = true;
try {
if (closeButton) {
if (!await confirmAction('确认关闭这笔支付吗?')) return;
await api.closeVipPayment(transactionId);
setStatus('', '支付单已关闭');
} else {
paymentStatus = normalizePaymentStatus(await api.vipPaymentStatus(transactionId));
if (!paymentStatus) throw new Error('支付状态响应无效');
setStatus('', paymentStatus.status === 'SUCCESS' ? '支付成功' : '当前支付状态:' + paymentStatus.status);
}
await loadOrders();
} catch (error) {
setStatus('error', error.message || '支付状态操作失败');
} finally { writePending = false; }
});
}
return {
normalizeVipPackage: normalizeVipPackage,
normalizeVipPackages: normalizeVipPackages,
normalizeGenealogyOption: normalizeGenealogyOption,
normalizeGenealogyOptions: normalizeGenealogyOptions,
resolveContextGenealogyId: resolveContextGenealogyId,
buildVipOrderBody: buildVipOrderBody,
validateVipOrderBody: validateVipOrderBody,
normalizeVipCapability: normalizeVipCapability,
normalizeVipOrder: normalizeVipOrder,
normalizeVipOrders: normalizeVipOrders,
normalizePaymentOrder: normalizePaymentOrder,
normalizePaymentStatus: normalizePaymentStatus,
renderVipPackages: renderVipPackages,
renderGenealogyOptions: renderGenealogyOptions,
renderVipOrders: renderVipOrders,
init: init
};
});