修复全页面走查发现的问题(第四批:精准查找、隐私锁定、搜索防抖)

精准查找搜不到手机号(第5章 5.5)
- 一直写死 type=exact_account,而该分支只比对 profile.public_account / u.username / u.id
  (tenant_adapter.go:9170-9172),**不匹配 u.phone**。参考图 5.5 里输的恰恰是手机号,
  这个场景必然搜不到。改为按输入形态分流:11 位 1 开头走 type=phone。
- phone 分支服务端不做 E.164 归一化(只有模糊的 account 分支才调 normalizeUserPhone),
  库里存的是 +86 开头,所以裸号码由前端补区号——注册流程本来也是这么拼的。
- 已用真实后端验证请求正确路由为 q=%2B8613800001188&type=phone。

隐私设置忽略 editable(第10章 10.12/10.13)
- 后端 userPrivacyPayload 返回 editable{add_me_methods, profile_visibility,
  last_seen_visibility, read_receipts} 表达"企业强制策略锁死了这一项",前端完全没读。
  被锁的开关照样能点,点了 400 再回滚,用户只看到开关自己弹回去。现在禁用并给出说明文案,
  同时补 .personal-switch:disabled 的视觉反馈(否则看不出为什么点不动)。

搜索防抖
- 新增共享的 useDebouncedValue,用在三处真正发服务端请求的搜索:收藏、会话内检索、
  群列表、群文件。原来 query 直接进 effect 依赖,每敲一个字符发一次请求;群列表更糟,
  reload 对三个分类各发一次,一个字符就是 3 个请求。而且返回顺序没有保证,后发的短前缀
  请求可能后到、把正确结果覆盖掉。
