fix(导航): 排查全项目后修复其余同类tab切换污染历史记录问题

上一提交(e61eb402)只修了"我的收藏"/"查找聊天记录"两处,用户要求排查其他页面是否
也有同样问题。全项目排查<nav>/tabbar/active态切换元素,逐一确认onClick是否直接调用
HashNavigation.navigateTo()(每次都push一条历史记录),找到5处同类实例并修复:

- message-center-view.jsx / workplace-collaboration-view.jsx:底部导航"工作台"<->"消息"
  两个tab——这是全应用唯一的根级tab bar,也是点击频率最高的一处,之前同样每次切换都
  push一条历史记录。
- workplace-collaboration-view.jsx:日报/月报列表页的"我的xx"<->"团队统计"切换(同一个
  组件、同一个页面,只是team boolean不同),同样的问题。
- new-topic-view.jsx:"开始新会话"页的搜索/新群组/通过id三个tab。
- wallpapers.jsx:"壁纸"设置页的图案/图片两个tab。

均改用上一提交新增的HashNavigation.navigateReplace()。已在浏览器逐一实测:连续切换
多次tab后,无论真实浏览器返回还是应用自己的返回按钮,都能一次性返回到进入该页面之前
的真实上一页。

以下经排查确认不受影响,未改动:group-management-view.jsx的群聊分类tab(我创建的/
管理的/加入的)和contact-management-view.jsx的字母索引锚点均只用本地state/scrollIntoView,
不涉及hash跳转;auth-onboarding-view.jsx的登录<->注册切换、new-topic-view.jsx提交后的
三处跳转是真实的"离开当前页"操作,保留push语义符合预期。
This commit is contained in:
Your Name
2026-09-06 11:16:01 +08:00
parent e61eb40250
commit 623f7dc40f
8 changed files with 43 additions and 18 deletions
+10 -1
View File
@@ -20,6 +20,15 @@ const navigate = (view, topic) => {
HashNavigation.navigateTo(hash); HashNavigation.navigateTo(hash);
}; };
// BottomNavigation's 消息<->工作台 switch is the app's root-level tab bar, not a navigation to a
// "different page" in the back-stack sense -- same bug/fix as content-library-view.jsx's
// navigateReplace (see its comment): navigate() here pushed a history entry on every tab click, so
// bouncing between 消息 and 工作台 a few times (a completely normal usage pattern for the app's only
// two bottom tabs) made the back button just cycle through past tab switches instead of leaving the
// app, exactly like a standard bottom-tab-bar app's back button should not have to unwind every past
// tab switch to back out.
const navigateReplace = (view) => HashNavigation.navigateReplace(HashNavigation.setUrlSidePanel(window.location.hash, view));
// Was a hardcoded FREQUENT_CONTACTS mock (5 fake names, always clicking through to the same // Was a hardcoded FREQUENT_CONTACTS mock (5 fake names, always clicking through to the same
// direct-message-view.jsx demo regardless of which avatar was tapped -- confirmed independently by // direct-message-view.jsx demo regardless of which avatar was tapped -- confirmed independently by
// a user screenshot showing every contact opening the identical "接语彤" script). Real "frequently // a user screenshot showing every contact opening the identical "接语彤" script). Real "frequently
@@ -183,7 +192,7 @@ function normalizeConversations(chatList, myUserId) {
function BottomNavigation() { function BottomNavigation() {
return <nav className="messages-bottom-nav" aria-label="主导航"> return <nav className="messages-bottom-nav" aria-label="主导航">
<button type="button" onClick={() => navigate('workbench')}><Icon>dashboard</Icon><span>工作台</span></button> <button type="button" onClick={() => navigateReplace('workbench')}><Icon>dashboard</Icon><span>工作台</span></button>
<button type="button" className="active"><Icon>person</Icon><span>消息</span></button> <button type="button" className="active"><Icon>person</Icon><span>消息</span></button>
</nav>; </nav>;
} }
+5 -1
View File
@@ -56,7 +56,11 @@ class NewTopicView extends React.Component {
handleTabClick(e) { handleTabClick(e) {
e.preventDefault(); e.preventDefault();
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id)); // Same-page tab switch (find user / new group / find by id), not a navigation to a different
// page -- navigateTo() pushed a history entry per click purely to keep the `tab` url param in
// sync, so repeatedly switching tabs made the back button cycle through past tabs instead of
// leaving this page. navigateReplace() keeps the param in sync without that side effect.
HashNavigation.navigateReplace(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({tabSelected: e.currentTarget.dataset.id}); this.setState({tabSelected: e.currentTarget.dataset.id});
} }
+3 -1
View File
@@ -29,7 +29,9 @@ export default class WallpapersView extends React.PureComponent {
handleTabClick(e) { handleTabClick(e) {
e.preventDefault(); e.preventDefault();
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id)); // Same-page tab switch (pattern / image), not a navigation to a different page -- see the same
// fix/comment in new-topic-view.jsx's handleTabClick.
HashNavigation.navigateReplace(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({tab: e.currentTarget.dataset.id}); this.setState({tab: e.currentTarget.dataset.id});
} }
+13 -5
View File
@@ -22,6 +22,14 @@ const navigate = (view, params) => {
HashNavigation.navigateTo(hash); HashNavigation.navigateTo(hash);
}; };
// Same bug/fix as content-library-view.jsx's navigateReplace: DailyReportList/MonthlyReportList's
// 我的日报|月报 <-> 团队统计 switch is a same-page tab toggle (same component, just a `team` boolean),
// not a navigation to a different page -- using navigate() here pushed a history entry per click, so
// switching back and forth a few times made the system back button (and this app's own Header back
// button, gated on HashNavigation.goBack()) cycle through past toggle states instead of leaving the
// report page. replaceState-based navigateReplace keeps the hash in sync without that side effect.
const navigateReplace = (view) => HashNavigation.navigateReplace(HashNavigation.setUrlSidePanel(window.location.hash, view));
export const WORKPLACE_COLLABORATION_VIEWS = [ export const WORKPLACE_COLLABORATION_VIEWS = [
'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail',
'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail',
@@ -166,7 +174,7 @@ function Workbench({onToast}) {
</button>)}</section> </button>)}</section>
</main><nav className="workplace-bottom-nav" aria-label="主导航"> </main><nav className="workplace-bottom-nav" aria-label="主导航">
<button type="button" className="active"><Icon>dashboard</Icon><span>工作台</span></button> <button type="button" className="active"><Icon>dashboard</Icon><span>工作台</span></button>
<button type="button" onClick={() => navigate('messages-home')}><Icon>chat_bubble_outline</Icon><span>消息</span></button> <button type="button" onClick={() => navigateReplace('messages-home')}><Icon>chat_bubble_outline</Icon><span>消息</span></button>
</nav></div>; </nav></div>;
} }
@@ -756,7 +764,7 @@ function DailyReportList({team, myUserId, onToast}) {
<main> <main>
{team && stats ? <section className="report-statistics"> {team && stats ? <section className="report-statistics">
<header><span><small>{formatMeetingDate(new Date())}</small><strong>全公司</strong></span> <header><span><small>{formatMeetingDate(new Date())}</small><strong>全公司</strong></span>
<button type="button" onClick={() => navigate('daily-report-mine')}>我的日报</button></header> <button type="button" onClick={() => navigateReplace('daily-report-mine')}>我的日报</button></header>
<div><span><b>{stats.expected_count || 0}</b><small>应提交</small></span> <div><span><b>{stats.expected_count || 0}</b><small>应提交</small></span>
<span><b>{stats.submitted_count || 0}</b><small>已提交</small></span> <span><b>{stats.submitted_count || 0}</b><small>已提交</small></span>
<span><b>{(stats.expected_count || 0) - (stats.submitted_count || 0)}</b><small>未提交</small></span></div> <span><b>{(stats.expected_count || 0) - (stats.submitted_count || 0)}</b><small>未提交</small></span></div>
@@ -766,7 +774,7 @@ function DailyReportList({team, myUserId, onToast}) {
<input value={query} onChange={e => setQuery(e.target.value)} placeholder={team ? '搜索姓名或内容' : '搜索日期'} /></label> <input value={query} onChange={e => setQuery(e.target.value)} placeholder={team ? '搜索姓名或内容' : '搜索日期'} /></label>
{!team ? <nav className="report-role-switch"> {!team ? <nav className="report-role-switch">
<button type="button" className="active">我的日报</button> <button type="button" className="active">我的日报</button>
<button type="button" onClick={() => navigate('daily-report-team')}>团队统计</button> <button type="button" onClick={() => navigateReplace('daily-report-team')}>团队统计</button>
</nav> : null} </nav> : null}
<section className="report-cards"> <section className="report-cards">
{reports == null ? <p className="organization-loading">加载中…</p> : {reports == null ? <p className="organization-loading">加载中…</p> :
@@ -954,7 +962,7 @@ function MonthlyReportList({team, myUserId, onToast}) {
<main> <main>
{team && stats ? <section className="report-statistics"> {team && stats ? <section className="report-statistics">
<header><span><small>{formatMonthLabel(toMonthInputValue(new Date()))}</small><strong>全公司</strong></span> <header><span><small>{formatMonthLabel(toMonthInputValue(new Date()))}</small><strong>全公司</strong></span>
<button type="button" onClick={() => navigate('monthly-report-mine')}>我的月报</button></header> <button type="button" onClick={() => navigateReplace('monthly-report-mine')}>我的月报</button></header>
<div><span><b>{stats.expected_count || 0}</b><small>应提交</small></span> <div><span><b>{stats.expected_count || 0}</b><small>应提交</small></span>
<span><b>{stats.submitted_count || 0}</b><small>已提交</small></span> <span><b>{stats.submitted_count || 0}</b><small>已提交</small></span>
<span><b>{(stats.expected_count || 0) - (stats.submitted_count || 0)}</b><small>未提交</small></span></div> <span><b>{(stats.expected_count || 0) - (stats.submitted_count || 0)}</b><small>未提交</small></span></div>
@@ -963,7 +971,7 @@ function MonthlyReportList({team, myUserId, onToast}) {
<input value={query} onChange={e => setQuery(e.target.value)} placeholder={team ? '搜索姓名或内容' : '搜索月份或内容'} /></label> <input value={query} onChange={e => setQuery(e.target.value)} placeholder={team ? '搜索姓名或内容' : '搜索月份或内容'} /></label>
{!team ? <nav className="report-role-switch"> {!team ? <nav className="report-role-switch">
<button type="button" className="active">我的月报</button> <button type="button" className="active">我的月报</button>
<button type="button" onClick={() => navigate('monthly-report-team')}>团队统计</button> <button type="button" onClick={() => navigateReplace('monthly-report-team')}>团队统计</button>
</nav> : null} </nav> : null}
<section className="report-cards"> <section className="report-cards">
{reports == null ? <p className="organization-loading">加载中…</p> : {reports == null ? <p className="organization-loading">加载中…</p> :
+9 -7
View File
@@ -13206,6 +13206,7 @@ const navigate = (view, topic) => {
hash = topic ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(hash, 'topic', topic) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].removeUrlParam(hash, 'topic'); hash = topic ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(hash, 'topic', topic) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].removeUrlParam(hash, 'topic');
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash); _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash);
}; };
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view));
function frequentContacts(chatList) { function frequentContacts(chatList) {
if (!chatList || chatList.length === 0) return []; if (!chatList || chatList.length === 0) return [];
return chatList.filter(item => item.topic && tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Tinode.isP2PTopicName(item.topic)).slice().sort((a, b) => new Date(b.touched || 0).getTime() - new Date(a.touched || 0).getTime()).slice(0, 8).map(item => ({ return chatList.filter(item => item.topic && tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Tinode.isP2PTopicName(item.topic)).slice().sort((a, b) => new Date(b.touched || 0).getTime() - new Date(a.touched || 0).getTime()).slice(0, 8).map(item => ({
@@ -13392,7 +13393,7 @@ function BottomNavigation() {
"aria-label": "主导航", "aria-label": "主导航",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("button", { children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("button", {
type: "button", type: "button",
onClick: () => navigate('workbench'), onClick: () => navigateReplace('workbench'),
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(Icon, { children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(Icon, {
children: "dashboard" children: "dashboard"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("span", { }, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("span", {
@@ -14897,7 +14898,7 @@ class NewTopicView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
} }
handleTabClick(e) { handleTabClick(e) {
e.preventDefault(); e.preventDefault();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id)); _lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_7__["default"].addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({ this.setState({
tabSelected: e.currentTarget.dataset.id tabSelected: e.currentTarget.dataset.id
}); });
@@ -20570,6 +20571,7 @@ const navigate = (view, params) => {
}); });
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash); _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(hash);
}; };
const navigateReplace = view => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].setUrlSidePanel(window.location.hash, view));
const WORKPLACE_COLLABORATION_VIEWS = ['workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'monthly-report-editor', 'monthly-report-mine', 'monthly-report-team', 'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice']; const WORKPLACE_COLLABORATION_VIEWS = ['workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'monthly-report-editor', 'monthly-report-mine', 'monthly-report-team', 'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice'];
function Icon({ function Icon({
children children
@@ -20805,7 +20807,7 @@ function Workbench({
}, void 0, false)] }, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", { }, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
type: "button", type: "button",
onClick: () => navigate('messages-home'), onClick: () => navigateReplace('messages-home'),
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, { children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
children: "chat_bubble_outline" children: "chat_bubble_outline"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("span", { }, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("span", {
@@ -21836,7 +21838,7 @@ function DailyReportList({
}, void 0, false)] }, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", { }, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
type: "button", type: "button",
onClick: () => navigate('daily-report-mine'), onClick: () => navigateReplace('daily-report-mine'),
children: "我的日报" children: "我的日报"
}, void 0, false)] }, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("div", { }, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("div", {
@@ -21883,7 +21885,7 @@ function DailyReportList({
children: "我的日报" children: "我的日报"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", { }, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
type: "button", type: "button",
onClick: () => navigate('daily-report-team'), onClick: () => navigateReplace('daily-report-team'),
children: "团队统计" children: "团队统计"
}, void 0, false)] }, void 0, false)]
}, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("section", { }, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("section", {
@@ -22178,7 +22180,7 @@ function MonthlyReportList({
}, void 0, false)] }, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", { }, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
type: "button", type: "button",
onClick: () => navigate('monthly-report-mine'), onClick: () => navigateReplace('monthly-report-mine'),
children: "我的月报" children: "我的月报"
}, void 0, false)] }, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("div", { }, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("div", {
@@ -22219,7 +22221,7 @@ function MonthlyReportList({
children: "我的月报" children: "我的月报"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", { }, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
type: "button", type: "button",
onClick: () => navigate('monthly-report-team'), onClick: () => navigateReplace('monthly-report-team'),
children: "团队统计" children: "团队统计"
}, void 0, false)] }, void 0, false)]
}, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("section", { }, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("section", {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -46,7 +46,7 @@ class WallpapersView extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureC
} }
handleTabClick(e) { handleTabClick(e) {
e.preventDefault(); e.preventDefault();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id)); _lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].navigateReplace(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_2__["default"].addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({ this.setState({
tab: e.currentTarget.dataset.id tab: e.currentTarget.dataset.id
}); });
File diff suppressed because one or more lines are too long