优化个人中心信息架构与手机编辑体验,置顶改为文字标识。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
import { Pin } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/** 置顶图钉标识 */
|
||||
export default function PinnedIcon({ className, size = 16 }: Props) {
|
||||
return (
|
||||
<Pin
|
||||
className={cn('post-pinned-icon', className)}
|
||||
size={size}
|
||||
fill="currentColor"
|
||||
aria-label="置顶"
|
||||
role="img"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { memo } from 'react';
|
||||
import { Eye, Image as ImageIcon, MessageCircle, ThumbsUp } from 'lucide-react';
|
||||
import BoardBadge from '@/components/BoardBadge';
|
||||
import FeaturedIcon from '@/components/FeaturedIcon';
|
||||
import PinnedIcon from '@/components/PinnedIcon';
|
||||
import UserLink from '@/components/UserLink';
|
||||
import type { PostItem } from '../api/types';
|
||||
import type { FeedSort } from './FeedSortBar';
|
||||
@@ -79,9 +78,7 @@ function PostListItem({ post, sort = 'latest', onSelect }: Props) {
|
||||
|
||||
<div className="post-title-row">
|
||||
{post.pinned && (
|
||||
<span className="post-pin-badge post-pin-badge--icon" title="置顶">
|
||||
<PinnedIcon size={13} />
|
||||
</span>
|
||||
<span className="post-pin-badge" title="置顶">置顶</span>
|
||||
)}
|
||||
{post.featured && (
|
||||
<span className="post-feature-badge" title="精华">
|
||||
|
||||
@@ -7,12 +7,19 @@ const REFRESH_THRESHOLD = 68;
|
||||
const PULL_MAX = 108;
|
||||
/** 判定为「下拉」意图的最小位移,避免误触滚动 */
|
||||
const ARM_DELTA = 10;
|
||||
/** 指示器休息位在视口上方的隐藏量,下拉时再滑入 */
|
||||
const INDICATOR_HIDE = 40;
|
||||
|
||||
/**
|
||||
* 定位当前真正滚动的容器。
|
||||
* SPA 使用内部滚动(body overflow:hidden),原生下拉刷新不可用,需挂到此容器。
|
||||
* 发帖/编辑页不参与:主栏 overflow:hidden,滚动在编辑器内层,绑 PTR 会误触并打断编辑。
|
||||
*/
|
||||
function pickScrollEl(): HTMLElement | null {
|
||||
if (document.querySelector('.main-content--compose, .compose-page')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 手机 Feed 整栏滚动(板块 / 排序栏可滚走)
|
||||
const mobileFeed = document.querySelector<HTMLElement>('.main-content--feed-mobile-scroll');
|
||||
if (mobileFeed) return mobileFeed;
|
||||
@@ -23,9 +30,6 @@ function pickScrollEl(): HTMLElement | null {
|
||||
const page = document.querySelector<HTMLElement>('.page-wrap:not(.page-wrap--feed)');
|
||||
if (page) return page;
|
||||
|
||||
const compose = document.querySelector<HTMLElement>('.main-content--compose');
|
||||
if (compose) return compose;
|
||||
|
||||
const admin = document.querySelector<HTMLElement>('.admin-main');
|
||||
if (admin) return admin;
|
||||
|
||||
@@ -40,6 +44,37 @@ function isTouchDevice(): boolean {
|
||||
|| navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
/** 输入框 / 富文本编辑中不触发下拉刷新 */
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return Boolean(
|
||||
target.closest(
|
||||
'textarea, input, select, [contenteditable="true"], .ProseMirror, .article-editor, .compose-page',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸落在内层可滚动区域且该区域未到顶时,交给内层滚动,不武装 PTR。
|
||||
*/
|
||||
function isNestedScrollBlocking(target: EventTarget | null, bound: HTMLElement): boolean {
|
||||
let node: Element | null = target instanceof Element ? target : null;
|
||||
while (node && node !== bound) {
|
||||
if (node instanceof HTMLElement) {
|
||||
const { overflowY } = getComputedStyle(node);
|
||||
if (
|
||||
(overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay')
|
||||
&& node.scrollHeight > node.clientHeight + 1
|
||||
&& node.scrollTop > 1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机端下拉刷新:在内部滚动容器顶部下拉后整页重载。
|
||||
* (浏览器原生 PTR 依赖 document 滚动,与本站 app-shell 布局不兼容。)
|
||||
@@ -49,6 +84,7 @@ function isTouchDevice(): boolean {
|
||||
export default function PullToRefresh() {
|
||||
const [pull, setPull] = useState(0);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [settling, setSettling] = useState(false);
|
||||
const pullRef = useRef(0);
|
||||
const refreshingRef = useRef(false);
|
||||
const startYRef = useRef(0);
|
||||
@@ -75,7 +111,10 @@ export default function PullToRefresh() {
|
||||
trackingRef.current = false;
|
||||
pullingRef.current = false;
|
||||
startYRef.current = 0;
|
||||
if (!refreshingRef.current) setPull(0);
|
||||
if (!refreshingRef.current) {
|
||||
setSettling(false);
|
||||
setPull(0);
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
@@ -87,8 +126,11 @@ export default function PullToRefresh() {
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (isEditableTarget(e.target)) return;
|
||||
if (isNestedScrollBlocking(e.target, el)) return;
|
||||
trackingRef.current = true;
|
||||
pullingRef.current = false;
|
||||
setSettling(false);
|
||||
startYRef.current = e.touches[0].clientY;
|
||||
};
|
||||
|
||||
@@ -109,6 +151,7 @@ export default function PullToRefresh() {
|
||||
}
|
||||
|
||||
pullingRef.current = true;
|
||||
setSettling(false);
|
||||
setPull(Math.min(PULL_MAX, dy * 0.55));
|
||||
if (e.cancelable) e.preventDefault();
|
||||
};
|
||||
@@ -121,12 +164,14 @@ export default function PullToRefresh() {
|
||||
|
||||
if (shouldRefresh) {
|
||||
setRefreshing(true);
|
||||
setSettling(true);
|
||||
setPull(REFRESH_THRESHOLD * 0.7);
|
||||
window.setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 180);
|
||||
return;
|
||||
}
|
||||
setSettling(true);
|
||||
setPull(0);
|
||||
};
|
||||
|
||||
@@ -153,7 +198,17 @@ export default function PullToRefresh() {
|
||||
const tryBind = () => {
|
||||
if (cancelled) return;
|
||||
const next = pickScrollEl();
|
||||
if (next) bind(next);
|
||||
if (next) {
|
||||
bind(next);
|
||||
return;
|
||||
}
|
||||
// 发帖页等:卸掉旧绑定并收起指示器,避免残留气泡 / 误触
|
||||
if (!bound && !scrollElRef.current && pullRef.current <= 0 && !trackingRef.current) {
|
||||
return;
|
||||
}
|
||||
unbind();
|
||||
scrollElRef.current = null;
|
||||
resetGesture();
|
||||
};
|
||||
|
||||
const scheduleBind = () => {
|
||||
@@ -187,8 +242,13 @@ export default function PullToRefresh() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ptr-indicator${ready ? ' ptr-indicator--ready' : ''}${refreshing ? ' ptr-indicator--refreshing' : ''}`}
|
||||
style={{ transform: `translateY(${offset}px)` }}
|
||||
className={[
|
||||
'ptr-indicator',
|
||||
ready ? 'ptr-indicator--ready' : '',
|
||||
refreshing ? 'ptr-indicator--refreshing' : '',
|
||||
settling ? 'ptr-indicator--settling' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
style={{ transform: `translateY(${offset - INDICATOR_HIDE}px)` }}
|
||||
aria-hidden
|
||||
>
|
||||
<div className="ptr-indicator__chip">
|
||||
|
||||
Reference in New Issue
Block a user