408 lines
14 KiB
JavaScript
408 lines
14 KiB
JavaScript
(function (root, factory) {
|
|
// 家谱模块脚本同时支持浏览器运行和 Node 单元测试。
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.GenealogyPages = factory(root);
|
|
if (root.document) {
|
|
root.document.addEventListener('DOMContentLoaded', function () {
|
|
root.GenealogyPages.init();
|
|
});
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
|
'use strict';
|
|
|
|
var documentRef = root.document;
|
|
|
|
function normalizeList(data) {
|
|
// 列表接口可能返回数组、rows、records 或 list,这里统一成数组供页面渲染。
|
|
if (Array.isArray(data)) return data;
|
|
if (!data || typeof data !== 'object') return [];
|
|
if (Array.isArray(data.rows)) return data.rows;
|
|
if (Array.isArray(data.records)) return data.records;
|
|
if (Array.isArray(data.list)) return data.list;
|
|
if (Array.isArray(data.data)) return data.data;
|
|
return [];
|
|
}
|
|
|
|
function pick(item, keys, fallback) {
|
|
// 后端字段名存在少量差异时,按优先级取第一个可用字段。
|
|
var source = item || {};
|
|
var index;
|
|
var value;
|
|
|
|
for (index = 0; index < keys.length; index += 1) {
|
|
value = source[keys[index]];
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
|
}
|
|
|
|
return fallback || '';
|
|
}
|
|
|
|
function buildGenealogyBody(values) {
|
|
// 创建家谱接口必填 genealogyName、surname、regionCode,地区编码由地区选择器回填。
|
|
var source = values || {};
|
|
|
|
return {
|
|
genealogyName: source.genealogyName || '',
|
|
surname: source.surname || '',
|
|
regionCode: source.regionCode || '',
|
|
addressDetail: source.addressDetail || '',
|
|
intro: source.intro || '',
|
|
visibility: source.visibility || '1',
|
|
joinMode: source.joinMode || '1'
|
|
};
|
|
}
|
|
|
|
function getApi() {
|
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
}
|
|
|
|
function query(selector, rootNode) {
|
|
return (rootNode || documentRef).querySelector(selector);
|
|
}
|
|
|
|
function queryAll(selector, rootNode) {
|
|
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
|
|
}
|
|
|
|
function readForm(form) {
|
|
// 页面表单 name 与接口字段对齐,后续加字段只需要补 HTML。
|
|
var values = {};
|
|
|
|
queryAll('input[name], textarea[name], select[name]', form).forEach(function (field) {
|
|
values[field.name] = String(field.value || '').trim();
|
|
});
|
|
|
|
return values;
|
|
}
|
|
|
|
function getQueryParam(search, name) {
|
|
// 详情页和加入页用 query 参数传递 genealogyId,测试环境也可直接传 search 字符串。
|
|
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
|
return params.get(name) || '';
|
|
}
|
|
|
|
function getCurrentGenealogyId() {
|
|
var search = root.location && root.location.search;
|
|
return getQueryParam(search, 'id') || getQueryParam(search, 'genealogyId');
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
// 接口内容进入 innerHTML 前统一转义,避免公开页面渲染异常内容。
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function setBusy(button, busy) {
|
|
// 加载状态只加 class,样式统一写在 CSS 中。
|
|
if (!button) return;
|
|
button.classList.toggle('is-loading', busy);
|
|
if ('disabled' in button) button.disabled = busy;
|
|
button.setAttribute('aria-busy', busy ? 'true' : 'false');
|
|
}
|
|
|
|
function showMessage(message) {
|
|
if (root.layui && root.layui.layer) {
|
|
root.layui.layer.msg(message);
|
|
return;
|
|
}
|
|
|
|
root.alert(message);
|
|
}
|
|
|
|
function requireValue(value, message) {
|
|
if (value) return true;
|
|
showMessage(message);
|
|
return false;
|
|
}
|
|
|
|
function buildFamilyHeroTitle(detail) {
|
|
return pick(detail, ['genealogyName', 'name', 'title'], '未命名家谱');
|
|
}
|
|
|
|
function formatJoinMode(value) {
|
|
var map = {
|
|
0: '关闭加入',
|
|
1: '审核加入',
|
|
2: '邀请码加入'
|
|
};
|
|
|
|
return map[value] || value || '多人共修';
|
|
}
|
|
|
|
function buildFamilySummary(detail) {
|
|
// 后端详情字段可能来自统计字段或普通展示字段,这里集中做字段映射。
|
|
var memberCount = pick(detail, ['memberCount', 'membersCount'], '0');
|
|
var generationCount = pick(detail, ['generationCount', 'generationTotal'], '0');
|
|
var articleCount = pick(detail, ['articleCount', 'articlesCount'], '0');
|
|
var albumCount = pick(detail, ['albumCount', 'albumsCount'], '0');
|
|
|
|
return [
|
|
{ label: '成员', value: memberCount },
|
|
{ label: '世代', value: generationCount },
|
|
{ label: '谱文', value: articleCount },
|
|
{ label: '相册', value: albumCount }
|
|
];
|
|
}
|
|
|
|
function buildJoinApplyBody(values) {
|
|
// 接口文档要求 applicantName、phone、relationDesc、applyReason,可选 inviterUserId。
|
|
var source = values || {};
|
|
var inviterUserId = source.inviterUserId ? Number(source.inviterUserId) : undefined;
|
|
|
|
return {
|
|
applicantName: String(source.applicantName || '').trim(),
|
|
phone: String(source.phone || '').trim(),
|
|
relationDesc: String(source.relationDesc || '').trim(),
|
|
applyReason: String(source.applyReason || '').trim() || '申请加入家谱',
|
|
inviterUserId: inviterUserId
|
|
};
|
|
}
|
|
|
|
function renderPublicGenealogies(items) {
|
|
// 广场页复用现有卡片结构,只替换接口返回的数据。
|
|
var container = query('[data-genealogy-list="public"]');
|
|
var html;
|
|
|
|
if (!container) return;
|
|
if (!items.length) {
|
|
container.innerHTML = '<div class="api-empty">暂无公开家谱</div>';
|
|
return;
|
|
}
|
|
|
|
html = items.slice(0, 6).map(function (item, index) {
|
|
// 第一条沿用推荐卡片样式,其余走普通家谱卡片样式。
|
|
var id = pick(item, ['genealogyId', 'id'], '');
|
|
var name = pick(item, ['genealogyName', 'name', 'title'], '未命名家谱');
|
|
var surname = pick(item, ['surname'], '家谱');
|
|
var region = pick(item, ['addressDetail', 'regionName', 'originPlace'], '地区待完善');
|
|
var intro = pick(item, ['intro', 'description', 'summary'], '家谱资料正在完善中。');
|
|
var className = index === 0 ? 'family-card featured tilt-card' : 'card family-lite tilt-card';
|
|
|
|
return '<a class="' + className + '" href="family-detail.html?id=' + encodeURIComponent(id) + '">' +
|
|
'<h3>' + name + '</h3>' +
|
|
'<p>' + intro + '</p>' +
|
|
'<div class="tag-row"><span class="tag">' + region + '</span><span class="tag">' + surname + '</span></div>' +
|
|
'</a>';
|
|
}).join('');
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function buildMyGenealogyRow(item) {
|
|
// 我的家谱列表保持 profile-module.css 的 module-row 结构。
|
|
var id = pick(item, ['genealogyId', 'id'], '');
|
|
var name = pick(item, ['genealogyName', 'name'], '未命名家谱');
|
|
var role = pick(item, ['roleName', 'memberRole'], '成员');
|
|
var count = pick(item, ['memberCount', 'membersCount'], '0');
|
|
|
|
return '<a class="module-row" href="profile-family-home.html?id=' + encodeURIComponent(id) + '">' +
|
|
'<div><h3>' + name + '</h3><p>' + role + ' · 共修入 ' + count + ' 人</p></div>' +
|
|
'<span class="pill">进入主页</span></a>';
|
|
}
|
|
|
|
function renderMyGenealogies(items) {
|
|
// 没有数据时显示公共空状态,避免保留旧静态示例误导用户。
|
|
var container = query('[data-genealogy-list="mine"]');
|
|
|
|
if (!container) return;
|
|
if (!items.length) {
|
|
container.innerHTML = '<div class="api-empty">你还没有创建或加入家谱</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = items.map(buildMyGenealogyRow).join('');
|
|
}
|
|
|
|
function renderFamilyDetail(detail) {
|
|
var page = query('[data-family-detail]');
|
|
var summaryGrid = query('[data-family-summary]');
|
|
var joinLink = query('[data-family-join-link]');
|
|
var id;
|
|
|
|
if (!page) return;
|
|
|
|
id = pick(detail, ['genealogyId', 'id'], getCurrentGenealogyId());
|
|
|
|
queryAll('[data-family-text]').forEach(function (node) {
|
|
var field = node.getAttribute('data-family-text');
|
|
var fallback = node.getAttribute('data-fallback') || '';
|
|
var value = field === 'title'
|
|
? buildFamilyHeroTitle(detail)
|
|
: pick(detail, field.split('|'), fallback);
|
|
|
|
node.textContent = field.indexOf('joinMode') !== -1 ? formatJoinMode(value) : value;
|
|
});
|
|
|
|
if (summaryGrid) {
|
|
summaryGrid.innerHTML = buildFamilySummary(detail).map(function (item) {
|
|
return '<span><b>' + escapeHtml(item.value) + '</b>' + escapeHtml(item.label) + '</span>';
|
|
}).join('');
|
|
}
|
|
|
|
if (joinLink && id) {
|
|
joinLink.href = 'join-genealogy.html?id=' + encodeURIComponent(id);
|
|
}
|
|
}
|
|
|
|
async function loadFamilyDetail() {
|
|
// 只有详情页存在 data-family-detail,且 URL 带 id/genealogyId 时才请求接口。
|
|
var api = getApi();
|
|
var page = query('[data-family-detail]');
|
|
var genealogyId = getCurrentGenealogyId();
|
|
|
|
if (!api || !page) return;
|
|
if (!genealogyId) {
|
|
showMessage('缺少家谱ID');
|
|
return;
|
|
}
|
|
|
|
page.classList.add('is-loading');
|
|
try {
|
|
renderFamilyDetail(await api.genealogyDetail(genealogyId));
|
|
} catch (error) {
|
|
showMessage(error.message || '家谱详情加载失败');
|
|
} finally {
|
|
page.classList.remove('is-loading');
|
|
}
|
|
}
|
|
|
|
async function submitJoinApply(form, button) {
|
|
var api = getApi();
|
|
var values = readForm(form);
|
|
var genealogyId = values.genealogyId || getCurrentGenealogyId();
|
|
var body = buildJoinApplyBody(values);
|
|
|
|
if (!api) return;
|
|
if (!requireValue(genealogyId, '请填写家谱ID或邀请码')) return;
|
|
if (!requireValue(body.applicantName, '请填写你的姓名')) return;
|
|
if (!requireValue(body.phone, '请填写手机号')) return;
|
|
if (!requireValue(body.relationDesc, '请填写关系说明')) return;
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.applyJoinGenealogy(genealogyId, body);
|
|
showMessage('申请已提交,请等待管理员审核');
|
|
root.location.href = form.getAttribute('data-success-url') || 'profile-join-family.html';
|
|
} catch (error) {
|
|
showMessage(error.message || '申请提交失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
async function loadPublicGenealogies() {
|
|
// 只有带 data-genealogy-list="public" 的页面才会真正发起请求。
|
|
var api = getApi();
|
|
var container = query('[data-genealogy-list="public"]');
|
|
|
|
if (!api || !container) return;
|
|
container.classList.add('is-loading');
|
|
try {
|
|
renderPublicGenealogies(normalizeList(await api.publicGenealogies()));
|
|
} catch (error) {
|
|
showMessage(error.message || '公开家谱加载失败');
|
|
} finally {
|
|
container.classList.remove('is-loading');
|
|
}
|
|
}
|
|
|
|
async function loadMyGenealogies() {
|
|
// 只有个人中心“我的家谱”页面会命中这个容器。
|
|
var api = getApi();
|
|
var container = query('[data-genealogy-list="mine"]');
|
|
|
|
if (!api || !container) return;
|
|
container.classList.add('is-loading');
|
|
try {
|
|
renderMyGenealogies(normalizeList(await api.myGenealogies()));
|
|
} catch (error) {
|
|
showMessage(error.message || '我的家谱加载失败');
|
|
} finally {
|
|
container.classList.remove('is-loading');
|
|
}
|
|
}
|
|
|
|
async function submitGenealogy(form, button) {
|
|
// 创建成功后跳到我的家谱列表,后续继续维护成员和内容。
|
|
var api = getApi();
|
|
var body = buildGenealogyBody(readForm(form));
|
|
|
|
if (!api) return;
|
|
if (!requireValue(body.genealogyName, '请填写家谱名称')) return;
|
|
if (!requireValue(body.surname, '请填写主要姓氏')) return;
|
|
if (!requireValue(body.regionCode, '请选择家族所在地')) return;
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.createGenealogy(body);
|
|
showMessage('家谱创建成功');
|
|
root.location.href = form.getAttribute('data-success-url') || 'profile-families.html';
|
|
} catch (error) {
|
|
showMessage(error.message || '家谱创建失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
function bindCreateForm() {
|
|
// 创建表单用 submit 事件,兼容按钮点击和键盘回车提交。
|
|
var form = query('[data-genealogy-form="create"]');
|
|
|
|
if (!form) return;
|
|
form.addEventListener('submit', function (event) {
|
|
event.preventDefault();
|
|
submitGenealogy(form, query('[type="submit"]', form));
|
|
});
|
|
}
|
|
|
|
function bindJoinForm() {
|
|
// 加入页用同一个 submit 入口,兼容详情页带 id 跳转和用户手动输入家谱ID。
|
|
var form = query('[data-genealogy-form="join"]');
|
|
var idField;
|
|
var currentId;
|
|
|
|
if (!form) return;
|
|
|
|
idField = query('[name="genealogyId"]', form);
|
|
currentId = getCurrentGenealogyId();
|
|
if (idField && currentId && !idField.value) idField.value = currentId;
|
|
|
|
form.addEventListener('submit', function (event) {
|
|
event.preventDefault();
|
|
submitJoinApply(form, query('[type="submit"]', form));
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
// 一个模块脚本服务多个家谱页面,通过 data 属性决定当前页面要做什么。
|
|
if (!documentRef) return;
|
|
|
|
bindCreateForm();
|
|
bindJoinForm();
|
|
loadPublicGenealogies();
|
|
loadMyGenealogies();
|
|
loadFamilyDetail();
|
|
}
|
|
|
|
return {
|
|
normalizeList: normalizeList,
|
|
pick: pick,
|
|
buildGenealogyBody: buildGenealogyBody,
|
|
getQueryParam: getQueryParam,
|
|
buildFamilyHeroTitle: buildFamilyHeroTitle,
|
|
buildFamilySummary: buildFamilySummary,
|
|
formatJoinMode: formatJoinMode,
|
|
buildJoinApplyBody: buildJoinApplyBody,
|
|
buildMyGenealogyRow: buildMyGenealogyRow,
|
|
init: init
|
|
};
|
|
});
|