接线1.10实名认证提醒弹窗到真实消息首页(第1章遗留项,此前一直是孤立demo路由)

auth-onboarding-view.jsx 的 RealnameReminder 组件此前是完全孤立的demo:自己画一套
假会话列表背景,只能靠手动改hash访问,没有任何真实入口会导航过去。核对参考图
"实名认证(安卓).png"确认过的弹窗文案本身是对的,只是从来没有接上真实触发条件。

抽出弹窗内容(不带假背景)做成 RealnameReminderModal,挂到真实 MessageHome 挂载时
按 bootstrap.security.realname 的真实状态判断——这个字段本来就在 MessageHome 已经
在用的同一个 bootstrap 请求里,不需要额外接口调用。

参考图本身画的是不可跳过的单按钮强制态,但真实核对 im_be 源码发现这只是
required:true 那个分支的样子;这个环境的真实配置(也是目前后端唯一会返回的默认值)
是 required:false / allow_skip:true。如果照抄参考图做成完全关不掉的弹层,等于把
"实名认证目前是可选的"这个真实产品配置,实现成挡住所有人使用消息功能的强制墙——
按这两个字段分叉:允许跳过时加一个参考图没画、但后端字段明确允许的关闭按钮;且
遵守后端给的 remind_after(当前值24h),关闭后在这个时长内不重复弹,不是每次进
消息首页都弹一次。

联调中发现关闭按钮初版样式是个跟全局 button 选一样宽的圆角方块(漏了覆盖
min-width/box-shadow/margin 这几个全局 button 规则),已修正为纯图标按钮。