- 实测:群列表输入 4 个字,请求数从 12 降到 3。
- FavoriteShare 那处搜索是本地过滤 chatList、不发请求,没有动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-03 18:56:41 +08:00
co-authored by Claude Opus 5
parent c9b8125013
commit 35d3e1cf6b
8 changed files with 144 additions and 39 deletions
+63 -18
View File
@@ -4692,6 +4692,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ readCachedData: function() { return /* binding */ readCachedData; },
/* harmony export */ useCachedData: function() { return /* binding */ useCachedData; },
/* harmony export */ useDebouncedValue: function() { return /* binding */ useDebouncedValue; },
/* harmony export */ writeCachedData: function() { return /* binding */ writeCachedData; }
/* harmony export */ });
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
@@ -4725,6 +4726,15 @@ function readCachedData(key) {
function writeCachedData(key, value) {
if (key) cache.set(key, value);
}
function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(value);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (value === debounced) return undefined;
const timer = window.setTimeout(() => setDebounced(value), delay);
return () => window.clearTimeout(timer);
}, [value, delay, debounced]);
return debounced;
}
/***/ }),
@@ -7715,6 +7725,26 @@ function FriendRequests({
}, void 0, true)]
}, void 0, true);
}
const CN_MOBILE = /^1[3-9]\d{9}$/;
function classifySearchInput(text) {
if (text.startsWith('+')) {
return {
query: text.replace(/[\s-]/g, ''),
type: 'phone'
};
}
const digits = text.replace(/[\s-]/g, '');
if (CN_MOBILE.test(digits)) {
return {
query: '+86' + digits,
type: 'phone'
};
}
return {
query: text,
type: 'exact_account'
};
}
const GENDER_LABELS = {
male: '男',
female: '女',
@@ -7739,14 +7769,19 @@ function AddFriend({
const [searched, setSearched] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(result);
const [results, setResults] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
const runSearch = q => {
if (!q.trim()) {
const text = q.trim();
if (!text) {
setSearched(false);
setResults([]);
return;
}
setSearching(true);
setSearched(true);
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.searchUsers)(q.trim(), 'exact_account').then(data => setResults(data.items || [])).catch(() => setResults([])).finally(() => setSearching(false));
const {
query,
type
} = classifySearchInput(text);
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.searchUsers)(query, type).then(data => setResults(data.items || [])).catch(() => setResults([])).finally(() => setSearching(false));
};
const shareCard = () => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.createMyQRCode)().then(data => {
@@ -8917,7 +8952,8 @@ function Favorites({
const initialType = view === 'favorites-files' ? 'file' : view === 'favorites-text' ? 'text' : view === 'favorites-voice' ? 'voice' : view === 'favorites-links' ? 'link' : 'all';
const selecting = view === 'favorites-select';
const [type, setType] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(selecting ? 'file' : initialType);
const [query, setQuery] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [queryInput, setQueryInput] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const query = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_6__.useDebouncedValue)(queryInput, 300);
const [selected, setSelected] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_6__.readCachedData)('favorites:' + type) || null);
const [cursor, setCursor] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
@@ -9004,8 +9040,8 @@ function Favorites({
right: selecting ? '取消' : '多选',
onRight: () => navigate(selecting ? 'favorites-files' : 'favorites-select')
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(Search, {
value: query,
onChange: setQuery,
value: queryInput,
onChange: setQueryInput,
placeholder: "搜索收藏内容"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("nav", {
className: "library-type-tabs",
@@ -9405,7 +9441,8 @@ function History({
}) {
const topic = hashParams && hashParams.topic;
const cacheKey = topic ? `history:${topic}:${view}` : null;
const [query, setQuery] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [queryInput, setQueryInput] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const query = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_6__.useDebouncedValue)(queryInput, 300);
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => cacheKey && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_6__.readCachedData)(cacheKey) || null);
const [cursor, setCursor] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [hasMore, setHasMore] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
@@ -9480,8 +9517,8 @@ function History({
uid: topic
}
}, void 0, false), view === 'history-search' ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(Search, {
value: query,
onChange: setQuery,
value: queryInput,
onChange: setQueryInput,
placeholder: "搜索当前会话"
}, void 0, false) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(HistoryNav, {
active: view
@@ -10573,7 +10610,8 @@ function GroupList({
joined: false
});
const [category, setCategory] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('owned');
const [query, setQuery] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [queryInput, setQueryInput] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const query = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useDebouncedValue)(queryInput, 300);
const [creating, setCreating] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [name, setName] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [submitting, setSubmitting] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
@@ -10625,8 +10663,8 @@ function GroupList({
right: "创建",
onRight: () => setCreating(true)
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Search, {
value: query,
onChange: setQuery,
value: queryInput,
onChange: setQueryInput,
placeholder: "搜索群聊"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("nav", {
className: "group-tabs",
@@ -12020,7 +12058,8 @@ function GroupFiles({
tinode,
onToast
}) {
const [query, setQuery] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [queryInput, setQueryInput] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const query = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useDebouncedValue)(queryInput, 300);
const [files, setFiles] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => topic && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('group-files:' + topic) || null);
const [menuOpen, setMenuOpen] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [uploading, setUploading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
@@ -12147,8 +12186,8 @@ function GroupFiles({
}, void 0, false), "上传视频"]
}, void 0, true)]
}, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Search, {
value: query,
onChange: setQuery,
value: queryInput,
onChange: setQueryInput,
placeholder: "搜索文件名或者上传者"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
className: "group-inline-hint",
@@ -15312,6 +15351,7 @@ function PrivacySettings({
if (on.length === 4) return '全部允许';
return on.length ? on.join('、') : '全部关闭';
};
const locked = key => !!(privacy && privacy.editable && privacy.editable[key] === false);
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "personal-page",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Header, {
@@ -15331,10 +15371,11 @@ function PrivacySettings({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("strong", {
children: "显示消息已读状态"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("small", {
children: "关闭后你也无法查看对方的已读状态"
children: locked('read_receipts') ? '该项由企业统一设置,不可修改' : '关闭后你也无法查看对方的已读状态'
}, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Toggle, {
checked: !!privacy.read_receipts,
disabled: locked('read_receipts'),
onChange: v => update('read_receipts', v)
}, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
@@ -15342,10 +15383,11 @@ function PrivacySettings({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("strong", {
children: "展示最后在线时间"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("small", {
children: "仅向联系人显示"
children: locked('last_seen_visibility') ? '该项由企业统一设置,不可修改' : '仅向联系人显示'
}, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Toggle, {
checked: privacy.last_seen_visibility !== 'nobody',
disabled: locked('last_seen_visibility'),
onChange: v => update('last_seen_visibility', v ? 'friends' : 'nobody')
}, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
@@ -15353,10 +15395,11 @@ function PrivacySettings({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("strong", {
children: "允许好友查看我的资料"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("small", {
children: "关闭后仅你自己可见"
children: locked('profile_visibility') ? '该项由企业统一设置,不可修改' : '关闭后仅你自己可见'
}, void 0, false)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Toggle, {
checked: privacy.profile_visibility !== 'nobody',
disabled: locked('profile_visibility'),
onChange: v => update('profile_visibility', v ? 'friends' : 'nobody')
}, void 0, false)]
}, void 0, true)]
@@ -15409,6 +15452,7 @@ function FriendDiscoverySettings({
key: 'group',
title: '群聊'
}];
const methodsLocked = !!(privacy && privacy.editable && privacy.editable.add_me_methods === false);
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "personal-page",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Header, {
@@ -15418,7 +15462,7 @@ function FriendDiscoverySettings({
className: "privacy-list",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("p", {
className: "privacy-note",
children: "关闭某种方式后,其他人将无法通过该渠道搜索或添加你。"
children: methodsLocked ? '添加方式由企业统一设置,不可修改。' : '关闭某种方式后,其他人将无法通过该渠道搜索或添加你。'
}, void 0, false), privacy ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("section", {
children: rows.map(row => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("span", {
@@ -15427,6 +15471,7 @@ function FriendDiscoverySettings({
}, void 0, true)
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(Toggle, {
checked: !!(privacy.add_me_methods || {})[row.key],
disabled: methodsLocked,
onChange: v => update(row.key, v)
}, void 0, false)]
}, row.key, true))