路由从 hash 换成 History API(pushState/replaceState/popstate),不再依赖 location.hash
HashNavigation 改名 Navigation(src/lib/navigation.js),接口不变,内部改用真实路径: parseUrlHash/composeUrlHash 改名 parseUrl/composeUrl;navigateTo/navigateReplace 改调 pushState/replaceState 后手动派发 popstate(pushState/replaceState 不会像赋值 location.hash 那样自动触发事件,这里复用 navigateReplace 早就在用的同一个手动 dispatch 手法);goBack() 不用改,history.back() 本来就是真实浏览器历史机制。新增 Navigation.currentUrl() 替代调用方 原本直接读的 window.location.hash(这个类从来没包装过这个读取)。 一处需要专门处理的边界情况:当 path[0] 是空字符串("不选中侧栏视图、只开一个会话"这个仓库里 的合法状态)时,拼出的真实路径开头会变成双斜杠,被浏览器当协议相对地址解析、试图跳到一个叫 会话id的host,pushState 直接抛 SecurityError。composeUrl/parseUrl 内部用占位 token "_" 处理 这种情况,对外的调用方(19个文件)完全无感。 tinode-web.jsx 的 handleHashRoute 改名 handleRouteChange,监听从 hashchange 换成 popstate; service-worker.js 的推送通知点击跳转手动改写成同样的路径格式(这个文件在打包之外单独执行, 没法 import Navigation)。index.html/index-dev.html 加了 <base href="/">——所有静态资源引用 都是相对路径,hash 路由下页面路径永远是 /,换真实路径后深层地址刷新会按当前目录解析出错误 资源地址导致白屏,这一行是标准修法。nginx.conf 的 try_files 兜底规则本来就适用两种路由,只 更新了注释。 实机验证:地址栏不再有 #;点击会话/设置等各类视图正常跳转;浏览器前进后退和应用内"返回" 按钮都正常;收藏页内部的类型 tab 切换(navigateReplace)确认不产生新的历史记录;在会话页/ 个人中心页刷新、直接用完整深层路径打开新标签页都能正确恢复到位,资源正常加载不白屏。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
97a6b88d59
commit
4e8f45d7d8
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import QRCode from 'qrcodejs';
|
||||
import { pinyin } from 'pinyin-pro';
|
||||
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
import Navigation from '../lib/navigation.js';
|
||||
import {
|
||||
getGroups, createGroup, getGroup, updateGroup, getGroupSettings, updateGroupMemberSettings,
|
||||
updateMyGroupProfile, getMyGroupProfile, leaveGroup, getGroupMembers, createGroupQRCode, getGroupJoinRequests,
|
||||
@@ -17,9 +17,9 @@ import { readCachedData, writeCachedData, useDebouncedValue } from '../lib/use-c
|
||||
// Carries the group `topic` (and anything else already in the hash) across navigation, the same way
|
||||
// contact-management-view.jsx threads `uid` -- these pages all need to know *which* group.
|
||||
const navigate = (view, topic) => {
|
||||
let hash = HashNavigation.setUrlSidePanel(window.location.hash, view);
|
||||
hash = topic ? HashNavigation.addUrlParam(hash, 'topic', topic) : HashNavigation.removeUrlParam(hash, 'topic');
|
||||
HashNavigation.navigateTo(hash);
|
||||
let hash = Navigation.setUrlSidePanel(Navigation.currentUrl(), view);
|
||||
hash = topic ? Navigation.addUrlParam(hash, 'topic', topic) : Navigation.removeUrlParam(hash, 'topic');
|
||||
Navigation.navigateTo(hash);
|
||||
};
|
||||
|
||||
// 'group-member-profile' is contact-management-view.jsx's Profile component (Ch.5) reused as-is for
|
||||
@@ -45,8 +45,8 @@ const navigateToProfile = (topic, userId, member) => {
|
||||
...(member.gender ? {gender: member.gender} : {})
|
||||
});
|
||||
}
|
||||
HashNavigation.navigateTo(
|
||||
HashNavigation.addUrlParam(HashNavigation.setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
|
||||
Navigation.navigateTo(
|
||||
Navigation.addUrlParam(Navigation.setUrlSidePanel(Navigation.currentUrl(), 'group-member-profile'), 'uid', userId));
|
||||
// 群场景下真正该用的是 GET /groups/{topic}/members/{user_id}/profile——群上下文里权限判断更
|
||||
// 宽松,实测对方哪怕不是好友也能拿到 {capabilities, relationship}(Profile 组件目标页读的
|
||||
// GET /users/{id}/profile 对非好友是 403,只能回退用种子)。异步补种:请求回来后如果用户还
|
||||
@@ -140,7 +140,7 @@ function Icon({children}) { return <i className="material-icons" aria-hidden="tr
|
||||
// navigate 辅助函数只会写 query 参数),这类情况传一个自己的回调进来。
|
||||
function Header({title, back = 'group-list', backTopic, onBack, right, onRight}) {
|
||||
return <header className="group-header">
|
||||
<button type="button" onClick={() => { if (!HashNavigation.goBack()) { if (onBack) onBack(); else navigate(back, backTopic); } }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
|
||||
<button type="button" onClick={() => { if (!Navigation.goBack()) { if (onBack) onBack(); else navigate(back, backTopic); } }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
|
||||
<h1>{title}</h1>
|
||||
{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}
|
||||
</header>;
|
||||
@@ -266,7 +266,7 @@ function JoinRequests({topic, fromChat, onToast}) {
|
||||
// ambiguity above -- without it, returning from a click on the in-chat banner always landed on
|
||||
// "group-settings" (closing the open conversation) instead of back to the chat itself.
|
||||
const backToChat = React.useCallback(() => {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(HashNavigation.setUrlSidePanel('', ''), topic));
|
||||
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.setUrlSidePanel('', ''), topic));
|
||||
}, [topic]);
|
||||
const [items, setItems] = React.useState(() => (topic && readCachedData('group-join-requests:' + topic)) || null);
|
||||
const [profiles, setProfiles] = React.useState({});
|
||||
@@ -337,14 +337,14 @@ function Announcement({topic, myRole, fromChat, onToast}) {
|
||||
const canEdit = myRole === 'owner' || myRole === 'admin';
|
||||
// Coming from the announcement banner atop the chat itself (messages-view.jsx's
|
||||
// handleShowAnnouncement) vs. from group-settings' "群公告" row land on the identical
|
||||
// path[0]=group-announcement-detail hash -- `fromChat` is the only thing telling them apart, so
|
||||
// path[0]=group-announcement-detail URL -- `fromChat` is the only thing telling them apart, so
|
||||
// "back" can return to the chat (close the sidepanel) instead of always forcing group-settings.
|
||||
// 从聊天页横幅进来时(messages-view.jsx:1152 用空基准拼 hash,所以 path[1] 是空的),返回应该
|
||||
// 从聊天页横幅进来时(messages-view.jsx:1152 用空基准拼路径,所以 path[1] 是空的),返回应该
|
||||
// 关掉侧栏回到那个会话。原来写的是 navigate('', topic)——navigate 只把 topic 写成 query 参数,
|
||||
// path[1] 依旧为空,tinode-web 的 handleHashRoute 就把 topicSelected 清成 null,会话跟着一起关了。
|
||||
// path[1] 依旧为空,tinode-web 的 handleRouteChange 就把 topicSelected 清成 null,会话跟着一起关了。
|
||||
// 这里显式把 topic 写回 path[1]。
|
||||
const backToChat = React.useCallback(() => {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(HashNavigation.setUrlSidePanel('', ''), topic));
|
||||
Navigation.navigateTo(Navigation.setUrlTopic(Navigation.setUrlSidePanel('', ''), topic));
|
||||
}, [topic]);
|
||||
React.useEffect(() => {
|
||||
if (!topic) return;
|
||||
@@ -491,13 +491,13 @@ function GroupSettings({topic, myUserId, tinode, onToast}) {
|
||||
{/* target_type='group' 这条链路早就是通的(tinode-web.jsx 的 handleReportTopic 就在用),
|
||||
这里原来一直是个"暂未开放"的死桩。跟 contact-management-view.jsx 的好友举报入口一样,
|
||||
举报页要的是 target_type/target_id/target_name 三个参数,本文件的 navigate 辅助函数
|
||||
只会写 topic,所以直接拼 hash。 */}
|
||||
只会写 topic,所以直接拼路径。 */}
|
||||
<button type="button" onClick={() => {
|
||||
let hash = HashNavigation.setUrlSidePanel('', 'abuse-report-reasons');
|
||||
hash = HashNavigation.addUrlParam(hash, 'target_type', 'group');
|
||||
hash = HashNavigation.addUrlParam(hash, 'target_id', topic);
|
||||
hash = HashNavigation.addUrlParam(hash, 'target_name', (group && group.name) || topic);
|
||||
HashNavigation.navigateTo(hash);
|
||||
let hash = Navigation.setUrlSidePanel('', 'abuse-report-reasons');
|
||||
hash = Navigation.addUrlParam(hash, 'target_type', 'group');
|
||||
hash = Navigation.addUrlParam(hash, 'target_id', topic);
|
||||
hash = Navigation.addUrlParam(hash, 'target_name', (group && group.name) || topic);
|
||||
Navigation.navigateTo(hash);
|
||||
}}><span>投诉/举报</span><Icon>chevron_right</Icon></button>
|
||||
</section>
|
||||
<button type="button" className="group-danger" onClick={leave}>退出群组</button>
|
||||
|
||||
Reference in New Issue
Block a user