feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
(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';
|
||||
|
||||
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, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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 buildVipOrderBody(values) {
|
||||
var source = values || {};
|
||||
var body = {};
|
||||
var packageId = normalizeId(source.packageId);
|
||||
var genealogyId = normalizeId(source.genealogyId);
|
||||
var payType = optionalText(source.payType);
|
||||
|
||||
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;
|
||||
if (payType) body.payType = payType;
|
||||
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 '家谱选项无效';
|
||||
}
|
||||
if (body.payType && body.payType !== 'wechat') return '支付方式无效';
|
||||
return '';
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
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 = Array.isArray(data) ? data.map(normalizeGenealogyOption).filter(Boolean) : [];
|
||||
|
||||
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></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 api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var packages = [];
|
||||
var selectedPackageId = '';
|
||||
var writePending = false;
|
||||
|
||||
if (!page) return;
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
if (!packageList || !selectedText || !genealogySelect || !form || !orderList ||
|
||||
!api.vipPackages || !api.genealogiesMine || !api.vipOrders || !api.createVipOrder) {
|
||||
if (status) status.textContent = '会员服务接口初始化失败,请刷新后重试';
|
||||
return;
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
if (status) status.textContent = message;
|
||||
}
|
||||
|
||||
function renderPackages() {
|
||||
packageList.innerHTML = renderVipPackages(packages, selectedPackageId);
|
||||
}
|
||||
|
||||
function renderOrders(orders) {
|
||||
orderList.innerHTML = renderVipOrders(orders);
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
var orders = normalizeVipOrders(await api.vipOrders());
|
||||
renderOrders(orders);
|
||||
return orders;
|
||||
}
|
||||
|
||||
try {
|
||||
var initial = await Promise.all([api.vipPackages(), api.genealogiesMine(), api.vipOrders()]);
|
||||
packages = normalizeVipPackages(initial[0]);
|
||||
renderPackages();
|
||||
genealogySelect.innerHTML = renderGenealogyOptions(initial[1]);
|
||||
renderOrders(initial[2]);
|
||||
setStatus(packages.length ? '请选择会员套餐' : '当前没有可用会员套餐');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
packageList.innerHTML = '<div class="api-empty">会员套餐读取失败,请稍后重试</div>';
|
||||
orderList.innerHTML = '<div class="api-empty">会员订单读取失败,请稍后重试</div>';
|
||||
setStatus(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;
|
||||
refresh.disabled = true;
|
||||
setStatus('正在刷新订单…');
|
||||
try {
|
||||
await loadOrders();
|
||||
setStatus('会员订单已刷新');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '会员订单刷新失败,请稍后重试');
|
||||
} finally {
|
||||
refresh.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body;
|
||||
var validation;
|
||||
var created;
|
||||
var refreshed;
|
||||
var verified;
|
||||
|
||||
event.preventDefault();
|
||||
if (writePending) return;
|
||||
body = buildVipOrderBody({
|
||||
packageId: selectedPackageId,
|
||||
genealogyId: genealogySelect.value
|
||||
});
|
||||
validation = validateVipOrderBody(body);
|
||||
if (validation) {
|
||||
setStatus(validation);
|
||||
return;
|
||||
}
|
||||
|
||||
writePending = true;
|
||||
if (submit) submit.disabled = true;
|
||||
setStatus('正在创建会员订单…');
|
||||
try {
|
||||
created = normalizeVipOrder(await api.createVipOrder(body));
|
||||
if (!created) throw new Error('创建响应缺少有效会员订单信息');
|
||||
refreshed = normalizeVipOrders(await api.vipOrders());
|
||||
verified = refreshed.find(function (item) { return item.orderId === created.orderId; });
|
||||
if (!verified) throw new Error('订单已提交,但重新读取未找到对应记录');
|
||||
renderOrders(refreshed);
|
||||
setStatus('订单已创建;当前 PC 暂未开放在线支付');
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || '会员订单创建失败,请稍后重试');
|
||||
} finally {
|
||||
writePending = false;
|
||||
if (submit) submit.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeVipPackage: normalizeVipPackage,
|
||||
normalizeVipPackages: normalizeVipPackages,
|
||||
normalizeGenealogyOption: normalizeGenealogyOption,
|
||||
buildVipOrderBody: buildVipOrderBody,
|
||||
validateVipOrderBody: validateVipOrderBody,
|
||||
normalizeVipOrder: normalizeVipOrder,
|
||||
normalizeVipOrders: normalizeVipOrders,
|
||||
renderVipPackages: renderVipPackages,
|
||||
renderGenealogyOptions: renderGenealogyOptions,
|
||||
renderVipOrders: renderVipOrders,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user