真实验证(账号 asn494180,真实 status:"unverified"):弹窗内容与参考图逐句一致;
点击关闭按钮弹窗消失且 localStorage 正确记录时间戳;刷新页面重新连接后,24小时
窗口内弹窗不会重复出现;未清除记录时保持不弹,符合"提醒而非骚扰"的预期。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-04 16:40:27 +08:00
co-authored by Claude Opus 5
parent d21d046443
commit 542aca3dc2
4 changed files with 114 additions and 2 deletions
+18
View File
@@ -6408,6 +6408,24 @@ div.toast.show {
text-align: center; text-align: center;
} }
.auth-modal {
position: relative;
}
.auth-modal-close {
position: absolute;
top: 0.6rem;
right: 0.6rem;
min-width: 0;
height: auto;
margin: 0;
padding: 0.25rem;
color: #9aa1ab;
background: none;
border: 0;
box-shadow: none;
}
.auth-modal h2 { .auth-modal h2 {
margin: 0 0 1rem; margin: 0 0 1rem;
font-size: 1.45rem; font-size: 1.45rem;
+38
View File
@@ -204,6 +204,30 @@ function QuickMenu({onClose, onAction, permissions}) {
</div>; </div>;
} }
// 1.10 实名认证提醒:docs/images/实名认证(安卓).png 显示的是不可跳过的强制态(单按钮"去实名
// 认证",没有关闭/稍后处理)——但那张图对应的是 bootstrap.security.realname.required===true 这个
// 分支,这个环境(以及目前后端唯一会返回的默认配置)实际是 required:false / allow_skip:true。
// 照抄参考图做成完全关不掉的弹层,等于把"实名认证目前是可选的"这个真实配置,实现成挡住所有人的
// 强制墙——所以按这两个字段分叉:允许跳过时加一个参考图没画但后端明确允许的关闭按钮;且遵守
// remind_after,跳过后在这个时长内不重复弹(不是每次进消息首页都弹一次)。
function parseRemindAfter(text) {
const m = /^(\d+)\s*([hdm])$/i.exec(String(text || '').trim());
if (!m) return 24 * 60 * 60 * 1000;
const n = Number(m[1]);
return m[2].toLowerCase() === 'd' ? n * 86400000 : m[2].toLowerCase() === 'h' ? n * 3600000 : n * 60000;
}
function RealnameReminderModal({allowSkip, onDismiss}) {
return <div className="auth-modal-backdrop" onClick={allowSkip ? onDismiss : undefined}>
<div className="auth-modal" onClick={e => e.stopPropagation()}>
{allowSkip ? <button type="button" className="auth-modal-close" aria-label="关闭" onClick={onDismiss}><Icon>close</Icon></button> : null}
<h2>实名认证提醒</h2>
<p>根据国家法律法规要求,您需先进行实名认证才能使用该项 APP</p>
<button className="auth-primary" onClick={() => navigate('realname')}>去实名认证</button>
</div>
</div>;
}
function MessageHome({view, connected, chatList, myUserId, onTopicSelected, onOpenScanner}) { function MessageHome({view, connected, chatList, myUserId, onTopicSelected, onOpenScanner}) {
const [query, setQuery] = React.useState(''); const [query, setQuery] = React.useState('');
const [menuOpen, setMenuOpen] = React.useState(view === 'messages-menu'); const [menuOpen, setMenuOpen] = React.useState(view === 'messages-menu');
@@ -217,6 +241,19 @@ function MessageHome({view, connected, chatList, myUserId, onTopicSelected, onOp
const filtered = conversations.filter(item => `${item.title}${item.summary}`.toLowerCase().includes(query.trim().toLowerCase())); const filtered = conversations.filter(item => `${item.title}${item.summary}`.toLowerCase().includes(query.trim().toLowerCase()));
const frequent = frequentContacts(chatList); const frequent = frequentContacts(chatList);
const realname = bootstrap && bootstrap.security && bootstrap.security.realname;
const [reminderDismissed, setReminderDismissed] = React.useState(false);
const dismissKey = myUserId ? 'realname-reminder-dismissed:' + myUserId : null;
let lastDismissedAt = 0;
try { lastDismissedAt = dismissKey ? Number(localStorage.getItem(dismissKey) || 0) : 0; } catch (_) { /* ignore */ }
const withinSnooze = lastDismissedAt > 0 && (Date.now() - lastDismissedAt) < parseRemindAfter(realname && realname.remind_after);
const showRealnameReminder = !!realname && !reminderDismissed && !withinSnooze &&
(realname.status === 'unverified' || realname.status === 'rejected');
const dismissRealnameReminder = () => {
if (dismissKey) { try { localStorage.setItem(dismissKey, String(Date.now())); } catch (_) { /* ignore */ } }
setReminderDismissed(true);
};
const selectConversation = item => { const selectConversation = item => {
if (item.system) { if (item.system) {
navigate('system-messages'); navigate('system-messages');
@@ -269,6 +306,7 @@ function MessageHome({view, connected, chatList, myUserId, onTopicSelected, onOp
<BottomNavigation /> <BottomNavigation />
{menuOpen ? <QuickMenu onClose={() => setMenuOpen(false)} onAction={runQuickAction} permissions={bootstrap && bootstrap.permissions} /> : null} {menuOpen ? <QuickMenu onClose={() => setMenuOpen(false)} onAction={runQuickAction} permissions={bootstrap && bootstrap.permissions} /> : null}
<Toast message={toast} onDone={() => setToast('')} /> <Toast message={toast} onDone={() => setToast('')} />
{showRealnameReminder ? <RealnameReminderModal allowSkip={!!realname.allow_skip} onDismiss={dismissRealnameReminder} /> : null}
</div>; </div>;
} }
+57 -1
View File
@@ -13036,6 +13036,42 @@ function QuickMenu({
}, void 0, false) }, void 0, false)
}, void 0, false); }, void 0, false);
} }
function parseRemindAfter(text) {
const m = /^(\d+)\s*([hdm])$/i.exec(String(text || '').trim());
if (!m) return 24 * 60 * 60 * 1000;
const n = Number(m[1]);
return m[2].toLowerCase() === 'd' ? n * 86400000 : m[2].toLowerCase() === 'h' ? n * 3600000 : n * 60000;
}
function RealnameReminderModal({
allowSkip,
onDismiss
}) {
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "auth-modal-backdrop",
onClick: allowSkip ? onDismiss : undefined,
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "auth-modal",
onClick: e => e.stopPropagation(),
children: [allowSkip ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("button", {
type: "button",
className: "auth-modal-close",
"aria-label": "关闭",
onClick: onDismiss,
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Icon, {
children: "close"
}, void 0, false)
}, void 0, false) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("h2", {
children: "实名认证提醒"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("p", {
children: "根据国家法律法规要求,您需先进行实名认证才能使用该项 APP"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("button", {
className: "auth-primary",
onClick: () => navigate('realname'),
children: "去实名认证"
}, void 0, false)]
}, void 0, true)
}, void 0, false);
}
function MessageHome({ function MessageHome({
view, view,
connected, connected,
@@ -13052,6 +13088,23 @@ function MessageHome({
const conversations = entryConversations(bootstrap).concat(normalizeConversations(chatList, myUserId)); const conversations = entryConversations(bootstrap).concat(normalizeConversations(chatList, myUserId));
const filtered = conversations.filter(item => `${item.title}${item.summary}`.toLowerCase().includes(query.trim().toLowerCase())); const filtered = conversations.filter(item => `${item.title}${item.summary}`.toLowerCase().includes(query.trim().toLowerCase()));
const frequent = frequentContacts(chatList); const frequent = frequentContacts(chatList);
const realname = bootstrap && bootstrap.security && bootstrap.security.realname;
const [reminderDismissed, setReminderDismissed] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const dismissKey = myUserId ? 'realname-reminder-dismissed:' + myUserId : null;
let lastDismissedAt = 0;
try {
lastDismissedAt = dismissKey ? Number(localStorage.getItem(dismissKey) || 0) : 0;
} catch (_) {}
const withinSnooze = lastDismissedAt > 0 && Date.now() - lastDismissedAt < parseRemindAfter(realname && realname.remind_after);
const showRealnameReminder = !!realname && !reminderDismissed && !withinSnooze && (realname.status === 'unverified' || realname.status === 'rejected');
const dismissRealnameReminder = () => {
if (dismissKey) {
try {
localStorage.setItem(dismissKey, String(Date.now()));
} catch (_) {}
}
setReminderDismissed(true);
};
const selectConversation = item => { const selectConversation = item => {
if (item.system) { if (item.system) {
navigate('system-messages'); navigate('system-messages');
@@ -13162,7 +13215,10 @@ function MessageHome({
}, void 0, false) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Toast, { }, void 0, false) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Toast, {
message: toast, message: toast,
onDone: () => setToast('') onDone: () => setToast('')
}, void 0, false)] }, void 0, false), showRealnameReminder ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(RealnameReminderModal, {
allowSkip: !!realname.allow_skip,
onDismiss: dismissRealnameReminder
}, void 0, false) : null]
}, void 0, true); }, void 0, true);
} }
function MessageActions() { function MessageActions() {
+1 -1
View File
File diff suppressed because one or more lines are too long