feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
+466
-111
@@ -1,5 +1,4 @@
|
||||
(function (root, factory) {
|
||||
// 成长记录页面只消费 ApiClient 中已在 Apifox PC 目录核验的方法。
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
@@ -15,6 +14,7 @@
|
||||
'use strict';
|
||||
|
||||
var documentRef = root.document;
|
||||
var recordsById = Object.create(null);
|
||||
var writePending = false;
|
||||
|
||||
function getApi() {
|
||||
@@ -29,37 +29,70 @@
|
||||
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
|
||||
}
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function toSafeIntegerOrUndefined(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
var number;
|
||||
|
||||
if (!text) return undefined;
|
||||
number = Number(text);
|
||||
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
|
||||
}
|
||||
|
||||
function getQueryParam(search, name) {
|
||||
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
||||
|
||||
return params.get(name) || '';
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
return /^[1-9][0-9]*$/.test(String(value)) ? String(value) : '';
|
||||
}
|
||||
|
||||
function getCurrentGenealogyId(search) {
|
||||
var page = query('[data-growth-page], [data-growth-edit-page]');
|
||||
var source = search === undefined && root.location ? root.location.search : search;
|
||||
var profileGenealogyId;
|
||||
var fromProfile;
|
||||
|
||||
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
|
||||
profileGenealogyId = root.ProfileUI.getGenealogyId();
|
||||
if (profileGenealogyId) return profileGenealogyId;
|
||||
fromProfile = root.ProfileUI.getGenealogyId();
|
||||
if (fromProfile) return normalizeId(fromProfile);
|
||||
}
|
||||
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
|
||||
return normalizeId(getQueryParam(source, 'genealogyId'));
|
||||
}
|
||||
|
||||
function getCurrentRecordId(search) {
|
||||
var source = search === undefined && root.location ? root.location.search : search;
|
||||
|
||||
return normalizeId(getQueryParam(source, 'recordId'));
|
||||
}
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function toSafeInteger(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
var number;
|
||||
|
||||
if (text === undefined) return undefined;
|
||||
number = Number(text);
|
||||
return Number.isSafeInteger(number) ? number : undefined;
|
||||
}
|
||||
|
||||
function toBackendDateTime(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
|
||||
if (!text) return undefined;
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ') + ':00';
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(text)) return text.replace('T', ' ');
|
||||
return text;
|
||||
}
|
||||
|
||||
function toDateInputValue(value) {
|
||||
var text = String(value || '');
|
||||
|
||||
return /^\d{4}-\d{2}-\d{2}/.test(text) ? text.slice(0, 10) : '';
|
||||
}
|
||||
|
||||
function toDateTimeInputValue(value) {
|
||||
var text = String(value || '');
|
||||
|
||||
if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) return '';
|
||||
return text.slice(0, 16).replace(' ', 'T');
|
||||
}
|
||||
|
||||
function buildGrowthRecordBody(values) {
|
||||
@@ -71,10 +104,10 @@
|
||||
var recordType = trimOrUndefined(source.recordType);
|
||||
var recordContent = trimOrUndefined(source.recordContent);
|
||||
var recordDate = trimOrUndefined(source.recordDate);
|
||||
var remindTime = trimOrUndefined(source.remindTime);
|
||||
var remindTime = toBackendDateTime(source.remindTime);
|
||||
var mediaOssIds = trimOrUndefined(source.mediaOssIds);
|
||||
var status = trimOrUndefined(source.status);
|
||||
var sortOrderText = trimOrUndefined(source.sortOrder);
|
||||
var status = trimOrUndefined(source.status);
|
||||
|
||||
if (lineagePersonId !== undefined) body.lineagePersonId = lineagePersonId;
|
||||
if (recordType !== undefined) body.recordType = recordType;
|
||||
@@ -82,27 +115,105 @@
|
||||
if (recordDate !== undefined) body.recordDate = recordDate;
|
||||
if (remindTime !== undefined) body.remindTime = remindTime;
|
||||
if (mediaOssIds !== undefined) body.mediaOssIds = mediaOssIds;
|
||||
if (sortOrderText !== undefined) body.sortOrder = toSafeIntegerOrUndefined(sortOrderText);
|
||||
if (sortOrderText !== undefined) {
|
||||
body.sortOrder = toSafeInteger(sortOrderText);
|
||||
if (body.sortOrder === undefined) body.invalidSortOrder = true;
|
||||
}
|
||||
if (status !== undefined) body.status = status;
|
||||
return body;
|
||||
}
|
||||
|
||||
function validateGrowthRecordBody(body) {
|
||||
if (!body || !body.recordTitle) return '请填写记录标题';
|
||||
if (body.lineagePersonId !== undefined && !/^\d+$/.test(body.lineagePersonId)) return '世系人物 ID 必须是整数';
|
||||
if (body.lineagePersonId !== undefined && !normalizeId(body.lineagePersonId)) return '请选择有效的世系人物';
|
||||
if (body.mediaOssIds !== undefined && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(body.mediaOssIds)) {
|
||||
return '附件 OSS ID 请使用英文逗号分隔的正整数';
|
||||
return '附件上传结果无效';
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && body.sortOrder === undefined) return '排序值必须是安全整数';
|
||||
if (body.invalidSortOrder || (Object.prototype.hasOwnProperty.call(body, 'sortOrder') && !Number.isSafeInteger(body.sortOrder))) {
|
||||
return '排序值必须是安全整数';
|
||||
}
|
||||
if (body.recordDate !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(body.recordDate)) return '记录日期格式无效';
|
||||
if (body.remindTime !== undefined && !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(body.remindTime)) {
|
||||
return '提醒时间格式无效';
|
||||
}
|
||||
if (body.status === '1') return '当前 PC 无法重新读取停用记录,暂不开放停用';
|
||||
if (body.status !== undefined && body.status !== '0') return '记录状态只能是 0';
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeGrowthList(data) {
|
||||
return Array.isArray(data) ? data : [];
|
||||
function stringValue(value) {
|
||||
return value === undefined || value === null ? '' : String(value);
|
||||
}
|
||||
|
||||
function optionalSafeInteger(value) {
|
||||
var number;
|
||||
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
number = Number(value);
|
||||
return Number.isSafeInteger(number) ? number : undefined;
|
||||
}
|
||||
|
||||
function normalizeGrowthRecord(item) {
|
||||
var recordId = normalizeId(item && item.recordId);
|
||||
var genealogyId = normalizeId(item && item.genealogyId);
|
||||
var appUserId = normalizeId(item && item.appUserId);
|
||||
var lineagePersonId = normalizeId(item && item.lineagePersonId);
|
||||
var title = String(item && item.recordTitle || '').trim();
|
||||
var status = stringValue(item && item.status);
|
||||
var mediaOssIds = stringValue(item && item.mediaOssIds);
|
||||
var sortOrder = optionalSafeInteger(item && item.sortOrder);
|
||||
var record;
|
||||
|
||||
if (!recordId || !genealogyId || !title || (status !== '0' && status !== '1')) return null;
|
||||
if (item.appUserId !== undefined && item.appUserId !== null && item.appUserId !== '' && !appUserId) return null;
|
||||
if (item.lineagePersonId !== undefined && item.lineagePersonId !== null && item.lineagePersonId !== '' && !lineagePersonId) return null;
|
||||
if (mediaOssIds && !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(mediaOssIds)) return null;
|
||||
if (item.sortOrder !== undefined && item.sortOrder !== null && item.sortOrder !== '' && sortOrder === undefined) return null;
|
||||
record = {
|
||||
recordId: recordId,
|
||||
genealogyId: genealogyId,
|
||||
genealogyNo: stringValue(item.genealogyNo),
|
||||
genealogyName: stringValue(item.genealogyName),
|
||||
surname: stringValue(item.surname),
|
||||
appUserNickName: stringValue(item.appUserNickName),
|
||||
appUserPhone: stringValue(item.appUserPhone),
|
||||
lineagePersonNo: stringValue(item.lineagePersonNo),
|
||||
lineagePersonName: stringValue(item.lineagePersonName),
|
||||
recordType: stringValue(item.recordType),
|
||||
recordTitle: title,
|
||||
recordContent: stringValue(item.recordContent),
|
||||
recordDate: stringValue(item.recordDate),
|
||||
remindTime: stringValue(item.remindTime),
|
||||
mediaOssIds: mediaOssIds,
|
||||
status: status,
|
||||
remark: stringValue(item.remark)
|
||||
};
|
||||
if (appUserId) record.appUserId = appUserId;
|
||||
if (lineagePersonId) record.lineagePersonId = lineagePersonId;
|
||||
if (sortOrder !== undefined) record.sortOrder = sortOrder;
|
||||
return record;
|
||||
}
|
||||
|
||||
function matchesSavedRecord(item, expectedRecordId) {
|
||||
var record = normalizeGrowthRecord(item);
|
||||
|
||||
return Boolean(record && record.recordId === normalizeId(expectedRecordId));
|
||||
}
|
||||
|
||||
function normalizeLineageOption(item) {
|
||||
var personId = normalizeId(item && (item.personId || item.lineagePersonId));
|
||||
var name = String(item && (item.name || item.lineagePersonName) || '').trim();
|
||||
|
||||
if (!personId || !name) return null;
|
||||
return {
|
||||
personId: personId,
|
||||
name: name,
|
||||
generationName: stringValue(item.generationName)
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
return stringValue(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
@@ -110,32 +221,76 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function stringifyRecord(record) {
|
||||
try {
|
||||
return typeof record === 'string' ? record : JSON.stringify(record);
|
||||
} catch (error) {
|
||||
return String(record);
|
||||
function renderLineageOptions(data, selectedId, fallbackName) {
|
||||
var selected = normalizeId(selectedId);
|
||||
var options = Array.isArray(data) ? data.map(normalizeLineageOption).filter(Boolean) : [];
|
||||
var selectedFound = options.some(function (option) { return option.personId === selected; });
|
||||
|
||||
if (selected && !selectedFound && fallbackName) {
|
||||
options.push({ personId: selected, name: String(fallbackName), generationName: '' });
|
||||
}
|
||||
return '<option value="">不关联世系人物</option>' + options.map(function (option) {
|
||||
return '<option value="' + escapeHtml(option.personId) + '"' +
|
||||
(option.personId === selected ? ' selected' : '') + '>' +
|
||||
escapeHtml(option.name + (option.generationName ? ' · ' + option.generationName : '')) +
|
||||
'</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderGrowthRecords(data) {
|
||||
var container = query('[data-growth-list]');
|
||||
var records = normalizeGrowthList(data);
|
||||
function attachmentCount(mediaOssIds) {
|
||||
return mediaOssIds ? mediaOssIds.split(',').filter(Boolean).length : 0;
|
||||
}
|
||||
|
||||
if (!container) return;
|
||||
if (!Array.isArray(data)) {
|
||||
container.innerHTML = '<div class="api-empty">成长记录响应未按 Apifox ListResult 返回数组,无法安全展示或操作记录。</div>';
|
||||
return;
|
||||
}
|
||||
if (!records.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
|
||||
return;
|
||||
}
|
||||
// ListResult 的元素 DTO 尚未展开,不能假设 recordId、标题或权限字段。
|
||||
container.innerHTML = records.map(function (record, index) {
|
||||
return '<article class="module-row"><div><h3>成长记录 ' + (index + 1) + '</h3><p>' +
|
||||
escapeHtml(stringifyRecord(record)) + '</p></div></article>';
|
||||
}).join('');
|
||||
function buildListUrl(genealogyId, recordId) {
|
||||
var url = 'profile-growth.html?genealogyId=' + encodeURIComponent(genealogyId);
|
||||
|
||||
return recordId ? url + '&recordId=' + encodeURIComponent(recordId) : url;
|
||||
}
|
||||
|
||||
function buildEditUrl(genealogyId, recordId) {
|
||||
var url = 'profile-growth-edit.html?genealogyId=' + encodeURIComponent(genealogyId);
|
||||
|
||||
return recordId ? url + '&recordId=' + encodeURIComponent(recordId) : url;
|
||||
}
|
||||
|
||||
function renderGrowthRow(record) {
|
||||
var summary = [];
|
||||
|
||||
if (record.lineagePersonName) summary.push(record.lineagePersonName);
|
||||
if (record.recordType) summary.push(record.recordType);
|
||||
if (record.recordDate) summary.push(record.recordDate);
|
||||
if (!summary.length) summary.push('未填写日期和关联人物');
|
||||
return '<article class="module-row growth-row"><div><h3>' + escapeHtml(record.recordTitle) + '</h3><p>' +
|
||||
escapeHtml(summary.join(' · ')) + '</p></div><div class="row-actions">' +
|
||||
'<button class="pill" type="button" data-growth-detail-id="' + escapeHtml(record.recordId) + '">查看详情</button>' +
|
||||
'<a class="pill" href="' + escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑</a>' +
|
||||
'<button class="pill is-danger" type="button" data-growth-delete-id="' + escapeHtml(record.recordId) + '">删除</button>' +
|
||||
'</div></article>';
|
||||
}
|
||||
|
||||
function detailValue(label, value) {
|
||||
return '<p><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value || '未提供') + '</b></p>';
|
||||
}
|
||||
|
||||
function renderGrowthDetail(record) {
|
||||
var count;
|
||||
|
||||
if (!record) return '<div class="api-empty">请选择一条成长记录查看详情</div>';
|
||||
count = attachmentCount(record.mediaOssIds);
|
||||
return '<div class="form-like growth-detail">' +
|
||||
detailValue('标题', record.recordTitle) +
|
||||
detailValue('关联人物', record.lineagePersonName || record.lineagePersonNo) +
|
||||
detailValue('记录类型', record.recordType) +
|
||||
detailValue('记录日期', record.recordDate) +
|
||||
detailValue('提醒时间', record.remindTime) +
|
||||
detailValue('记录内容', record.recordContent) +
|
||||
detailValue('附件', count ? count + ' 个附件' : '未上传') +
|
||||
detailValue('记录人', record.appUserNickName) +
|
||||
detailValue('状态', record.status === '0' ? '正常' : '停用') +
|
||||
detailValue('备注', record.remark) +
|
||||
'</div><div class="bottom-actions"><a class="btn ghost" href="' +
|
||||
escapeHtml(buildEditUrl(record.genealogyId, record.recordId)) + '">编辑记录</a>' +
|
||||
'<button class="btn ghost" type="button" data-growth-delete-id="' + escapeHtml(record.recordId) + '">删除记录</button></div>';
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
@@ -152,6 +307,10 @@
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function isForbidden(error) {
|
||||
return Number(error && (error.status || error.code)) === 403;
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
@@ -164,6 +323,133 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncGenealogyLinks() {
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) root.ProfileUI.syncGenealogyContextLinks();
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var status = query('[data-growth-form-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
}
|
||||
|
||||
function setEditorEnabled(enabled, message) {
|
||||
var placeholder = query('[data-growth-editor-placeholder]');
|
||||
|
||||
queryAll('[data-growth-editor], [data-growth-editor-action]').forEach(function (element) {
|
||||
element.hidden = !enabled;
|
||||
});
|
||||
if (placeholder) {
|
||||
placeholder.hidden = Boolean(enabled);
|
||||
if (message) placeholder.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNoContext() {
|
||||
var list = query('[data-growth-list]');
|
||||
var detail = query('[data-growth-detail]');
|
||||
var message = '请先选择家谱,再查看或维护成长记录。';
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">' + message + '</div>';
|
||||
if (detail) detail.innerHTML = '<div class="api-empty">当前没有家谱上下文。</div>';
|
||||
setEditorEnabled(false, message);
|
||||
setFormStatus(message);
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
|
||||
if (!genealogyId) renderNoContext();
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function setWritePending(pending) {
|
||||
writePending = Boolean(pending);
|
||||
queryAll('[data-growth-form] input, [data-growth-form] textarea, [data-growth-form] select, [data-growth-form] button, [data-growth-delete-id]').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
}
|
||||
|
||||
function renderGrowthRecords(data) {
|
||||
var container = query('[data-growth-list]');
|
||||
var normalized = Array.isArray(data) ? data.map(normalizeGrowthRecord) : [];
|
||||
var invalidCount = normalized.filter(function (record) { return !record; }).length;
|
||||
var records = normalized.filter(Boolean);
|
||||
|
||||
recordsById = Object.create(null);
|
||||
records.forEach(function (record) {
|
||||
recordsById[record.recordId] = record;
|
||||
});
|
||||
if (!container) return records;
|
||||
if (!Array.isArray(data) || invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">成长记录响应缺少稳定 GrowthRecordVo 字段,请联系后端核对。</div>';
|
||||
return [];
|
||||
}
|
||||
if (!records.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无成长记录</div>';
|
||||
return [];
|
||||
}
|
||||
container.innerHTML = records.map(renderGrowthRow).join('');
|
||||
return records;
|
||||
}
|
||||
|
||||
function renderDetail(record) {
|
||||
var container = query('[data-growth-detail]');
|
||||
|
||||
if (container) container.innerHTML = renderGrowthDetail(record);
|
||||
}
|
||||
|
||||
async function loadGrowthDetail(recordId) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var record;
|
||||
|
||||
if (!genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return null;
|
||||
try {
|
||||
record = normalizeGrowthRecord(await api.growthRecordDetail(genealogyId, recordId));
|
||||
if (!record) throw new Error('成长记录详情响应缺少稳定 GrowthRecordVo 字段');
|
||||
recordsById[record.recordId] = record;
|
||||
renderDetail(record);
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return null;
|
||||
renderDetail(null);
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该成长记录。' : (error.message || '成长记录详情加载失败'));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGrowthRecords() {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var records;
|
||||
var requestedRecordId;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks();
|
||||
try {
|
||||
records = renderGrowthRecords(await api.growthRecords(genealogyId));
|
||||
requestedRecordId = getCurrentRecordId();
|
||||
if (requestedRecordId && recordsById[requestedRecordId]) {
|
||||
await loadGrowthDetail(requestedRecordId);
|
||||
} else if (records.length) {
|
||||
await loadGrowthDetail(records[0].recordId);
|
||||
} else {
|
||||
renderDetail(null);
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (query('[data-growth-list]')) {
|
||||
query('[data-growth-list]').innerHTML = '<div class="api-empty">' +
|
||||
(isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : '成长记录加载失败,请稍后重试。') +
|
||||
'</div>';
|
||||
}
|
||||
showMessage(isForbidden(error) ? '当前账号无权查看该家谱的成长记录。' : (error.message || '成长记录加载失败'));
|
||||
}
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
var values = {};
|
||||
|
||||
@@ -173,102 +459,171 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var status = query('[data-growth-form-status]');
|
||||
function setFieldValue(name, value) {
|
||||
var field = query('[data-growth-form] [name="' + name + '"]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
if (field) field.value = value === undefined || value === null ? '' : String(value);
|
||||
}
|
||||
|
||||
function setWritePending(pending) {
|
||||
writePending = Boolean(pending);
|
||||
queryAll('[data-growth-form] button, [data-growth-form] input, [data-growth-form] textarea').forEach(function (control) {
|
||||
control.disabled = writePending;
|
||||
});
|
||||
}
|
||||
function fillGrowthForm(record) {
|
||||
var contentField;
|
||||
var editorInstance;
|
||||
|
||||
function syncGenealogyLinks(genealogyId) {
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
|
||||
root.ProfileUI.syncGenealogyContextLinks();
|
||||
return;
|
||||
setFieldValue('lineagePersonId', record.lineagePersonId);
|
||||
setFieldValue('recordType', record.recordType);
|
||||
setFieldValue('recordTitle', record.recordTitle);
|
||||
setFieldValue('recordContent', record.recordContent);
|
||||
setFieldValue('recordDate', toDateInputValue(record.recordDate));
|
||||
setFieldValue('remindTime', toDateTimeInputValue(record.remindTime));
|
||||
setFieldValue('mediaOssIds', record.mediaOssIds);
|
||||
setFieldValue('sortOrder', record.sortOrder);
|
||||
setFieldValue('status', '0');
|
||||
contentField = query('[data-growth-form] [name="recordContent"]');
|
||||
if (contentField && root.AppRichEditor && root.AppRichEditor.init) {
|
||||
editorInstance = root.AppRichEditor.init(contentField);
|
||||
if (editorInstance && editorInstance.editor && editorInstance.editor.setHtml) {
|
||||
editorInstance.editor.setHtml(record.recordContent || '');
|
||||
}
|
||||
}
|
||||
queryAll('[data-genealogy-context-link]').forEach(function (link) {
|
||||
var href = link.getAttribute('href');
|
||||
var base;
|
||||
|
||||
if (!href || href === '#') return;
|
||||
base = href.split('?')[0];
|
||||
link.href = base + '?genealogyId=' + encodeURIComponent(genealogyId);
|
||||
});
|
||||
}
|
||||
|
||||
function renderNoContext() {
|
||||
var list = query('[data-growth-list]');
|
||||
|
||||
if (list) list.innerHTML = '<div class="api-empty">等待家谱入口接口;请从具体家谱进入成长记录。</div>';
|
||||
setFormStatus('等待家谱入口接口;请从具体家谱进入成长记录。');
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
|
||||
if (!genealogyId) {
|
||||
renderNoContext();
|
||||
showMessage('等待家谱入口接口;请从具体家谱进入成长记录。');
|
||||
if (query('[data-growth-media-status]')) {
|
||||
query('[data-growth-media-status]').textContent = record.mediaOssIds
|
||||
? '已保留 ' + attachmentCount(record.mediaOssIds) + ' 个附件,可继续选择追加'
|
||||
: '未选择文件';
|
||||
}
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function buildListUrl(genealogyId) {
|
||||
return 'profile-growth.html?genealogyId=' + encodeURIComponent(genealogyId);
|
||||
function applyLineageOptions(data, selectedId, fallbackName) {
|
||||
var select = query('[data-growth-lineage-person]');
|
||||
|
||||
if (select) select.innerHTML = renderLineageOptions(data, selectedId, fallbackName);
|
||||
}
|
||||
|
||||
async function loadGrowthRecords() {
|
||||
async function loadGrowthEditor() {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var recordId;
|
||||
var result;
|
||||
var record;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
recordId = getCurrentRecordId();
|
||||
syncGenealogyLinks();
|
||||
setEditorEnabled(false, '正在加载成长记录编辑信息…');
|
||||
try {
|
||||
renderGrowthRecords(await api.growthRecords(genealogyId));
|
||||
result = await Promise.all([
|
||||
api.lineagePersonOptions(genealogyId),
|
||||
recordId ? api.growthRecordDetail(genealogyId, recordId) : Promise.resolve(null)
|
||||
]);
|
||||
record = recordId ? normalizeGrowthRecord(result[1]) : null;
|
||||
if (recordId && !record) throw new Error('成长记录详情响应缺少稳定 GrowthRecordVo 字段');
|
||||
applyLineageOptions(result[0], record && record.lineagePersonId, record && record.lineagePersonName);
|
||||
if (record) {
|
||||
fillGrowthForm(record);
|
||||
if (query('[data-growth-editor-title]')) query('[data-growth-editor-title]').textContent = '编辑成长记录';
|
||||
}
|
||||
setEditorEnabled(true);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
renderGrowthRecords(null);
|
||||
showMessage(error.message || '成长记录加载失败');
|
||||
setEditorEnabled(false, isForbidden(error)
|
||||
? '当前账号无权维护该家谱的成长记录。'
|
||||
: (error.message || '成长记录编辑信息加载失败'));
|
||||
setFormStatus(error.message || '成长记录编辑信息加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitGrowthRecord(form) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var genealogyId = requireGenealogyId();
|
||||
var recordId = getCurrentRecordId();
|
||||
var body;
|
||||
var validation;
|
||||
var result;
|
||||
var saved;
|
||||
var savedId;
|
||||
var verifiedDetail;
|
||||
|
||||
if (writePending || redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
if (writePending || !genealogyId || redirectUnauthorized(api)) return;
|
||||
body = buildGrowthRecordBody(getFormValues(form));
|
||||
validation = validateGrowthRecordBody(body);
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
return;
|
||||
}
|
||||
delete body.invalidSortOrder;
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存成长记录...');
|
||||
setFormStatus('正在保存成长记录…');
|
||||
try {
|
||||
await api.createGrowthRecord(genealogyId, body);
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId);
|
||||
result = recordId
|
||||
? await api.updateGrowthRecord(genealogyId, recordId, body)
|
||||
: await api.createGrowthRecord(genealogyId, body);
|
||||
saved = normalizeGrowthRecord(result);
|
||||
savedId = recordId || (saved && saved.recordId);
|
||||
if (!savedId) throw new Error('保存响应缺少 recordId');
|
||||
verifiedDetail = await api.growthRecordDetail(genealogyId, savedId);
|
||||
if (!matchesSavedRecord(verifiedDetail, savedId)) throw new Error('保存后重读未返回同一条成长记录');
|
||||
if (root.location) root.location.href = buildListUrl(genealogyId, savedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setFormStatus(error.message || '成长记录保存失败');
|
||||
setFormStatus(isForbidden(error) ? '当前账号无权保存该成长记录。' : (error.message || '成长记录保存失败'));
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGrowthRecord(recordId) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var record = recordsById[recordId];
|
||||
|
||||
if (writePending || !genealogyId || !normalizeId(recordId) || redirectUnauthorized(api)) return;
|
||||
if (root.confirm && !root.confirm('确认删除“' + (record ? record.recordTitle : '该成长记录') + '”吗?删除后不可恢复,并会释放附件引用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.deleteGrowthRecord(genealogyId, recordId);
|
||||
await loadGrowthRecords();
|
||||
showMessage('成长记录已删除');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(isForbidden(error) ? '当前账号无权删除该成长记录。' : (error.message || '成长记录删除失败'));
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
function clearMediaSelection() {
|
||||
var target = query('[data-growth-form] [name="mediaOssIds"]');
|
||||
var status = query('[data-growth-media-status]');
|
||||
var fileInput = query('[data-growth-media-input]');
|
||||
|
||||
if (target) target.value = '';
|
||||
if (fileInput) fileInput.value = '';
|
||||
if (status) status.textContent = '未选择文件';
|
||||
}
|
||||
|
||||
function bindActions() {
|
||||
if (!documentRef) return;
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var detailButton = event.target.closest('[data-growth-detail-id]');
|
||||
var deleteButton = event.target.closest('[data-growth-delete-id]');
|
||||
var clearMediaButton = event.target.closest('[data-growth-clear-media]');
|
||||
|
||||
if (detailButton) {
|
||||
event.preventDefault();
|
||||
loadGrowthDetail(detailButton.getAttribute('data-growth-detail-id'));
|
||||
return;
|
||||
}
|
||||
if (deleteButton) {
|
||||
event.preventDefault();
|
||||
deleteGrowthRecord(deleteButton.getAttribute('data-growth-delete-id'));
|
||||
return;
|
||||
}
|
||||
if (clearMediaButton) {
|
||||
event.preventDefault();
|
||||
clearMediaSelection();
|
||||
}
|
||||
});
|
||||
documentRef.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('[data-growth-form]');
|
||||
|
||||
@@ -279,23 +634,23 @@
|
||||
}
|
||||
|
||||
function init() {
|
||||
var genealogyId;
|
||||
|
||||
if (!documentRef) return;
|
||||
bindActions();
|
||||
if (query('[data-growth-page]')) loadGrowthRecords();
|
||||
if (query('[data-growth-edit-page]')) {
|
||||
genealogyId = requireGenealogyId();
|
||||
if (genealogyId) syncGenealogyLinks(genealogyId);
|
||||
}
|
||||
if (query('[data-growth-edit-page]')) loadGrowthEditor();
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentGenealogyId: getCurrentGenealogyId,
|
||||
getCurrentRecordId: getCurrentRecordId,
|
||||
buildGrowthRecordBody: buildGrowthRecordBody,
|
||||
validateGrowthRecordBody: validateGrowthRecordBody,
|
||||
normalizeGrowthList: normalizeGrowthList,
|
||||
normalizeGrowthRecord: normalizeGrowthRecord,
|
||||
matchesSavedRecord: matchesSavedRecord,
|
||||
renderLineageOptions: renderLineageOptions,
|
||||
renderGrowthDetail: renderGrowthDetail,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user