完成10%
This commit is contained in:
+96
-47
@@ -18,25 +18,26 @@
|
||||
var FORM_CONFIG = {
|
||||
login: {
|
||||
selector: '#login-password-form',
|
||||
captchaScene: 'WEB_H5_LOGIN',
|
||||
captchaScene: 'PC_SMS_LOGIN',
|
||||
successUrl: 'profile.html'
|
||||
},
|
||||
'sms-login': {
|
||||
selector: '#login-sms-form',
|
||||
captchaScene: 'WEB_H5_LOGIN',
|
||||
captchaScene: 'PC_SMS_LOGIN',
|
||||
successUrl: 'profile.html'
|
||||
},
|
||||
register: {
|
||||
selector: '#register-form',
|
||||
captchaScene: 'WEB_H5_REGISTER',
|
||||
captchaScene: 'PC_REGISTER',
|
||||
successUrl: 'login.html'
|
||||
},
|
||||
'password-reset': {
|
||||
selector: '#password-reset-form',
|
||||
captchaScene: 'WEB_H5_FORGOT_PASSWORD',
|
||||
captchaScene: 'PC_FORGOT_PASSWORD',
|
||||
successUrl: 'login.html'
|
||||
}
|
||||
};
|
||||
var SMS_CODE_COOLDOWN_SECONDS = 60;
|
||||
|
||||
function hashPassword(value) {
|
||||
// 接口文档要求密码传 32 位 MD5;没有加载 md5.js 时保留原值,便于测试定位。
|
||||
@@ -46,50 +47,40 @@
|
||||
}
|
||||
|
||||
function buildLoginBody(values, hashFn) {
|
||||
// 这里只组装页面字段,grantType、tenantId、clientId 由 api-client 统一补齐。
|
||||
// 这里只组装登录 DTO 字段,grantType、tenantId、clientId 由 api-client 统一补齐。
|
||||
var hash = hashFn || hashPassword;
|
||||
var body = {
|
||||
return {
|
||||
phone: values.phone,
|
||||
password: hash(values.password)
|
||||
};
|
||||
|
||||
if (values.validToken) body.validToken = values.validToken;
|
||||
return body;
|
||||
}
|
||||
|
||||
function buildSmsLoginBody(values) {
|
||||
// 短信登录接口只需要页面字段,grantType、tenantId、clientId 由 api-client 统一补齐。
|
||||
var body = {
|
||||
return {
|
||||
phone: values.phone,
|
||||
smsCode: values.smsCode
|
||||
};
|
||||
|
||||
if (values.validToken) body.validToken = values.validToken;
|
||||
return body;
|
||||
}
|
||||
|
||||
function buildRegisterBody(values, hashFn) {
|
||||
// 注册验证码当前对应 validToken,可选字段只在有值时提交。
|
||||
// 确认密码只用于页面校验;注册接口需要手机号短信验证码。
|
||||
var hash = hashFn || hashPassword;
|
||||
var body = {
|
||||
return {
|
||||
phone: values.phone,
|
||||
password: hash(values.password),
|
||||
nickName: values.nickName || values.phone
|
||||
nickName: values.nickName || values.phone,
|
||||
smsCode: values.smsCode
|
||||
};
|
||||
|
||||
if (values.validToken) body.validToken = values.validToken;
|
||||
return body;
|
||||
}
|
||||
|
||||
function buildPasswordResetBody(values, hashFn) {
|
||||
// 找回密码接口使用 newPassword 字段,页面确认密码只在提交前校验,不传给后端。
|
||||
var hash = hashFn || hashPassword;
|
||||
|
||||
return {
|
||||
phone: values.phone,
|
||||
smsCode: values.smsCode,
|
||||
newPassword: hash(values.newPassword),
|
||||
validToken: values.validToken || ''
|
||||
newPassword: hash(values.newPassword)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,8 +112,10 @@
|
||||
// 未配置 PC/H5 验证场景时不调用验证中心,也不伪造 validToken。
|
||||
if (!sceneCode) return true;
|
||||
|
||||
// 验证适配层不存在时不阻塞测试环境;真实页面会加载 captcha-pages.js。
|
||||
if (!root.CaptchaPages || !root.CaptchaPages.ensureToken) return true;
|
||||
if (!root.CaptchaPages || !root.CaptchaPages.ensureToken) {
|
||||
showMessage('验证码组件未加载,请刷新后重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(await root.CaptchaPages.ensureToken(form, {
|
||||
sceneCode: sceneCode,
|
||||
@@ -149,6 +142,41 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function clearCaptchaToken(form) {
|
||||
var field = query('input[name="validToken"]', form);
|
||||
|
||||
if (field) field.value = '';
|
||||
}
|
||||
|
||||
function startSmsCooldown(button) {
|
||||
var originalText;
|
||||
var remaining = SMS_CODE_COOLDOWN_SECONDS;
|
||||
var interval;
|
||||
|
||||
if (!button || typeof root.setInterval !== 'function') return;
|
||||
if (button.__smsCooldownTimer && typeof root.clearInterval === 'function') {
|
||||
root.clearInterval(button.__smsCooldownTimer);
|
||||
}
|
||||
|
||||
originalText = button.getAttribute('data-sms-code-label') || button.textContent.trim();
|
||||
button.setAttribute('data-sms-code-label', originalText);
|
||||
button.disabled = true;
|
||||
button.textContent = remaining + ' 秒后重试';
|
||||
interval = root.setInterval(function () {
|
||||
remaining -= 1;
|
||||
if (remaining > 0) {
|
||||
button.textContent = remaining + ' 秒后重试';
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof root.clearInterval === 'function') root.clearInterval(interval);
|
||||
button.__smsCooldownTimer = null;
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
}, 1000);
|
||||
button.__smsCooldownTimer = interval;
|
||||
}
|
||||
|
||||
function setBusy(button, busy) {
|
||||
// 加载状态只切 class 和 aria,不在 JS 中写样式。
|
||||
if (!button) return;
|
||||
@@ -197,7 +225,14 @@
|
||||
}
|
||||
|
||||
if (formType === 'register') {
|
||||
return validatePassword(data.password, '请填写密码');
|
||||
if (!data.smsCode) return '请填写短信验证码';
|
||||
if (!isSmsCode(data.smsCode)) return '请输入正确的短信验证码';
|
||||
|
||||
var registerPasswordMessage = validatePassword(data.password, '请填写密码');
|
||||
if (registerPasswordMessage) return registerPasswordMessage;
|
||||
if (!data.confirmPassword) return '请再次输入密码';
|
||||
if (data.password !== data.confirmPassword) return '两次输入的密码不一致';
|
||||
return '';
|
||||
}
|
||||
|
||||
if (formType === 'password-reset') {
|
||||
@@ -261,6 +296,7 @@
|
||||
var isActive = panel.id === activePanelId;
|
||||
|
||||
panel.hidden = !isActive;
|
||||
if (!isActive) clearCaptchaToken(panel);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,12 +305,11 @@
|
||||
var api = getApi();
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('login', values)) return;
|
||||
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'login'), values.phone)) return;
|
||||
|
||||
values = readForm(form);
|
||||
|
||||
setBusy(button, true);
|
||||
try {
|
||||
await api.login(buildLoginBody(values));
|
||||
@@ -291,12 +326,11 @@
|
||||
var api = getApi();
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('sms-login', values)) return;
|
||||
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'sms-login'), values.phone)) return;
|
||||
|
||||
values = readForm(form);
|
||||
|
||||
setBusy(button, true);
|
||||
try {
|
||||
await api.loginBySms(buildSmsLoginBody(values));
|
||||
@@ -313,12 +347,11 @@
|
||||
var api = getApi();
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('register', values)) return;
|
||||
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'register'), values.phone)) return;
|
||||
|
||||
values = readForm(form);
|
||||
|
||||
setBusy(button, true);
|
||||
try {
|
||||
await api.register(buildRegisterBody(values));
|
||||
@@ -338,7 +371,10 @@
|
||||
var formType = getFormType(form);
|
||||
var sceneCode = getFormCaptchaScene(form, formType);
|
||||
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSendCode(values)) return;
|
||||
if (!await ensureCaptcha(form, sceneCode, values.phone)) return;
|
||||
|
||||
@@ -351,10 +387,13 @@
|
||||
phone: values.phone,
|
||||
validToken: values.validToken || ''
|
||||
});
|
||||
clearCaptchaToken(form);
|
||||
showMessage('验证码已发送');
|
||||
setBusy(button, false);
|
||||
startSmsCooldown(button);
|
||||
} catch (error) {
|
||||
clearCaptchaToken(form);
|
||||
showMessage(error.message || '验证码发送失败');
|
||||
} finally {
|
||||
setBusy(button, false);
|
||||
}
|
||||
}
|
||||
@@ -364,12 +403,11 @@
|
||||
var api = getApi();
|
||||
var values = readForm(form);
|
||||
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
showMessage('接口初始化失败,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
if (!validateBeforeSubmit('password-reset', values)) return;
|
||||
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'password-reset'), values.phone)) return;
|
||||
|
||||
values = readForm(form);
|
||||
|
||||
setBusy(button, true);
|
||||
try {
|
||||
await api.resetPassword(buildPasswordResetBody(values));
|
||||
@@ -399,6 +437,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function bindCaptchaTokenInvalidation() {
|
||||
queryAll('form input[name="phone"]').forEach(function (field) {
|
||||
field.addEventListener('input', function () {
|
||||
clearCaptchaToken(field.closest('form'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getFormType(form) {
|
||||
if (!form) return '';
|
||||
|
||||
@@ -417,6 +463,7 @@
|
||||
bindSubmit(FORM_CONFIG['password-reset'].selector, submitPasswordReset);
|
||||
bindLoginModeTabs();
|
||||
bindCodeButtons();
|
||||
bindCaptchaTokenInvalidation();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -424,6 +471,8 @@
|
||||
buildSmsLoginBody: buildSmsLoginBody,
|
||||
buildRegisterBody: buildRegisterBody,
|
||||
buildPasswordResetBody: buildPasswordResetBody,
|
||||
clearCaptchaToken: clearCaptchaToken,
|
||||
startSmsCooldown: startSmsCooldown,
|
||||
getCaptchaScene: getCaptchaScene,
|
||||
getFormCaptchaScene: getFormCaptchaScene,
|
||||
getSuccessUrl: getSuccessUrl,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_SCENE_CODE = 'WEB_H5_LOGIN';
|
||||
var DEFAULT_SCENE_CODE = 'PC_SMS_LOGIN';
|
||||
var CAPTCHA_LAYER_AREA = ['318px', '318px'];
|
||||
var captchaLayerSeed = 0;
|
||||
|
||||
|
||||
+160
-36
@@ -20,6 +20,24 @@
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
if (!root.location) return true;
|
||||
if (typeof root.location.replace === 'function') {
|
||||
root.location.replace('login.html');
|
||||
return true;
|
||||
}
|
||||
root.location.href = 'login.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
|
||||
}
|
||||
@@ -62,7 +80,7 @@
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function toNumberOrUndefined(value) {
|
||||
function toIntegerOrUndefined(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
var number;
|
||||
|
||||
@@ -80,23 +98,21 @@
|
||||
mediaOssIds: trimOrUndefined(source.mediaOssIds),
|
||||
status: trimOrUndefined(source.status)
|
||||
};
|
||||
var sortOrder = toNumberOrUndefined(source.sortOrder);
|
||||
var sortOrder = toIntegerOrUndefined(source.sortOrder);
|
||||
|
||||
if (sortOrder !== undefined) body.sortOrder = sortOrder;
|
||||
return body;
|
||||
}
|
||||
|
||||
function buildCommentBody(values) {
|
||||
// FamilyFeedCommentBody 的 content 是唯一必填字段,其余字段按需携带。
|
||||
// FamilyFeedCommentBody 仅接受 commentContent 和可选的 parentCommentId。
|
||||
var source = values || {};
|
||||
var body = {
|
||||
content: String(source.content || '').trim()
|
||||
commentContent: String(source.commentContent || '').trim()
|
||||
};
|
||||
var parentCommentId = toNumberOrUndefined(source.parentCommentId);
|
||||
var replyUserId = toNumberOrUndefined(source.replyUserId);
|
||||
var parentCommentId = trimOrUndefined(source.parentCommentId);
|
||||
|
||||
if (parentCommentId !== undefined) body.parentCommentId = parentCommentId;
|
||||
if (replyUserId !== undefined) body.replyUserId = replyUserId;
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -136,12 +152,17 @@
|
||||
|
||||
function normalizeComment(item) {
|
||||
var commentId = getCommentId(item);
|
||||
var content = item && item.content;
|
||||
var commentContent = item && item.commentContent;
|
||||
var userDeleted;
|
||||
|
||||
if (!commentId || content === undefined || content === null) return null;
|
||||
if (!commentId || commentContent === undefined) return null;
|
||||
userDeleted = String(item && item.userDeleted || '') === '1';
|
||||
return {
|
||||
commentId: commentId,
|
||||
content: String(content)
|
||||
commentContent: commentContent === null ? '该评论已删除' : String(commentContent),
|
||||
appUserNickName: trimOrUndefined(item && item.appUserNickName),
|
||||
replyCount: item && item.replyCount,
|
||||
userDeleted: userDeleted || commentContent === null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,17 +224,46 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function hasReplies(comment) {
|
||||
var count = comment && comment.replyCount;
|
||||
|
||||
return count !== undefined && count !== null && String(count) !== '' && String(count) !== '0';
|
||||
}
|
||||
|
||||
function renderCommentRow(feedId, comment) {
|
||||
var author = comment.appUserNickName ? '<strong>' + escapeHtml(comment.appUserNickName) + '</strong> ' : '';
|
||||
var actions = '';
|
||||
|
||||
if (!comment.userDeleted) {
|
||||
actions += '<button class="pill" type="button" data-feed-comment-reply="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">回复</button>';
|
||||
actions += '<button class="pill" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button>';
|
||||
}
|
||||
if (hasReplies(comment)) {
|
||||
actions += '<button class="pill" type="button" data-feed-comment-replies="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">查看回复</button>';
|
||||
}
|
||||
|
||||
return '<div class="module-row" data-feed-comment-node="' + escapeHtml(comment.commentId) + '"><p>' + author + escapeHtml(comment.commentContent) + '</p>' + actions + '</div>';
|
||||
}
|
||||
|
||||
function normalizeComments(data) {
|
||||
var comments = normalizeList(data).map(normalizeComment);
|
||||
|
||||
return {
|
||||
invalidCount: comments.filter(function (item) { return !item; }).length,
|
||||
comments: comments.filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
function renderComments(feedId, data) {
|
||||
var row = query('[data-feed-id="' + feedId + '"]');
|
||||
var comments = normalizeList(data).map(normalizeComment);
|
||||
var invalidCount = comments.filter(function (item) { return !item; }).length;
|
||||
var list = comments.filter(Boolean);
|
||||
var result = normalizeComments(data);
|
||||
var list = result.comments;
|
||||
var previous = row && row.nextElementSibling;
|
||||
|
||||
if (!row) return;
|
||||
if (previous && previous.getAttribute('data-feed-comment-list') === feedId) previous.remove();
|
||||
if (invalidCount) {
|
||||
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">评论响应缺少 commentId 或 content,请联系后端补充 DTO。</div>');
|
||||
if (result.invalidCount) {
|
||||
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-comment-list="' + escapeHtml(feedId) + '">评论响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>');
|
||||
return;
|
||||
}
|
||||
if (!list.length) {
|
||||
@@ -222,7 +272,28 @@
|
||||
}
|
||||
|
||||
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-comment-list="' + escapeHtml(feedId) + '">' + list.map(function (comment) {
|
||||
return '<div class="module-row"><p>' + escapeHtml(comment.content) + '</p><button class="pill" type="button" data-feed-comment-delete="' + escapeHtml(comment.commentId) + '" data-feed-id="' + escapeHtml(feedId) + '">删除评论</button></div>';
|
||||
return renderCommentRow(feedId, comment);
|
||||
}).join('') + '</div>');
|
||||
}
|
||||
|
||||
function renderCommentReplies(feedId, parentCommentId, data) {
|
||||
var row = query('[data-feed-comment-node="' + parentCommentId + '"]');
|
||||
var result = normalizeComments(data);
|
||||
var previous = row && row.nextElementSibling;
|
||||
|
||||
if (!row) return;
|
||||
if (previous && previous.getAttribute('data-feed-reply-list') === parentCommentId) previous.remove();
|
||||
if (result.invalidCount) {
|
||||
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">回复响应缺少 commentId 或 commentContent,请联系后端补充 DTO。</div>');
|
||||
return;
|
||||
}
|
||||
if (!result.comments.length) {
|
||||
row.insertAdjacentHTML('afterend', '<div class="api-empty" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">暂无直接回复</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
row.insertAdjacentHTML('afterend', '<div class="module-list" data-feed-reply-list="' + escapeHtml(parentCommentId) + '">' + result.comments.map(function (comment) {
|
||||
return renderCommentRow(feedId, comment);
|
||||
}).join('') + '</div>');
|
||||
}
|
||||
|
||||
@@ -264,39 +335,47 @@
|
||||
|
||||
async function loadFeeds() {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
|
||||
if (!api || !genealogyId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
try {
|
||||
renderFeeds(await api.feedsPage(genealogyId, { pageNum: 1, pageSize: 20 }), genealogyId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '家族动态加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFeedForEdit() {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
var feedId = getCurrentFeedId();
|
||||
|
||||
if (!api || !genealogyId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
if (!feedId) return;
|
||||
try {
|
||||
fillFeedForm(await api.feedDetail(genealogyId, feedId));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '动态详情加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFeed(form) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
var feedId = getCurrentFeedId();
|
||||
var body;
|
||||
|
||||
if (!api || !genealogyId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
// 富文本编辑器在提交前同步回 textarea,保证发送的是用户当前输入。
|
||||
if (root.KindEditor && root.KindEditor.sync) root.KindEditor.sync('#feedContent');
|
||||
body = buildFeedBody(getFormValues(form));
|
||||
@@ -313,15 +392,18 @@
|
||||
showMessage('动态已保存');
|
||||
root.location.href = getListUrl(genealogyId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '动态保存失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function likeFeed(feedId, shouldLike) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
|
||||
if (!api || !genealogyId || !feedId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
try {
|
||||
if (shouldLike) {
|
||||
await api.likeFeed(genealogyId, feedId);
|
||||
@@ -330,64 +412,92 @@
|
||||
}
|
||||
await loadFeeds();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || (shouldLike ? '点赞失败' : '取消点赞失败'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFeed(feedId) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
|
||||
if (!api || !genealogyId || !feedId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
if (root.confirm && !root.confirm('确认删除这条动态吗?')) return;
|
||||
try {
|
||||
await api.deleteFeed(genealogyId, feedId);
|
||||
await loadFeeds();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '删除动态失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function showComments(feedId) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
|
||||
if (!api || !genealogyId || !feedId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
try {
|
||||
renderComments(feedId, await api.feedComments(genealogyId, feedId));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '评论加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function createComment(feedId) {
|
||||
async function showCommentReplies(feedId, commentId) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId || !commentId) return;
|
||||
try {
|
||||
renderCommentReplies(feedId, commentId, await api.feedCommentReplies(genealogyId, feedId, commentId));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '回复加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function createComment(feedId, parentCommentId) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var content;
|
||||
var body;
|
||||
|
||||
if (!api || !genealogyId || !feedId) return;
|
||||
content = root.prompt ? root.prompt('请输入评论内容', '') : '';
|
||||
body = buildCommentBody({ content: content });
|
||||
if (!body.content) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId) return;
|
||||
content = root.prompt ? root.prompt(parentCommentId ? '请输入回复内容' : '请输入评论内容', '') : '';
|
||||
body = buildCommentBody({ commentContent: content, parentCommentId: parentCommentId });
|
||||
if (!body.commentContent) return;
|
||||
try {
|
||||
await api.createFeedComment(genealogyId, feedId, body);
|
||||
await showComments(feedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '评论发布失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteComment(feedId, commentId) {
|
||||
var api = getApi();
|
||||
var genealogyId = requireGenealogyId();
|
||||
var genealogyId;
|
||||
|
||||
if (!api || !genealogyId || !feedId || !commentId) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !feedId || !commentId) return;
|
||||
if (root.confirm && !root.confirm('确认删除这条评论吗?')) return;
|
||||
try {
|
||||
await api.deleteFeedComment(genealogyId, feedId, commentId);
|
||||
await showComments(feedId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '删除评论失败');
|
||||
}
|
||||
}
|
||||
@@ -424,6 +534,18 @@
|
||||
feedId = target.closest('[data-feed-comment]').getAttribute('data-feed-comment');
|
||||
createComment(feedId);
|
||||
}
|
||||
if (target.closest('[data-feed-comment-reply]')) {
|
||||
event.preventDefault();
|
||||
commentId = target.closest('[data-feed-comment-reply]').getAttribute('data-feed-comment-reply');
|
||||
feedId = target.closest('[data-feed-comment-reply]').getAttribute('data-feed-id');
|
||||
createComment(feedId, commentId);
|
||||
}
|
||||
if (target.closest('[data-feed-comment-replies]')) {
|
||||
event.preventDefault();
|
||||
commentId = target.closest('[data-feed-comment-replies]').getAttribute('data-feed-comment-replies');
|
||||
feedId = target.closest('[data-feed-comment-replies]').getAttribute('data-feed-id');
|
||||
showCommentReplies(feedId, commentId);
|
||||
}
|
||||
if (target.closest('[data-feed-delete]')) {
|
||||
event.preventDefault();
|
||||
deleteFeed(target.closest('[data-feed-delete]').getAttribute('data-feed-delete'));
|
||||
@@ -451,7 +573,9 @@
|
||||
buildCommentBody: buildCommentBody,
|
||||
normalizeFeed: normalizeFeed,
|
||||
normalizeComment: normalizeComment,
|
||||
normalizeComments: normalizeComments,
|
||||
normalizeList: normalizeList,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
(function (root, factory) {
|
||||
// 字辈管理模块同时支持浏览器页面和 Node 单元测试。
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
|
||||
root.GenerationPages = factory(root);
|
||||
if (root.document) {
|
||||
root.document.addEventListener('DOMContentLoaded', function () {
|
||||
root.GenerationPages.init();
|
||||
});
|
||||
}
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var documentRef = root.document;
|
||||
var poemsById = Object.create(null);
|
||||
var lastPreviewSignature = '';
|
||||
var managementAccess = true;
|
||||
var writePending = false;
|
||||
|
||||
function getApi() {
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
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();
|
||||
if (!root.location) return true;
|
||||
if (typeof root.location.replace === 'function') {
|
||||
root.location.replace('login.html');
|
||||
return true;
|
||||
}
|
||||
root.location.href = 'login.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
|
||||
}
|
||||
|
||||
function queryAll(selector, rootNode) {
|
||||
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
if (root.layui && root.layui.layer) {
|
||||
root.layui.layer.msg(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function getQueryParam(search, name) {
|
||||
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
||||
|
||||
return params.get(name) || '';
|
||||
}
|
||||
|
||||
function getCurrentGenealogyId(search) {
|
||||
var page = query('[data-generation-page]');
|
||||
var source = search === undefined && root.location ? root.location.search : search;
|
||||
var profileGenealogyId;
|
||||
|
||||
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
|
||||
profileGenealogyId = root.ProfileUI.getGenealogyId();
|
||||
if (profileGenealogyId) return profileGenealogyId;
|
||||
}
|
||||
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
|
||||
}
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function toIntegerOrUndefined(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
var number;
|
||||
|
||||
if (!text) return undefined;
|
||||
number = Number(text);
|
||||
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
|
||||
}
|
||||
|
||||
function buildGenerationPoemBody(values) {
|
||||
var source = values || {};
|
||||
var body = {
|
||||
generationNo: toIntegerOrUndefined(source.generationNo),
|
||||
generationText: trimOrUndefined(source.generationText)
|
||||
};
|
||||
var description = trimOrUndefined(source.description);
|
||||
var sortOrder = trimOrUndefined(source.sortOrder);
|
||||
var status = trimOrUndefined(source.status);
|
||||
|
||||
if (description !== undefined) body.description = description;
|
||||
if (sortOrder !== undefined) body.sortOrder = Number(sortOrder);
|
||||
if (status !== undefined) body.status = status;
|
||||
return body;
|
||||
}
|
||||
|
||||
function buildBatchBody(values) {
|
||||
var source = values || {};
|
||||
|
||||
return {
|
||||
poemText: String(source.poemText || '').trim(),
|
||||
disableMissing: Boolean(source.disableMissing)
|
||||
};
|
||||
}
|
||||
|
||||
function validateGenerationPoemBody(body) {
|
||||
if (!body || !Number.isInteger(body.generationNo) || body.generationNo < 1 || body.generationNo > 2147483647) {
|
||||
return '世代序号必须是 1 到 2147483647 之间的整数';
|
||||
}
|
||||
if (!body.generationText) return '请填写字辈文字';
|
||||
if (body.generationText.length > 50) return '字辈文字不能超过 50 个字符';
|
||||
if (body.description && body.description.length > 500) return '说明不能超过 500 个字符';
|
||||
if (body.sortOrder !== undefined && (!Number.isInteger(body.sortOrder) || !Number.isSafeInteger(body.sortOrder))) {
|
||||
return '排序值必须是整数';
|
||||
}
|
||||
if (body.sortOrder !== undefined && (body.sortOrder < -2147483648 || body.sortOrder > 2147483647)) {
|
||||
return '排序值必须在 -2147483648 到 2147483647 之间';
|
||||
}
|
||||
if (body.status !== undefined && body.status !== '0' && body.status !== '1') return '字辈状态只能是 0 或 1';
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateBatchBody(body) {
|
||||
var words;
|
||||
|
||||
if (!body || !body.poemText) return '请填写批量字辈内容';
|
||||
if (body.poemText.length > 26000) return '批量字辈内容不能超过 26000 个字符';
|
||||
words = splitPoemText(body.poemText);
|
||||
if (words.length > 500) return '一次最多导入 500 个世代';
|
||||
if (words.some(function (word) { return word.length > 50; })) return '单个字辈不能超过 50 个字符';
|
||||
return '';
|
||||
}
|
||||
|
||||
function splitPoemText(value) {
|
||||
return String(value || '').trim().split(/[\s,,;;、//||]+/).filter(Boolean);
|
||||
}
|
||||
|
||||
function getPoemId(item) {
|
||||
var value = item && item.poemId;
|
||||
var text;
|
||||
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
text = String(value);
|
||||
return /^\d+$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function normalizeGenerationPoem(item) {
|
||||
var poemId = getPoemId(item);
|
||||
var generationNo = toIntegerOrUndefined(item && item.generationNo);
|
||||
var generationText = trimOrUndefined(item && item.generationText);
|
||||
var status = trimOrUndefined(item && item.status);
|
||||
var poem;
|
||||
|
||||
if (!poemId || generationNo === undefined || !generationText || (status !== '0' && status !== '1')) return null;
|
||||
poem = {
|
||||
poemId: poemId,
|
||||
generationNo: generationNo,
|
||||
generationText: generationText,
|
||||
status: status
|
||||
};
|
||||
if (item.description !== undefined && item.description !== null) poem.description = String(item.description);
|
||||
if (toIntegerOrUndefined(item.sortOrder) !== undefined) poem.sortOrder = toIntegerOrUndefined(item.sortOrder);
|
||||
return poem;
|
||||
}
|
||||
|
||||
function normalizeList(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (data && Array.isArray(data.rows)) return data.rows;
|
||||
return [];
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderGenerationPoem(poem) {
|
||||
var details = [];
|
||||
var stateText = poem.status === '1' ? '已停用' : '正常';
|
||||
|
||||
if (poem.description) details.push('说明:' + poem.description);
|
||||
if (poem.sortOrder !== undefined) details.push('排序:' + poem.sortOrder);
|
||||
details.push('状态:' + stateText);
|
||||
return '<div class="module-row" data-generation-poem-id="' + escapeHtml(poem.poemId) + '">' +
|
||||
'<div><h3>第 ' + escapeHtml(poem.generationNo) + ' 代:' + escapeHtml(poem.generationText) + '</h3><p>' + escapeHtml(details.join(' · ')) + '</p></div>' +
|
||||
'<div class="row-actions">' +
|
||||
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poem.poemId) + '">编辑</button>' +
|
||||
'<button class="pill" type="button" data-generation-status="' + escapeHtml(poem.poemId) + '">' + (poem.status === '1' ? '恢复' : '停用') + '</button>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
function renderGenerationPoems(data) {
|
||||
var container = query('[data-generation-list]');
|
||||
var normalized = normalizeList(data).map(normalizeGenerationPoem);
|
||||
var invalidCount = normalized.filter(function (item) { return !item; }).length;
|
||||
var poems = normalized.filter(Boolean);
|
||||
|
||||
poemsById = Object.create(null);
|
||||
poems.forEach(function (poem) {
|
||||
poemsById[poem.poemId] = poem;
|
||||
});
|
||||
if (!container) return;
|
||||
if (invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">字辈响应缺少 poemId、generationNo、generationText 或 status,请联系后端补充 DTO。</div>';
|
||||
return;
|
||||
}
|
||||
if (!poems.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无字辈记录</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = poems.map(renderGenerationPoem).join('');
|
||||
}
|
||||
|
||||
function normalizePreview(data) {
|
||||
if (!data || !Array.isArray(data.items)) return null;
|
||||
|
||||
return {
|
||||
createCount: toIntegerOrUndefined(data.createCount),
|
||||
updateCount: toIntegerOrUndefined(data.updateCount),
|
||||
keepCount: toIntegerOrUndefined(data.keepCount),
|
||||
disableCount: toIntegerOrUndefined(data.disableCount),
|
||||
items: data.items
|
||||
};
|
||||
}
|
||||
|
||||
function renderBatchPreview(data) {
|
||||
var container = query('[data-generation-batch-preview]');
|
||||
var preview = normalizePreview(data);
|
||||
var summary;
|
||||
|
||||
if (!container) return false;
|
||||
if (!preview) {
|
||||
container.innerHTML = '<div class="api-empty">批量预览响应缺少 items,请联系后端补充 DTO。</div>';
|
||||
return false;
|
||||
}
|
||||
summary = '新增 ' + (preview.createCount === undefined ? '-' : preview.createCount) +
|
||||
' 条 · 更新 ' + (preview.updateCount === undefined ? '-' : preview.updateCount) +
|
||||
' 条 · 保留 ' + (preview.keepCount === undefined ? '-' : preview.keepCount) +
|
||||
' 条 · 停用 ' + (preview.disableCount === undefined ? '-' : preview.disableCount) + ' 条';
|
||||
container.innerHTML = '<div class="module-row"><div><h3>变更预览</h3><p>' + escapeHtml(summary) + '</p></div></div>' +
|
||||
preview.items.map(function (item) {
|
||||
var generationNo = item && item.generationNo;
|
||||
var action = item && item.action;
|
||||
var oldText = item && item.oldGenerationText;
|
||||
var newText = item && item.newGenerationText;
|
||||
var warning = item && item.warning;
|
||||
var oldStatus = item && item.oldStatus;
|
||||
var newStatus = item && item.newStatus;
|
||||
var text = '第 ' + (generationNo === undefined || generationNo === null ? '-' : generationNo) + ' 代:' +
|
||||
(oldText === undefined || oldText === null ? '(新增)' : oldText) + ' → ' +
|
||||
(newText === undefined || newText === null ? '(无)' : newText);
|
||||
|
||||
if (action) text += ' · ' + action;
|
||||
if (oldStatus !== undefined || newStatus !== undefined) {
|
||||
text += ' · 状态:' + formatStatus(oldStatus) + ' → ' + formatStatus(newStatus);
|
||||
}
|
||||
if (warning) text += ' · 警告:' + warning;
|
||||
return '<div class="module-row"><p>' + escapeHtml(text) + '</p></div>';
|
||||
}).join('');
|
||||
return true;
|
||||
}
|
||||
|
||||
function formatStatus(value) {
|
||||
if (String(value) === '0') return '正常';
|
||||
if (String(value) === '1') return '停用';
|
||||
return value === undefined || value === null ? '未提供' : String(value);
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
var values = {};
|
||||
|
||||
queryAll('[name]', form).forEach(function (field) {
|
||||
values[field.name] = field.type === 'checkbox' ? field.checked : field.value;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function setBatchStatus(message) {
|
||||
var target = query('[data-generation-batch-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
}
|
||||
|
||||
function refreshManagementControls() {
|
||||
var disabled = !managementAccess || writePending;
|
||||
|
||||
queryAll('[data-generation-add], [data-generation-batch-action], [data-generation-edit], [data-generation-status]').forEach(function (control) {
|
||||
control.disabled = disabled;
|
||||
});
|
||||
}
|
||||
|
||||
function setManagementEnabled(enabled) {
|
||||
managementAccess = Boolean(enabled);
|
||||
refreshManagementControls();
|
||||
}
|
||||
|
||||
function setWritePending(pending) {
|
||||
writePending = Boolean(pending);
|
||||
refreshManagementControls();
|
||||
}
|
||||
|
||||
function renderForbiddenState() {
|
||||
var container = query('[data-generation-list]');
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">当前账号没有字辈维护权限</div>';
|
||||
lastPreviewSignature = '';
|
||||
setManagementEnabled(false);
|
||||
setBatchStatus('当前账号没有字辈维护权限');
|
||||
}
|
||||
|
||||
function syncGenealogyLinks(genealogyId) {
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
|
||||
root.ProfileUI.syncGenealogyContextLinks();
|
||||
return;
|
||||
}
|
||||
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 requireGenealogyId() {
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
var container;
|
||||
|
||||
if (!genealogyId) {
|
||||
container = query('[data-generation-list]');
|
||||
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入字辈管理</div>';
|
||||
showMessage('请从具体家谱进入字辈管理');
|
||||
}
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function promptForPoem(existing) {
|
||||
var generationNo;
|
||||
var generationText;
|
||||
var description;
|
||||
var sortOrder;
|
||||
|
||||
if (!root.prompt) return null;
|
||||
generationNo = root.prompt('世代序号(从 1 开始)', existing ? String(existing.generationNo) : '');
|
||||
if (generationNo === null) return null;
|
||||
generationText = root.prompt('字辈文字(最多 50 个字符)', existing ? existing.generationText : '');
|
||||
if (generationText === null) return null;
|
||||
description = root.prompt('说明(最多 500 个字符,可留空)', existing && existing.description ? existing.description : '');
|
||||
if (description === null) return null;
|
||||
sortOrder = root.prompt('排序值(可留空,越小越靠前)', existing && existing.sortOrder !== undefined ? String(existing.sortOrder) : '');
|
||||
if (sortOrder === null) return null;
|
||||
return buildGenerationPoemBody({
|
||||
generationNo: generationNo,
|
||||
generationText: generationText,
|
||||
description: description,
|
||||
sortOrder: sortOrder,
|
||||
status: existing && existing.status
|
||||
});
|
||||
}
|
||||
|
||||
async function loadGenerationPoems() {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
try {
|
||||
renderGenerationPoems(await api.generationPoemsManagement(genealogyId));
|
||||
setManagementEnabled(true);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '字辈列表加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function savePoem(poemId) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var existing = poemId ? poemsById[poemId] : null;
|
||||
var body;
|
||||
var validation;
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || (poemId && !existing)) return;
|
||||
body = promptForPoem(existing);
|
||||
if (!body) return;
|
||||
validation = validateGenerationPoemBody(body);
|
||||
if (validation) {
|
||||
showMessage(validation);
|
||||
return;
|
||||
}
|
||||
setWritePending(true);
|
||||
try {
|
||||
if (poemId) {
|
||||
await api.updateGenerationPoem(genealogyId, poemId, body);
|
||||
} else {
|
||||
await api.createGenerationPoem(genealogyId, body);
|
||||
}
|
||||
showMessage('字辈已保存');
|
||||
await loadGenerationPoems();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '字辈保存失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePoemStatus(poemId) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var existing = poemsById[poemId];
|
||||
var body;
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !existing) return;
|
||||
body = buildGenerationPoemBody({
|
||||
generationNo: existing.generationNo,
|
||||
generationText: existing.generationText,
|
||||
description: existing.description,
|
||||
sortOrder: existing.sortOrder,
|
||||
status: existing.status === '1' ? '0' : '1'
|
||||
});
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.updateGenerationPoem(genealogyId, poemId, body);
|
||||
showMessage(existing.status === '1' ? '字辈已恢复' : '字辈已停用');
|
||||
await loadGenerationPoems();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '字辈状态更新失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function previewBatch() {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var form = query('[data-generation-batch-form]');
|
||||
var body;
|
||||
var validation;
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api) || !form) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
body = buildBatchBody(getFormValues(form));
|
||||
validation = validateBatchBody(body);
|
||||
if (validation) {
|
||||
setBatchStatus(validation);
|
||||
return;
|
||||
}
|
||||
setBatchStatus('正在预览…');
|
||||
setWritePending(true);
|
||||
try {
|
||||
if (renderBatchPreview(await api.previewGenerationPoems(genealogyId, body))) {
|
||||
lastPreviewSignature = JSON.stringify(body);
|
||||
setBatchStatus('预览完成,请确认后保存');
|
||||
} else {
|
||||
lastPreviewSignature = '';
|
||||
setBatchStatus('预览响应不完整');
|
||||
}
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
lastPreviewSignature = '';
|
||||
setBatchStatus(error.message || '批量预览失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBatch() {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var form = query('[data-generation-batch-form]');
|
||||
var body;
|
||||
var validation;
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api) || !form) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
body = buildBatchBody(getFormValues(form));
|
||||
validation = validateBatchBody(body);
|
||||
if (validation) {
|
||||
setBatchStatus(validation);
|
||||
return;
|
||||
}
|
||||
if (lastPreviewSignature !== JSON.stringify(body)) {
|
||||
setBatchStatus('请先预览当前内容,再保存');
|
||||
return;
|
||||
}
|
||||
if (root.confirm && !root.confirm('确认按当前预览保存字辈吗?')) return;
|
||||
setBatchStatus('正在保存…');
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.saveGenerationPoems(genealogyId, body);
|
||||
lastPreviewSignature = '';
|
||||
setBatchStatus('批量字辈已保存');
|
||||
await loadGenerationPoems();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
setBatchStatus(error.message || '批量保存失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
function bindActions() {
|
||||
if (!documentRef) return;
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var target = event.target;
|
||||
var action = target.closest('[data-generation-batch-action]');
|
||||
var poemButton = target.closest('[data-generation-edit], [data-generation-status]');
|
||||
|
||||
if (action) {
|
||||
event.preventDefault();
|
||||
if (action.getAttribute('data-generation-batch-action') === 'preview') previewBatch();
|
||||
if (action.getAttribute('data-generation-batch-action') === 'save') saveBatch();
|
||||
return;
|
||||
}
|
||||
if (target.closest('[data-generation-add]')) {
|
||||
event.preventDefault();
|
||||
savePoem('');
|
||||
return;
|
||||
}
|
||||
if (!poemButton) return;
|
||||
event.preventDefault();
|
||||
if (poemButton.hasAttribute('data-generation-edit')) savePoem(poemButton.getAttribute('data-generation-edit'));
|
||||
if (poemButton.hasAttribute('data-generation-status')) togglePoemStatus(poemButton.getAttribute('data-generation-status'));
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!documentRef) return;
|
||||
bindActions();
|
||||
if (query('[data-generation-page]')) loadGenerationPoems();
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentGenealogyId: getCurrentGenealogyId,
|
||||
buildGenerationPoemBody: buildGenerationPoemBody,
|
||||
buildBatchBody: buildBatchBody,
|
||||
validateGenerationPoemBody: validateGenerationPoemBody,
|
||||
validateBatchBody: validateBatchBody,
|
||||
splitPoemText: splitPoemText,
|
||||
normalizeGenerationPoem: normalizeGenerationPoem,
|
||||
normalizePreview: normalizePreview,
|
||||
normalizeList: normalizeList,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,697 @@
|
||||
(function (root, factory) {
|
||||
// 世系人物模块同时支持浏览器页面和 Node 单元测试。
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(root);
|
||||
return;
|
||||
}
|
||||
|
||||
root.LineagePages = factory(root);
|
||||
if (root.document) {
|
||||
root.document.addEventListener('DOMContentLoaded', function () {
|
||||
root.LineagePages.init();
|
||||
});
|
||||
}
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var documentRef = root.document;
|
||||
var selectedPersonId = '';
|
||||
var editingPersonId = '';
|
||||
var relationMode = '';
|
||||
var peopleById = Object.create(null);
|
||||
var currentPageNum = 1;
|
||||
var currentPageSize = 20;
|
||||
var currentPageTotal = null;
|
||||
var managementAccess = true;
|
||||
var writePending = false;
|
||||
|
||||
function getApi() {
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
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();
|
||||
if (!root.location) return true;
|
||||
if (typeof root.location.replace === 'function') {
|
||||
root.location.replace('login.html');
|
||||
return true;
|
||||
}
|
||||
root.location.href = 'login.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
return documentRef ? (rootNode || documentRef).querySelector(selector) : null;
|
||||
}
|
||||
|
||||
function queryAll(selector, rootNode) {
|
||||
return documentRef ? Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)) : [];
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
if (root.layui && root.layui.layer) {
|
||||
root.layui.layer.msg(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function getQueryParam(search, name) {
|
||||
var params = new URLSearchParams(String(search || '').replace(/^\?/, ''));
|
||||
|
||||
return params.get(name) || '';
|
||||
}
|
||||
|
||||
function getCurrentGenealogyId(search) {
|
||||
var page = query('[data-lineage-page]');
|
||||
var source = search === undefined && root.location ? root.location.search : search;
|
||||
var profileGenealogyId;
|
||||
|
||||
if (search === undefined && root.ProfileUI && root.ProfileUI.getGenealogyId) {
|
||||
profileGenealogyId = root.ProfileUI.getGenealogyId();
|
||||
if (profileGenealogyId) return profileGenealogyId;
|
||||
}
|
||||
return getQueryParam(source, 'genealogyId') || (page && page.getAttribute('data-genealogy-id')) || '';
|
||||
}
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function toIntegerOrUndefined(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
var number;
|
||||
|
||||
if (!text) return undefined;
|
||||
number = Number(text);
|
||||
return Number.isInteger(number) && Number.isSafeInteger(number) ? number : undefined;
|
||||
}
|
||||
|
||||
function getPersonId(item) {
|
||||
var value = item && item.personId;
|
||||
var text;
|
||||
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
text = String(value);
|
||||
return /^\d+$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function buildLineagePersonBody(values) {
|
||||
var source = values || {};
|
||||
var body = { name: trimOrUndefined(source.name) };
|
||||
var optionalFields = [
|
||||
'personNo', 'aliasName', 'sex', 'generationName', 'birthDate', 'birthLunar', 'birthPlace',
|
||||
'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography',
|
||||
'remark', 'relationName'
|
||||
];
|
||||
var generation = trimOrUndefined(source.generation);
|
||||
var sortOrder = trimOrUndefined(source.sortOrder);
|
||||
|
||||
optionalFields.forEach(function (field) {
|
||||
var value = trimOrUndefined(source[field]);
|
||||
|
||||
if (value !== undefined) body[field] = value;
|
||||
});
|
||||
if (generation !== undefined) body.generation = Number(generation);
|
||||
if (sortOrder !== undefined) body.sortOrder = Number(sortOrder);
|
||||
return body;
|
||||
}
|
||||
|
||||
function validateLineagePersonBody(body) {
|
||||
if (!body || !body.name) return '请填写成员姓名';
|
||||
if (body.generation !== undefined && (!Number.isInteger(body.generation) || !Number.isSafeInteger(body.generation))) {
|
||||
return '世代序号必须是整数';
|
||||
}
|
||||
if (body.sortOrder !== undefined && (!Number.isInteger(body.sortOrder) || !Number.isSafeInteger(body.sortOrder))) {
|
||||
return '排序值必须是整数';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeLineagePerson(item) {
|
||||
var personId = getPersonId(item);
|
||||
var name = trimOrUndefined(item && item.name);
|
||||
var person;
|
||||
|
||||
if (!personId || !name) return null;
|
||||
person = { personId: personId, name: name };
|
||||
[
|
||||
'genealogyName', 'genealogyNo', 'appUserNickName', 'personNo', 'aliasName', 'sex', 'generationName',
|
||||
'fatherName', 'motherName', 'spouseNames', 'birthDate', 'birthLunar', 'birthPlace', 'deathDate',
|
||||
'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography', 'status', 'remark',
|
||||
'relationType', 'relationName'
|
||||
].forEach(function (field) {
|
||||
if (item && item[field] !== undefined && item[field] !== null) person[field] = String(item[field]);
|
||||
});
|
||||
['appUserId', 'genealogyId', 'fatherId', 'motherId', 'avatarOssId'].forEach(function (field) {
|
||||
if (item && item[field] !== undefined && item[field] !== null) {
|
||||
if (typeof item[field] === 'number' && !Number.isSafeInteger(item[field])) return;
|
||||
person[field] = String(item[field]);
|
||||
}
|
||||
});
|
||||
if (toIntegerOrUndefined(item && item.generation) !== undefined) person.generation = toIntegerOrUndefined(item.generation);
|
||||
if (toIntegerOrUndefined(item && item.sortOrder) !== undefined) person.sortOrder = toIntegerOrUndefined(item.sortOrder);
|
||||
return person;
|
||||
}
|
||||
|
||||
function normalizeList(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (data && Array.isArray(data.rows)) return data.rows;
|
||||
return [];
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getPersonSummary(person) {
|
||||
var values = [];
|
||||
|
||||
if (person.generation !== undefined) values.push('第 ' + person.generation + ' 世');
|
||||
if (person.generationName) values.push('字辈:' + person.generationName);
|
||||
if (person.aliasName) values.push('别名:' + person.aliasName);
|
||||
if (person.relationName) values.push('关系称谓:' + person.relationName);
|
||||
if (person.personStatus) values.push('人物状态:' + person.personStatus);
|
||||
return values.join(' · ') || '未填写世代信息';
|
||||
}
|
||||
|
||||
function renderLineageList(data) {
|
||||
var container = query('[data-lineage-list]');
|
||||
var normalized = normalizeList(data).map(normalizeLineagePerson);
|
||||
var invalidCount = normalized.filter(function (item) { return !item; }).length;
|
||||
var people = normalized.filter(Boolean);
|
||||
|
||||
peopleById = Object.create(null);
|
||||
people.forEach(function (person) {
|
||||
peopleById[person.personId] = person;
|
||||
});
|
||||
if (!container) return;
|
||||
if (invalidCount) {
|
||||
container.innerHTML = '<div class="api-empty">人物响应缺少可安全使用的 personId 或 name,请联系后端补充 DTO。</div>';
|
||||
return;
|
||||
}
|
||||
if (!people.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无世系人物</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = people.map(function (person) {
|
||||
var selected = person.personId === selectedPersonId ? ' is-selected' : '';
|
||||
|
||||
return '<button class="module-row lineage-row' + selected + '" type="button" data-lineage-person="' + escapeHtml(person.personId) + '">' +
|
||||
'<div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(getPersonSummary(person)) + '</p></div>' +
|
||||
'<span class="pill">查看</span></button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderTreeNode(item, ancestry) {
|
||||
var person = normalizeLineagePerson(item);
|
||||
var nextAncestry;
|
||||
var spouses;
|
||||
var children;
|
||||
var branches = '';
|
||||
|
||||
if (!person) return '';
|
||||
if (ancestry[person.personId]) {
|
||||
return '<li><span class="api-empty">' + escapeHtml(person.name) + ' 已在上级关系中展示</span></li>';
|
||||
}
|
||||
nextAncestry = Object.assign({}, ancestry);
|
||||
nextAncestry[person.personId] = true;
|
||||
spouses = normalizeList(item && item.spouses).map(function (node) { return renderTreeNode(node, nextAncestry); }).filter(Boolean).join('');
|
||||
children = normalizeList(item && item.children).map(function (node) { return renderTreeNode(node, nextAncestry); }).filter(Boolean).join('');
|
||||
if (spouses) branches += '<ul><li><span>配偶</span><ul>' + spouses + '</ul></li></ul>';
|
||||
if (children) branches += '<ul><li><span>子女</span><ul>' + children + '</ul></li></ul>';
|
||||
return '<li><button class="lineage-node" type="button" data-lineage-person="' + escapeHtml(person.personId) + '">' +
|
||||
'<strong>' + escapeHtml(person.name) + '</strong><span>' + escapeHtml(getPersonSummary(person)) + '</span></button>' + branches + '</li>';
|
||||
}
|
||||
|
||||
function renderTree(data) {
|
||||
var container = query('[data-lineage-tree]');
|
||||
var nodes = normalizeList(data).map(function (item) { return renderTreeNode(item, {}); }).filter(Boolean);
|
||||
|
||||
if (!container) return;
|
||||
if (!nodes.length) {
|
||||
container.innerHTML = '<div class="api-empty">暂无世系树</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '<ul class="lineage-tree">' + nodes.join('') + '</ul>';
|
||||
}
|
||||
|
||||
function renderOptions(data) {
|
||||
var select = query('[data-lineage-person-options]');
|
||||
var people = normalizeList(data).map(normalizeLineagePerson).filter(Boolean);
|
||||
|
||||
if (!select) return;
|
||||
select.innerHTML = '<option value="">请选择当前成员</option>' + people.map(function (person) {
|
||||
return '<option value="' + escapeHtml(person.personId) + '">' + escapeHtml(person.name + (person.generationName ? ' · ' + person.generationName : '')) + '</option>';
|
||||
}).join('');
|
||||
select.value = selectedPersonId;
|
||||
}
|
||||
|
||||
function renderListSummary(data) {
|
||||
var container = query('[data-lineage-list-summary]');
|
||||
var count;
|
||||
|
||||
if (!container) return;
|
||||
count = normalizeList(data).map(normalizeLineagePerson).filter(Boolean).length;
|
||||
container.textContent = count ? '成员总览已返回 ' + count + ' 人;下方结果按页展示。' : '成员总览暂无可展示人物。';
|
||||
}
|
||||
|
||||
function updatePaginationControls() {
|
||||
var container = query('[data-lineage-pagination]');
|
||||
var previous = query('[data-lineage-page-action="previous"]');
|
||||
var next = query('[data-lineage-page-action="next"]');
|
||||
var status = query('[data-lineage-page-status]');
|
||||
var disabled = !managementAccess || writePending;
|
||||
|
||||
if (!container) return;
|
||||
if (currentPageTotal === null) {
|
||||
container.hidden = true;
|
||||
return;
|
||||
}
|
||||
container.hidden = false;
|
||||
if (previous) previous.disabled = disabled || currentPageNum <= 1;
|
||||
if (next) next.disabled = disabled || currentPageNum * currentPageSize >= currentPageTotal;
|
||||
if (status) status.textContent = '第 ' + currentPageNum + ' 页,共 ' + currentPageTotal + ' 人';
|
||||
}
|
||||
|
||||
function renderPagination(data) {
|
||||
var total = data && Number(data.total);
|
||||
|
||||
currentPageTotal = Number.isFinite(total) && total >= 0 ? total : null;
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
function renderDetail(data) {
|
||||
var container = query('[data-lineage-detail]');
|
||||
var person = normalizeLineagePerson(data);
|
||||
var values;
|
||||
|
||||
if (!container) return;
|
||||
if (!person) {
|
||||
container.innerHTML = '<div class="api-empty">人物详情缺少可安全使用的 personId 或 name,请联系后端补充 DTO。</div>';
|
||||
return;
|
||||
}
|
||||
selectedPersonId = person.personId;
|
||||
peopleById[person.personId] = person;
|
||||
values = [
|
||||
person.sex ? '性别:' + person.sex : '',
|
||||
person.generation !== undefined ? '世代:' + person.generation : '',
|
||||
person.generationName ? '字辈:' + person.generationName : '',
|
||||
person.personNo ? '人物编号:' + person.personNo : '',
|
||||
person.birthDate ? '出生:' + person.birthDate : '',
|
||||
person.deathDate ? '逝世:' + person.deathDate : '',
|
||||
person.biography ? '简介:' + person.biography : ''
|
||||
].filter(Boolean);
|
||||
container.innerHTML = '<div class="module-row"><div><h3>' + escapeHtml(person.name) + '</h3><p>' + escapeHtml(values.join(' · ') || '暂无更多资料') + '</p></div>' +
|
||||
'<div class="row-actions"><button class="pill" type="button" data-lineage-edit="' + escapeHtml(person.personId) + '">编辑</button>' +
|
||||
'<button class="pill is-danger" type="button" data-lineage-disable="' + escapeHtml(person.personId) + '">停用</button></div></div>';
|
||||
renderLineageList(Object.keys(peopleById).map(function (id) { return peopleById[id]; }));
|
||||
if (query('[data-lineage-person-options]')) query('[data-lineage-person-options]').value = person.personId;
|
||||
refreshControls();
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
var values = {};
|
||||
|
||||
queryAll('[name]', form).forEach(function (field) {
|
||||
values[field.name] = field.value;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function setFormStatus(message) {
|
||||
var target = query('[data-lineage-form-status]');
|
||||
|
||||
if (target) target.textContent = message || '';
|
||||
}
|
||||
|
||||
function relationLabel(mode) {
|
||||
return {
|
||||
parents: '父母',
|
||||
spouses: '配偶',
|
||||
children: '子女',
|
||||
siblings: '兄弟姐妹'
|
||||
}[mode] || '';
|
||||
}
|
||||
|
||||
function syncGenealogyLinks(genealogyId) {
|
||||
if (root.ProfileUI && root.ProfileUI.syncGenealogyContextLinks) {
|
||||
root.ProfileUI.syncGenealogyContextLinks();
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
|
||||
var container = query(selector);
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">请从具体家谱进入世系管理</div>';
|
||||
});
|
||||
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
|
||||
currentPageTotal = null;
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
function requireGenealogyId() {
|
||||
var genealogyId = getCurrentGenealogyId();
|
||||
|
||||
if (!genealogyId) {
|
||||
renderNoContext();
|
||||
showMessage('请从具体家谱进入世系管理');
|
||||
}
|
||||
return genealogyId;
|
||||
}
|
||||
|
||||
function refreshControls() {
|
||||
var disabled = !managementAccess || writePending;
|
||||
|
||||
queryAll('[data-lineage-relation], [data-lineage-search], [data-lineage-form] button, [data-lineage-person-options], [data-lineage-edit], [data-lineage-disable]').forEach(function (control) {
|
||||
control.disabled = disabled;
|
||||
});
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
function setLineageEnabled(enabled) {
|
||||
managementAccess = Boolean(enabled);
|
||||
refreshControls();
|
||||
}
|
||||
|
||||
function setWritePending(pending) {
|
||||
writePending = Boolean(pending);
|
||||
refreshControls();
|
||||
}
|
||||
|
||||
function renderForbiddenState() {
|
||||
['[data-lineage-tree]', '[data-lineage-list]', '[data-lineage-detail]'].forEach(function (selector) {
|
||||
var container = query(selector);
|
||||
|
||||
if (container) container.innerHTML = '<div class="api-empty">当前账号没有世系人物管理权限</div>';
|
||||
});
|
||||
if (query('[data-lineage-list-summary]')) query('[data-lineage-list-summary]').textContent = '';
|
||||
currentPageTotal = null;
|
||||
setLineageEnabled(false);
|
||||
setFormStatus('当前账号没有世系人物管理权限');
|
||||
}
|
||||
|
||||
function getCurrentKeyword() {
|
||||
var input = query('[data-lineage-keyword]');
|
||||
|
||||
return input ? input.value : '';
|
||||
}
|
||||
|
||||
async function loadLineage(keyword, pageNum) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var results;
|
||||
var searchKeyword = trimOrUndefined(keyword);
|
||||
var requestedPage = Number(pageNum);
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
currentPageNum = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
|
||||
syncGenealogyLinks(genealogyId);
|
||||
try {
|
||||
results = await Promise.all([
|
||||
api.lineageTree(genealogyId),
|
||||
api.lineagePersons(genealogyId),
|
||||
api.lineagePersonsPage(genealogyId, {
|
||||
pageNum: currentPageNum,
|
||||
pageSize: currentPageSize,
|
||||
keyword: searchKeyword
|
||||
}),
|
||||
api.lineagePersonOptions(genealogyId)
|
||||
]);
|
||||
renderTree(results[0]);
|
||||
renderListSummary(results[1]);
|
||||
renderLineageList(results[2]);
|
||||
renderPagination(results[2]);
|
||||
renderOptions(results[3]);
|
||||
setLineageEnabled(true);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '世系人物加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPersonDetail(personId) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
|
||||
if (redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !personId) return;
|
||||
try {
|
||||
renderDetail(await api.lineagePersonDetail(genealogyId, personId));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '人物详情加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
function setRelationMode(mode) {
|
||||
var person = peopleById[selectedPersonId];
|
||||
var submitButton = query('[data-lineage-form] button[type="submit"]');
|
||||
|
||||
if (!managementAccess || writePending) return;
|
||||
if (!person) {
|
||||
showMessage('请先从成员列表、世系树或下拉框选择当前成员');
|
||||
return;
|
||||
}
|
||||
relationMode = mode;
|
||||
editingPersonId = '';
|
||||
if (submitButton) submitButton.textContent = '新增' + relationLabel(mode);
|
||||
setFormStatus('当前将为“' + person.name + '”新增' + relationLabel(mode));
|
||||
}
|
||||
|
||||
function fillForm(person) {
|
||||
var form = query('[data-lineage-form]');
|
||||
var values;
|
||||
var submitButton;
|
||||
|
||||
if (!form || !person) return;
|
||||
values = {
|
||||
name: person.name,
|
||||
sex: person.sex,
|
||||
generation: person.generation,
|
||||
generationName: person.generationName,
|
||||
personNo: person.personNo,
|
||||
aliasName: person.aliasName,
|
||||
birthDate: person.birthDate ? person.birthDate.slice(0, 10) : '',
|
||||
deathDate: person.deathDate ? person.deathDate.slice(0, 10) : '',
|
||||
sortOrder: person.sortOrder,
|
||||
relationName: person.relationName,
|
||||
biography: person.biography
|
||||
};
|
||||
queryAll('[name]', form).forEach(function (field) {
|
||||
field.value = values[field.name] === undefined || values[field.name] === null ? '' : values[field.name];
|
||||
});
|
||||
relationMode = '';
|
||||
editingPersonId = person.personId;
|
||||
submitButton = query('[data-lineage-form] button[type="submit"]');
|
||||
if (submitButton) submitButton.textContent = '保存修改';
|
||||
setFormStatus('正在编辑“' + person.name + '”');
|
||||
}
|
||||
|
||||
function resetEditor() {
|
||||
var submitButton = query('[data-lineage-form] button[type="submit"]');
|
||||
|
||||
relationMode = '';
|
||||
editingPersonId = '';
|
||||
if (submitButton) submitButton.textContent = '保存成员';
|
||||
setFormStatus('');
|
||||
}
|
||||
|
||||
async function submitPerson(form) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var body = buildLineagePersonBody(getFormValues(form));
|
||||
var validation = validateLineagePersonBody(body);
|
||||
var result;
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId) return;
|
||||
if (validation) {
|
||||
setFormStatus(validation);
|
||||
return;
|
||||
}
|
||||
setWritePending(true);
|
||||
setFormStatus('正在保存成员...');
|
||||
try {
|
||||
if (editingPersonId) {
|
||||
result = await api.updateLineagePerson(genealogyId, editingPersonId, body);
|
||||
} else if (relationMode === 'parents') {
|
||||
result = await api.createLineageParent(genealogyId, selectedPersonId, body);
|
||||
} else if (relationMode === 'spouses') {
|
||||
result = await api.createLineageSpouse(genealogyId, selectedPersonId, body);
|
||||
} else if (relationMode === 'children') {
|
||||
result = await api.createLineageChild(genealogyId, selectedPersonId, body);
|
||||
} else if (relationMode === 'siblings') {
|
||||
result = await api.createLineageSibling(genealogyId, selectedPersonId, body);
|
||||
} else {
|
||||
result = await api.createLineagePerson(genealogyId, body);
|
||||
}
|
||||
form.reset();
|
||||
resetEditor();
|
||||
setFormStatus('成员已保存');
|
||||
await loadLineage(getCurrentKeyword(), currentPageNum);
|
||||
if (getPersonId(result)) await loadPersonDetail(getPersonId(result));
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
setFormStatus(error.message || '成员保存失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function disablePerson(personId) {
|
||||
var api = getApi();
|
||||
var genealogyId;
|
||||
var person = peopleById[personId];
|
||||
|
||||
if (writePending || !managementAccess || redirectUnauthorized(api)) return;
|
||||
genealogyId = requireGenealogyId();
|
||||
if (!genealogyId || !personId) return;
|
||||
if (root.confirm && !root.confirm('确认停用“' + (person ? person.name : '该成员') + '”吗?有正常子女时后端会拒绝停用。')) return;
|
||||
setWritePending(true);
|
||||
try {
|
||||
await api.disableLineagePerson(genealogyId, personId);
|
||||
selectedPersonId = '';
|
||||
resetEditor();
|
||||
await loadLineage(getCurrentKeyword(), currentPageNum);
|
||||
showMessage('成员已停用');
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (isForbidden(error)) {
|
||||
renderForbiddenState();
|
||||
return;
|
||||
}
|
||||
showMessage(error.message || '成员停用失败');
|
||||
} finally {
|
||||
setWritePending(false);
|
||||
}
|
||||
}
|
||||
|
||||
function bindActions() {
|
||||
if (!documentRef) return;
|
||||
documentRef.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('[data-lineage-form]');
|
||||
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
submitPerson(form);
|
||||
});
|
||||
documentRef.addEventListener('reset', function (event) {
|
||||
if (event.target.closest('[data-lineage-form]')) resetEditor();
|
||||
});
|
||||
documentRef.addEventListener('change', function (event) {
|
||||
var select = event.target.closest('[data-lineage-person-options]');
|
||||
|
||||
if (select && select.value) loadPersonDetail(select.value);
|
||||
});
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var target = event.target;
|
||||
var personButton = target.closest('[data-lineage-person]');
|
||||
var relationButton = target.closest('[data-lineage-relation]');
|
||||
var editButton = target.closest('[data-lineage-edit]');
|
||||
var disableButton = target.closest('[data-lineage-disable]');
|
||||
var pageButton = target.closest('[data-lineage-page-action]');
|
||||
|
||||
if (target.closest('[data-lineage-search]')) {
|
||||
event.preventDefault();
|
||||
loadLineage(getCurrentKeyword(), 1);
|
||||
return;
|
||||
}
|
||||
if (pageButton) {
|
||||
event.preventDefault();
|
||||
if (!managementAccess || writePending) return;
|
||||
if (pageButton.getAttribute('data-lineage-page-action') === 'previous' && currentPageNum > 1) {
|
||||
loadLineage(getCurrentKeyword(), currentPageNum - 1);
|
||||
}
|
||||
if (pageButton.getAttribute('data-lineage-page-action') === 'next' && currentPageTotal !== null && currentPageNum * currentPageSize < currentPageTotal) {
|
||||
loadLineage(getCurrentKeyword(), currentPageNum + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (personButton) {
|
||||
event.preventDefault();
|
||||
loadPersonDetail(personButton.getAttribute('data-lineage-person'));
|
||||
return;
|
||||
}
|
||||
if (relationButton) {
|
||||
event.preventDefault();
|
||||
setRelationMode(relationButton.getAttribute('data-lineage-relation'));
|
||||
return;
|
||||
}
|
||||
if (editButton) {
|
||||
event.preventDefault();
|
||||
fillForm(peopleById[editButton.getAttribute('data-lineage-edit')]);
|
||||
return;
|
||||
}
|
||||
if (disableButton) {
|
||||
event.preventDefault();
|
||||
disablePerson(disableButton.getAttribute('data-lineage-disable'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!documentRef) return;
|
||||
bindActions();
|
||||
if (query('[data-lineage-page]')) loadLineage(getCurrentKeyword(), 1);
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentGenealogyId: getCurrentGenealogyId,
|
||||
getPersonId: getPersonId,
|
||||
buildLineagePersonBody: buildLineagePersonBody,
|
||||
validateLineagePersonBody: validateLineagePersonBody,
|
||||
normalizeLineagePerson: normalizeLineagePerson,
|
||||
normalizeList: normalizeList,
|
||||
relationLabel: relationLabel,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
isForbidden: isForbidden,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
@@ -121,6 +121,43 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getGenealogyId(search) {
|
||||
var source = search === undefined ? (window.location && window.location.search) : search;
|
||||
var params = new URLSearchParams(String(source || '').replace(/^\?/, ''));
|
||||
|
||||
return params.get('genealogyId') || '';
|
||||
}
|
||||
|
||||
function withGenealogyId(href, genealogyId) {
|
||||
var value = String(href || '');
|
||||
var hashIndex;
|
||||
var hash = '';
|
||||
var parts;
|
||||
var params;
|
||||
|
||||
if (!genealogyId || !value || value.charAt(0) === '#' || /^(?:https?:|mailto:|tel:)/i.test(value)) return value;
|
||||
hashIndex = value.indexOf('#');
|
||||
if (hashIndex >= 0) {
|
||||
hash = value.slice(hashIndex);
|
||||
value = value.slice(0, hashIndex);
|
||||
}
|
||||
parts = value.split('?');
|
||||
params = new URLSearchParams(parts[1] || '');
|
||||
params.set('genealogyId', genealogyId);
|
||||
return parts[0] + '?' + params.toString() + hash;
|
||||
}
|
||||
|
||||
function syncGenealogyContextLinks() {
|
||||
var genealogyId = getGenealogyId();
|
||||
|
||||
if (!genealogyId) return;
|
||||
$('[data-genealogy-context-link]').each(function () {
|
||||
var $link = $(this);
|
||||
|
||||
$link.attr('href', withGenealogyId($link.attr('href'), genealogyId));
|
||||
});
|
||||
}
|
||||
|
||||
function updateDisplay($trigger, value) {
|
||||
var target = $trigger.attr('data-update-target');
|
||||
var $target = target ? $(target) : $trigger.closest('p').find('b').first();
|
||||
@@ -226,11 +263,15 @@
|
||||
bindSelectActions();
|
||||
bindMessageActions();
|
||||
bindSelectableCards();
|
||||
syncGenealogyContextLinks();
|
||||
});
|
||||
|
||||
window.ProfileUI = {
|
||||
confirm: openConfirm,
|
||||
closeLogout: closeLegacyLogout,
|
||||
initLayui: initLayui
|
||||
initLayui: initLayui,
|
||||
getGenealogyId: getGenealogyId,
|
||||
withGenealogyId: withGenealogyId,
|
||||
syncGenealogyContextLinks: syncGenealogyContextLinks
|
||||
};
|
||||
})(window, window.jQuery || (window.layui && window.layui.$));
|
||||
|
||||
+82
-42
@@ -16,59 +16,41 @@
|
||||
|
||||
var documentRef = root.document;
|
||||
|
||||
function pick(item, keys, fallback) {
|
||||
// 后端资料字段可能有 nickName、userName、phone 等不同名称,按优先级取值。
|
||||
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 trimOrUndefined(value) {
|
||||
var text = value === undefined || value === null ? '' : String(value).trim();
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function toNumberOrUndefined(value) {
|
||||
var text = trimOrUndefined(value);
|
||||
var numberValue;
|
||||
function profileValue(profile, key, fallback) {
|
||||
var value = profile && profile[key];
|
||||
|
||||
if (!text) return undefined;
|
||||
|
||||
numberValue = Number(text);
|
||||
return Number.isNaN(numberValue) ? undefined : numberValue;
|
||||
return value === undefined || value === null || value === '' ? fallback : value;
|
||||
}
|
||||
|
||||
function getAvatarText(profile) {
|
||||
// 头像字只取展示名第一个字符,避免接口没有头像时页面空白。
|
||||
var displayName = pick(profile, ['nickName', 'userName', 'phone'], '家');
|
||||
var displayName = profileValue(profile, 'nickName', '家');
|
||||
return String(displayName).charAt(0) || '家';
|
||||
}
|
||||
|
||||
function buildRegionText(profile) {
|
||||
var parts = [
|
||||
profile && profile.provinceCode,
|
||||
profile && profile.cityCode,
|
||||
profile && profile.districtCode
|
||||
].filter(Boolean);
|
||||
function getFinalRegionCode(profile) {
|
||||
var source = profile || {};
|
||||
|
||||
return parts.length ? parts.join(' / ') : '待填写';
|
||||
return source.districtCode || source.cityCode || source.provinceCode || '';
|
||||
}
|
||||
|
||||
function buildRegionText(profile) {
|
||||
return getFinalRegionCode(profile) ? '加载中...' : '待填写';
|
||||
}
|
||||
|
||||
function buildProfileView(profile) {
|
||||
// 页面展示模型和接口返回分开,后续改文案不影响接口契约。
|
||||
return {
|
||||
displayName: pick(profile, ['nickName', 'userName', 'phone'], '未设置昵称'),
|
||||
displayName: profileValue(profile, 'nickName', '未设置昵称'),
|
||||
avatarText: getAvatarText(profile),
|
||||
phone: pick(profile, ['phone', 'phonenumber', 'mobile'], '未绑定'),
|
||||
sex: pick(profile, ['sex'], '待填写'),
|
||||
birthday: pick(profile, ['birthday'], '待填写'),
|
||||
phone: profileValue(profile, 'phone', '未绑定'),
|
||||
sex: profileValue(profile, 'sex', '待填写'),
|
||||
birthday: profileValue(profile, 'birthday', '待填写'),
|
||||
regionText: buildRegionText(profile || {})
|
||||
};
|
||||
}
|
||||
@@ -79,7 +61,8 @@
|
||||
|
||||
return {
|
||||
nickName: trimOrUndefined(source.nickName),
|
||||
avatarOssId: toNumberOrUndefined(source.avatarOssId),
|
||||
// int64 在浏览器中必须保留字符串,避免 Number 转换丢失精度。
|
||||
avatarOssId: trimOrUndefined(source.avatarOssId),
|
||||
sex: trimOrUndefined(source.sex),
|
||||
birthday: trimOrUndefined(source.birthday),
|
||||
provinceCode: trimOrUndefined(source.provinceCode),
|
||||
@@ -92,25 +75,25 @@
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function shouldRedirectToHome(api, error) {
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function redirectToHome() {
|
||||
function redirectToLogin() {
|
||||
if (!root.location) return;
|
||||
if (typeof root.location.replace === 'function') {
|
||||
root.location.replace('index.html');
|
||||
root.location.replace('login.html');
|
||||
return;
|
||||
}
|
||||
root.location.href = 'index.html';
|
||||
root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToHome(api, error)) return false;
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
redirectToHome();
|
||||
redirectToLogin();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -184,17 +167,72 @@
|
||||
});
|
||||
}
|
||||
|
||||
function buildRegionPathText(items) {
|
||||
return (Array.isArray(items) ? items : []).map(function (item) {
|
||||
return item && item.regionName || '';
|
||||
}).filter(Boolean).join(' / ');
|
||||
}
|
||||
|
||||
async function renderRegionText(profile, requestId) {
|
||||
var api = getApi();
|
||||
var regionCode = getFinalRegionCode(profile);
|
||||
var path;
|
||||
var text = '待填写';
|
||||
|
||||
if (!regionCode || !api || !api.regionPath) {
|
||||
renderProfile({ regionText: text });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
path = await api.regionPath(regionCode);
|
||||
text = buildRegionPathText(path) || text;
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
}
|
||||
|
||||
if (requestId === undefined || requestId === latestProfileRequestId) {
|
||||
renderProfile({ regionText: text });
|
||||
}
|
||||
}
|
||||
|
||||
async function fillRegionPicker(profile) {
|
||||
var regionPages = root.RegionPages;
|
||||
|
||||
if (!regionPages || !regionPages.applyProfileRegionSelection) return;
|
||||
|
||||
try {
|
||||
await regionPages.applyProfileRegionSelection({
|
||||
provinceCode: trimOrUndefined(profile && profile.provinceCode),
|
||||
cityCode: trimOrUndefined(profile && profile.cityCode),
|
||||
districtCode: trimOrUndefined(profile && profile.districtCode)
|
||||
});
|
||||
} catch (error) {
|
||||
var api = getApi();
|
||||
|
||||
if (!redirectUnauthorized(api, error)) showMessage(error.message || '地区资料加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
var latestProfileRequestId = 0;
|
||||
|
||||
async function loadProfile() {
|
||||
var api = getApi();
|
||||
var profile;
|
||||
var requestId = latestProfileRequestId + 1;
|
||||
|
||||
latestProfileRequestId = requestId;
|
||||
|
||||
if (!api || !documentRef || !documentRef.querySelector('[data-profile-page]')) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
|
||||
try {
|
||||
profile = await api.currentProfile();
|
||||
if (requestId !== latestProfileRequestId) return;
|
||||
renderProfile(buildProfileView(profile));
|
||||
fillProfileForm(profile);
|
||||
fillRegionPicker(profile);
|
||||
renderRegionText(profile, requestId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
showMessage(error.message || '个人资料加载失败');
|
||||
@@ -240,11 +278,13 @@
|
||||
}
|
||||
|
||||
return {
|
||||
pick: pick,
|
||||
getAvatarText: getAvatarText,
|
||||
getFinalRegionCode: getFinalRegionCode,
|
||||
buildRegionText: buildRegionText,
|
||||
buildProfileView: buildProfileView,
|
||||
buildProfileUpdateBody: buildProfileUpdateBody,
|
||||
shouldRedirectToHome: shouldRedirectToHome,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
reloadProfile: loadProfile,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
+132
-48
@@ -17,29 +17,11 @@
|
||||
var documentRef = root.document;
|
||||
|
||||
function normalizeList(data) {
|
||||
// 行政区划接口使用通用 ListResult,兼容常见数组包裹字段。
|
||||
// ApiClient 已解包 PC ListResult;组件只接收正式的数组结果。
|
||||
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 escapeHtml(value) {
|
||||
// 地区名称和编码进入 option 前统一转义。
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
@@ -51,23 +33,17 @@
|
||||
}
|
||||
|
||||
function getRegionCode(item) {
|
||||
return pick(item, ['regionCode', 'code', 'value'], '');
|
||||
return item && item.regionCode || '';
|
||||
}
|
||||
|
||||
function getRegionName(item) {
|
||||
return pick(item, ['regionName', 'name', 'label'], '未命名地区');
|
||||
return item && item.regionName || '未命名地区';
|
||||
}
|
||||
|
||||
function getRegionLevel(item) {
|
||||
var source = item || {};
|
||||
var level = Number(pick(source, ['level', 'regionLevel'], 0));
|
||||
var code = getRegionCode(source);
|
||||
var level = Number(item && item.regionLevel);
|
||||
|
||||
if (level >= 1 && level <= 3) return level;
|
||||
if (!/^\d{6}$/.test(code)) return 0;
|
||||
if (/0000$/.test(code)) return 1;
|
||||
if (/00$/.test(code)) return 2;
|
||||
return 3;
|
||||
return Number.isInteger(level) ? level : 0;
|
||||
}
|
||||
|
||||
function buildRegionSelection(items) {
|
||||
@@ -125,11 +101,35 @@
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function redirectToLogin() {
|
||||
if (!root.location) return;
|
||||
if (typeof root.location.replace === 'function') {
|
||||
root.location.replace('login.html');
|
||||
return;
|
||||
}
|
||||
root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
redirectToLogin();
|
||||
return true;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
if (!documentRef && !rootNode) return null;
|
||||
return (rootNode || documentRef).querySelector(selector);
|
||||
}
|
||||
|
||||
function queryAll(selector, rootNode) {
|
||||
if (!documentRef && !rootNode) return [];
|
||||
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
root.alert(message);
|
||||
if (root.alert) root.alert(message);
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
@@ -162,6 +162,7 @@
|
||||
|
||||
function resetSelect(select, placeholder) {
|
||||
if (!select) return;
|
||||
select.__regionRequestVersion = (select.__regionRequestVersion || 0) + 1;
|
||||
select.innerHTML = '<option value="">' + escapeHtml(placeholder) + '</option>';
|
||||
select.disabled = true;
|
||||
}
|
||||
@@ -179,31 +180,53 @@
|
||||
|
||||
async function loadChildrenInto(select, parentCode, placeholder) {
|
||||
var api = getApi();
|
||||
var requestVersion;
|
||||
var items;
|
||||
|
||||
if (!api || !select) return;
|
||||
setOptions(select, await api.regionChildren(parentCode || '0'), placeholder);
|
||||
if (!api || !select) return false;
|
||||
requestVersion = (select.__regionRequestVersion || 0) + 1;
|
||||
select.__regionRequestVersion = requestVersion;
|
||||
select.disabled = true;
|
||||
|
||||
try {
|
||||
items = await api.regionChildren(parentCode || '0');
|
||||
if (select.__regionRequestVersion !== requestVersion) return false;
|
||||
setOptions(select, items, placeholder);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (select.__regionRequestVersion === requestVersion) {
|
||||
select.innerHTML = '<option value="">' + escapeHtml(placeholder) + '</option>';
|
||||
select.disabled = true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function initPicker(picker) {
|
||||
var province = query('[data-region-level="province"]', picker);
|
||||
var city = query('[data-region-level="city"]', picker);
|
||||
var district = query('[data-region-level="district"]', picker);
|
||||
function handlePickerError(error) {
|
||||
var api = getApi();
|
||||
|
||||
resetSelect(city, '请选择市');
|
||||
resetSelect(district, '请选择区县');
|
||||
await loadChildrenInto(province, '0', '请选择省');
|
||||
if (!redirectUnauthorized(api, error)) showMessage(error.message || '地区加载失败');
|
||||
}
|
||||
|
||||
province.addEventListener('change', async function () {
|
||||
function bindPickerEvents(picker, province, city, district) {
|
||||
if (picker.__regionEventsBound) return;
|
||||
picker.__regionEventsBound = true;
|
||||
|
||||
province.addEventListener('change', function () {
|
||||
resetSelect(city, '请选择市');
|
||||
resetSelect(district, '请选择区县');
|
||||
updateTargetCode(picker);
|
||||
if (province.value) await loadChildrenInto(city, province.value, '请选择市');
|
||||
if (!province.value) return;
|
||||
|
||||
loadChildrenInto(city, province.value, '请选择市').catch(handlePickerError);
|
||||
});
|
||||
|
||||
city.addEventListener('change', async function () {
|
||||
city.addEventListener('change', function () {
|
||||
resetSelect(district, '请选择区县');
|
||||
updateTargetCode(picker);
|
||||
if (city.value) await loadChildrenInto(district, city.value, '请选择区县');
|
||||
if (!city.value) return;
|
||||
|
||||
loadChildrenInto(district, city.value, '请选择区县').catch(handlePickerError);
|
||||
});
|
||||
|
||||
district.addEventListener('change', function () {
|
||||
@@ -211,13 +234,40 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initPicker(picker) {
|
||||
var province = query('[data-region-level="province"]', picker);
|
||||
var city = query('[data-region-level="city"]', picker);
|
||||
var district = query('[data-region-level="district"]', picker);
|
||||
|
||||
if (!picker || !province || !city || !district) return Promise.resolve();
|
||||
if (picker.__regionInitPromise) return picker.__regionInitPromise;
|
||||
|
||||
resetSelect(city, '请选择市');
|
||||
resetSelect(district, '请选择区县');
|
||||
bindPickerEvents(picker, province, city, district);
|
||||
picker.__regionInitPromise = loadChildrenInto(province, '0', '请选择省').catch(function (error) {
|
||||
picker.__regionInitPromise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return picker.__regionInitPromise;
|
||||
}
|
||||
|
||||
async function applyRegionSelection(picker, selection) {
|
||||
var province = query('[data-region-level="province"]', picker);
|
||||
var city = query('[data-region-level="city"]', picker);
|
||||
var district = query('[data-region-level="district"]', picker);
|
||||
var values = selection || {};
|
||||
|
||||
if (!values.provinceCode) return;
|
||||
await initPicker(picker);
|
||||
|
||||
if (!values.provinceCode) {
|
||||
province.value = '';
|
||||
resetSelect(city, '请选择市');
|
||||
resetSelect(district, '请选择区县');
|
||||
updateTargetCode(picker);
|
||||
return;
|
||||
}
|
||||
|
||||
province.value = values.provinceCode;
|
||||
resetSelect(city, '请选择市');
|
||||
@@ -236,6 +286,12 @@
|
||||
updateTargetCode(picker);
|
||||
}
|
||||
|
||||
function applyProfileRegionSelection(selection) {
|
||||
return Promise.all(queryAll('[data-region-picker]').map(function (picker) {
|
||||
return applyRegionSelection(picker, selection);
|
||||
}));
|
||||
}
|
||||
|
||||
function renderSearchResults(container, items) {
|
||||
var list = normalizeList(items);
|
||||
|
||||
@@ -254,7 +310,7 @@
|
||||
var results = query('[data-region-search-results]', form.parentNode);
|
||||
var pathText = query('[data-region-search-path]', form.parentNode);
|
||||
|
||||
if (!api) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!values.regionKeyword || !values.regionKeyword.trim()) {
|
||||
showMessage('请输入地区名称或编码');
|
||||
return;
|
||||
@@ -268,6 +324,7 @@
|
||||
}));
|
||||
if (pathText) pathText.textContent = '请选择一个地区';
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (pathText) pathText.textContent = error.message || '地区搜索失败';
|
||||
showMessage(error.message || '地区搜索失败');
|
||||
}
|
||||
@@ -282,7 +339,7 @@
|
||||
var detail;
|
||||
var path;
|
||||
|
||||
if (!api || !code || !picker) return;
|
||||
if (!code || !picker || redirectUnauthorized(api)) return;
|
||||
|
||||
if (pathText) pathText.textContent = '正在定位地区...';
|
||||
try {
|
||||
@@ -291,6 +348,7 @@
|
||||
await applyRegionSelection(picker, buildRegionSelection(path));
|
||||
if (pathText) pathText.textContent = buildPathText(path);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (pathText) pathText.textContent = error.message || '地区定位失败';
|
||||
showMessage(error.message || '地区定位失败');
|
||||
}
|
||||
@@ -300,21 +358,36 @@
|
||||
var api = getApi();
|
||||
var values = getFormValues(form);
|
||||
var body = buildProfileRegionBody(values);
|
||||
var status = query('[data-region-profile-status]', form);
|
||||
|
||||
if (!api) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!body.provinceCode) {
|
||||
showMessage('请选择省份');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
if (status) status.textContent = '保存中...';
|
||||
await api.updateProfile(body);
|
||||
if (status) status.textContent = '已保存';
|
||||
showMessage('地区资料已保存');
|
||||
if (root.ProfilePages && root.ProfilePages.reloadProfile) await root.ProfilePages.reloadProfile();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (status) status.textContent = '保存失败';
|
||||
showMessage(error.message || '地区资料保存失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
}
|
||||
}
|
||||
|
||||
function setSubmitting(form, isSubmitting) {
|
||||
queryAll('button[type="submit"]', form).forEach(function (button) {
|
||||
button.disabled = isSubmitting;
|
||||
});
|
||||
}
|
||||
|
||||
function bindProfileForms() {
|
||||
queryAll('[data-region-profile-form]').forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
@@ -342,10 +415,15 @@
|
||||
}
|
||||
|
||||
function init() {
|
||||
var api;
|
||||
|
||||
if (!documentRef) return;
|
||||
if (!queryAll('[data-region-picker]').length && !queryAll('[data-region-search-form]').length) return;
|
||||
api = getApi();
|
||||
if (redirectUnauthorized(api)) return;
|
||||
|
||||
queryAll('[data-region-picker]').forEach(function (picker) {
|
||||
initPicker(picker);
|
||||
initPicker(picker).catch(handlePickerError);
|
||||
});
|
||||
bindProfileForms();
|
||||
bindSearchForms();
|
||||
@@ -353,11 +431,17 @@
|
||||
|
||||
return {
|
||||
normalizeList: normalizeList,
|
||||
getRegionCode: getRegionCode,
|
||||
getRegionName: getRegionName,
|
||||
getRegionLevel: getRegionLevel,
|
||||
buildRegionOption: buildRegionOption,
|
||||
buildPathText: buildPathText,
|
||||
buildRegionSelection: buildRegionSelection,
|
||||
getFinalRegionCode: getFinalRegionCode,
|
||||
buildProfileRegionBody: buildProfileRegionBody,
|
||||
applyRegionSelection: applyRegionSelection,
|
||||
applyProfileRegionSelection: applyProfileRegionSelection,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
+213
-35
@@ -16,9 +16,11 @@
|
||||
|
||||
var documentRef = root.document;
|
||||
var CAPTCHA_SCENES = {
|
||||
password: 'WEB_H5_CHANGE_PASSWORD',
|
||||
phone: 'WEB_H5_CHANGE_PHONE'
|
||||
phone: 'PC_PHONE_CHANGE',
|
||||
deactivate: 'PC_ACCOUNT_DEACTIVATE'
|
||||
};
|
||||
var SMS_CODE_COOLDOWN_SECONDS = 60;
|
||||
var boundPhone = '';
|
||||
|
||||
function getCaptchaScene(formType) {
|
||||
return CAPTCHA_SCENES[formType] || '';
|
||||
@@ -31,20 +33,13 @@
|
||||
return value;
|
||||
}
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text = String(value === undefined || value === null ? '' : value).trim();
|
||||
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function buildPasswordChangeBody(values, hashFn) {
|
||||
var source = values || {};
|
||||
var hash = hashFn || hashPassword;
|
||||
|
||||
return {
|
||||
oldPassword: hash(String(source.oldPassword || '').trim()),
|
||||
newPassword: hash(String(source.newPassword || '').trim()),
|
||||
validToken: trimOrUndefined(source.validToken)
|
||||
newPassword: hash(String(source.newPassword || '').trim())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,19 +47,16 @@
|
||||
var source = values || {};
|
||||
|
||||
return {
|
||||
newPhone: String(source.newPhone || '').trim(),
|
||||
smsCode: String(source.smsCode || '').trim(),
|
||||
validToken: trimOrUndefined(source.validToken)
|
||||
phone: String(source.phone || '').trim(),
|
||||
smsCode: String(source.smsCode || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function buildDeactivateBody(values, hashFn) {
|
||||
function buildDeactivateBody(values) {
|
||||
var source = values || {};
|
||||
var hash = hashFn || hashPassword;
|
||||
|
||||
return {
|
||||
password: hash(String(source.password || '').trim()),
|
||||
reason: trimOrUndefined(source.reason)
|
||||
smsCode: String(source.smsCode || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,12 +64,43 @@
|
||||
return String(value || '').trim() === '确认注销';
|
||||
}
|
||||
|
||||
function isPhone(value) {
|
||||
return /^1\d{10}$/.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
function isSmsCode(value) {
|
||||
return /^\d{4,6}$/.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
function getApi() {
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
redirectToLogin(api);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureCaptcha(form, formType, subject) {
|
||||
// 改密和换绑必须先取得与当前业务场景绑定的验证码票据。
|
||||
// 发送敏感业务短信前,先取得与当前场景绑定的验证码票据。
|
||||
var captcha = root.CaptchaPages;
|
||||
var sceneCode = getCaptchaScene(formType);
|
||||
|
||||
@@ -126,30 +149,110 @@
|
||||
if (status) status.textContent = message;
|
||||
}
|
||||
|
||||
function setSubmitting(form, isSubmitting) {
|
||||
queryAll('button[type="submit"]', form).forEach(function (button) {
|
||||
button.disabled = isSubmitting;
|
||||
});
|
||||
}
|
||||
|
||||
function clearCaptchaToken(form) {
|
||||
var field = query('input[name="validToken"]', form);
|
||||
|
||||
if (field) field.value = '';
|
||||
}
|
||||
|
||||
function startSmsCooldown(button) {
|
||||
var originalText;
|
||||
var remaining = SMS_CODE_COOLDOWN_SECONDS;
|
||||
var interval;
|
||||
|
||||
if (!button || typeof root.setInterval !== 'function') return;
|
||||
if (button.__smsCooldownTimer && typeof root.clearInterval === 'function') {
|
||||
root.clearInterval(button.__smsCooldownTimer);
|
||||
}
|
||||
|
||||
originalText = button.getAttribute('data-sms-code-label') || button.textContent.trim();
|
||||
button.setAttribute('data-sms-code-label', originalText);
|
||||
button.disabled = true;
|
||||
button.textContent = remaining + ' 秒后重试';
|
||||
interval = root.setInterval(function () {
|
||||
remaining -= 1;
|
||||
if (remaining > 0) {
|
||||
button.textContent = remaining + ' 秒后重试';
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof root.clearInterval === 'function') root.clearInterval(interval);
|
||||
button.__smsCooldownTimer = null;
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
}, 1000);
|
||||
button.__smsCooldownTimer = interval;
|
||||
}
|
||||
|
||||
function maskPhone(phone) {
|
||||
var value = String(phone || '').trim();
|
||||
|
||||
return isPhone(value) ? value.slice(0, 3) + '****' + value.slice(-4) : '';
|
||||
}
|
||||
|
||||
async function loadBoundPhone() {
|
||||
var api = getApi();
|
||||
var profile;
|
||||
var message;
|
||||
|
||||
if (!api || !documentRef) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
|
||||
try {
|
||||
profile = await api.currentProfile();
|
||||
boundPhone = String(profile && profile.phone || '').trim();
|
||||
message = isPhone(boundPhone)
|
||||
? '验证码将发送至 ' + maskPhone(boundPhone)
|
||||
: '未获取到已绑定手机号';
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
node.textContent = message;
|
||||
});
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
node.textContent = '读取已绑定手机号失败';
|
||||
});
|
||||
showMessage(error.message || '账号资料加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPassword(form) {
|
||||
var api = getApi();
|
||||
var values = getFormValues(form);
|
||||
|
||||
if (!api) return;
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!values.oldPassword || !values.newPassword) {
|
||||
showMessage('请填写原密码和新密码');
|
||||
return;
|
||||
}
|
||||
if (String(values.newPassword).length < 6) {
|
||||
showMessage('新密码至少需要 6 位');
|
||||
return;
|
||||
}
|
||||
if (values.newPassword !== values.confirmPassword) {
|
||||
showMessage('两次新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await ensureCaptcha(form, 'password')) return;
|
||||
values = getFormValues(form);
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '提交中...');
|
||||
await api.changePassword(buildPasswordChangeBody(values));
|
||||
setStatus(form, '密码已修改');
|
||||
showMessage('密码已修改');
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '修改失败');
|
||||
showMessage(error.message || '密码修改失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,22 +260,37 @@
|
||||
var api = getApi();
|
||||
var body = buildPhoneChangeBody(getFormValues(form));
|
||||
|
||||
if (!api) return;
|
||||
if (!body.newPhone || !body.smsCode) {
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!body.phone || !body.smsCode) {
|
||||
showMessage('请填写新手机号和验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await ensureCaptcha(form, 'phone', body.newPhone)) return;
|
||||
body = buildPhoneChangeBody(getFormValues(form));
|
||||
if (!isPhone(body.phone)) {
|
||||
showMessage('请输入正确的新手机号');
|
||||
return;
|
||||
}
|
||||
if (!isSmsCode(body.smsCode)) {
|
||||
showMessage('请输入正确的短信验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '提交中...');
|
||||
await api.changePhone(body);
|
||||
boundPhone = body.phone;
|
||||
queryAll('[data-security-bound-phone]').forEach(function (node) {
|
||||
node.textContent = '验证码将发送至 ' + maskPhone(boundPhone);
|
||||
});
|
||||
setStatus(form, '手机号已换绑');
|
||||
showMessage('手机号已换绑');
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '换绑失败');
|
||||
showMessage(error.message || '手机号换绑失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,26 +300,60 @@
|
||||
var form = query('[data-security-form="phone"]');
|
||||
var values;
|
||||
|
||||
if (!api || !form) return;
|
||||
if (!form || redirectUnauthorized(api)) return;
|
||||
values = getFormValues(form);
|
||||
if (!/^1[3-9]\d{9}$/.test(String(values.newPhone || '').trim())) {
|
||||
if (!isPhone(values.phone)) {
|
||||
showMessage('请输入正确的新手机号');
|
||||
return;
|
||||
}
|
||||
if (!await ensureCaptcha(form, 'phone', values.newPhone)) return;
|
||||
if (!await ensureCaptcha(form, 'phone', values.phone)) return;
|
||||
values = getFormValues(form);
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
await api.sendSmsCode({
|
||||
phone: values.newPhone,
|
||||
phone: values.phone,
|
||||
sceneCode: getCaptchaScene('phone'),
|
||||
validToken: values.validToken || ''
|
||||
});
|
||||
clearCaptchaToken(form);
|
||||
showMessage('验证码已发送');
|
||||
startSmsCooldown(button);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
clearCaptchaToken(form);
|
||||
showMessage(error.message || '验证码发送失败');
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendDeactivateCode(button) {
|
||||
var api = getApi();
|
||||
var form = query('[data-security-form="deactivate"]');
|
||||
var values;
|
||||
|
||||
if (!form || redirectUnauthorized(api)) return;
|
||||
if (!isPhone(boundPhone)) {
|
||||
showMessage('未获取到已绑定手机号,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
if (!await ensureCaptcha(form, 'deactivate', boundPhone)) return;
|
||||
values = getFormValues(form);
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
await api.sendSmsCode({
|
||||
phone: boundPhone,
|
||||
sceneCode: getCaptchaScene('deactivate'),
|
||||
validToken: values.validToken || ''
|
||||
});
|
||||
clearCaptchaToken(form);
|
||||
showMessage('验证码已发送');
|
||||
startSmsCooldown(button);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
clearCaptchaToken(form);
|
||||
showMessage(error.message || '验证码发送失败');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
@@ -210,9 +362,9 @@
|
||||
var api = getApi();
|
||||
var values = getFormValues(form);
|
||||
|
||||
if (!api) return;
|
||||
if (!values.password) {
|
||||
showMessage('请填写当前密码');
|
||||
if (redirectUnauthorized(api)) return;
|
||||
if (!isSmsCode(values.smsCode)) {
|
||||
showMessage('请输入正确的短信验证码');
|
||||
return;
|
||||
}
|
||||
if (!isDangerConfirmed(values.confirmText)) {
|
||||
@@ -222,11 +374,17 @@
|
||||
if (root.confirm && !root.confirm('账号注销后将退出登录,确认继续?')) return;
|
||||
|
||||
try {
|
||||
setSubmitting(form, true);
|
||||
setStatus(form, '提交中...');
|
||||
await api.deactivateAccount(buildDeactivateBody(values));
|
||||
showMessage('账号已提交注销');
|
||||
root.location.href = 'index.html';
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
setStatus(form, '注销失败');
|
||||
showMessage(error.message || '账号注销失败');
|
||||
} finally {
|
||||
setSubmitting(form, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +420,7 @@
|
||||
|
||||
documentRef.addEventListener('click', function (event) {
|
||||
var codeButton = event.target.closest('[data-security-send-code]');
|
||||
var deactivateCodeButton = event.target.closest('[data-security-send-deactivate-code]');
|
||||
var button = event.target.closest('[data-security-logout]');
|
||||
|
||||
if (codeButton) {
|
||||
@@ -269,16 +428,32 @@
|
||||
sendPhoneCode(codeButton);
|
||||
return;
|
||||
}
|
||||
if (deactivateCodeButton) {
|
||||
event.preventDefault();
|
||||
sendDeactivateCode(deactivateCodeButton);
|
||||
return;
|
||||
}
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
function bindCaptchaTokenInvalidation() {
|
||||
queryAll('[data-security-form] input[name="phone"]').forEach(function (field) {
|
||||
field.addEventListener('input', function () {
|
||||
clearCaptchaToken(field.closest('[data-security-form]'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!documentRef || !query('[data-security-page]')) return;
|
||||
if (redirectUnauthorized(getApi())) return;
|
||||
|
||||
bindActions();
|
||||
bindCaptchaTokenInvalidation();
|
||||
loadBoundPhone();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -287,6 +462,9 @@
|
||||
buildDeactivateBody: buildDeactivateBody,
|
||||
getCaptchaScene: getCaptchaScene,
|
||||
isDangerConfirmed: isDangerConfirmed,
|
||||
isPhone: isPhone,
|
||||
isSmsCode: isSmsCode,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
|
||||
function normalizeUploadResult(data) {
|
||||
var source = data || {};
|
||||
var ossId = source.ossId;
|
||||
|
||||
return {
|
||||
ossId: String(source.ossId || ''),
|
||||
// int64 OSS ID 不能经由 Number 转换,直接保留服务端原始文本。
|
||||
ossId: ossId === undefined || ossId === null ? '' : String(ossId),
|
||||
fileName: source.fileName || source.originalName || '',
|
||||
url: source.url || ''
|
||||
};
|
||||
@@ -38,6 +40,24 @@
|
||||
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function redirectUnauthorized(api, error) {
|
||||
if (!shouldRedirectToLogin(api, error)) return false;
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
if (!root.location) return true;
|
||||
if (typeof root.location.replace === 'function') {
|
||||
root.location.replace('login.html');
|
||||
return true;
|
||||
}
|
||||
root.location.href = 'login.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
function query(selector, rootNode) {
|
||||
return (rootNode || documentRef).querySelector(selector);
|
||||
}
|
||||
@@ -64,7 +84,7 @@
|
||||
var status = query(input.getAttribute('data-upload-status'));
|
||||
var result;
|
||||
|
||||
if (!api || !file || !target) return;
|
||||
if (!file || !target || redirectUnauthorized(api)) return;
|
||||
input.disabled = true;
|
||||
|
||||
try {
|
||||
@@ -73,6 +93,7 @@
|
||||
target.value = result.ossId;
|
||||
if (status) status.textContent = buildUploadStatus(result.fileName || file.name, result.ossId);
|
||||
} catch (error) {
|
||||
if (redirectUnauthorized(api, error)) return;
|
||||
if (status) status.textContent = error.message || '上传失败';
|
||||
showMessage(error.message || '上传失败');
|
||||
} finally {
|
||||
@@ -101,6 +122,7 @@
|
||||
buildUploadStatus: buildUploadStatus,
|
||||
getUploadMode: getUploadMode,
|
||||
uploadFileForPage: uploadFileForPage,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user