优化个人中心信息架构与手机编辑体验,置顶改为文字标识。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 16:41:34 +08:00
parent 6b0a1d4281
commit e6e7ed73e3
6 changed files with 394 additions and 148 deletions

View File

@@ -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"
/>
);
}

View File

@@ -2,7 +2,6 @@ import { memo } from 'react';
import { Eye, Image as ImageIcon, MessageCircle, ThumbsUp } from 'lucide-react'; import { Eye, Image as ImageIcon, MessageCircle, ThumbsUp } from 'lucide-react';
import BoardBadge from '@/components/BoardBadge'; import BoardBadge from '@/components/BoardBadge';
import FeaturedIcon from '@/components/FeaturedIcon'; import FeaturedIcon from '@/components/FeaturedIcon';
import PinnedIcon from '@/components/PinnedIcon';
import UserLink from '@/components/UserLink'; import UserLink from '@/components/UserLink';
import type { PostItem } from '../api/types'; import type { PostItem } from '../api/types';
import type { FeedSort } from './FeedSortBar'; import type { FeedSort } from './FeedSortBar';
@@ -79,9 +78,7 @@ function PostListItem({ post, sort = 'latest', onSelect }: Props) {
<div className="post-title-row"> <div className="post-title-row">
{post.pinned && ( {post.pinned && (
<span className="post-pin-badge post-pin-badge--icon" title="置顶"> <span className="post-pin-badge" title="置顶"></span>
<PinnedIcon size={13} />
</span>
)} )}
{post.featured && ( {post.featured && (
<span className="post-feature-badge" title="精华"> <span className="post-feature-badge" title="精华">

View File

@@ -7,12 +7,19 @@ const REFRESH_THRESHOLD = 68;
const PULL_MAX = 108; const PULL_MAX = 108;
/** 判定为「下拉」意图的最小位移,避免误触滚动 */ /** 判定为「下拉」意图的最小位移,避免误触滚动 */
const ARM_DELTA = 10; const ARM_DELTA = 10;
/** 指示器休息位在视口上方的隐藏量,下拉时再滑入 */
const INDICATOR_HIDE = 40;
/** /**
* 定位当前真正滚动的容器。 * 定位当前真正滚动的容器。
* SPA 使用内部滚动body overflow:hidden原生下拉刷新不可用需挂到此容器。 * SPA 使用内部滚动body overflow:hidden原生下拉刷新不可用需挂到此容器。
* 发帖/编辑页不参与:主栏 overflow:hidden滚动在编辑器内层绑 PTR 会误触并打断编辑。
*/ */
function pickScrollEl(): HTMLElement | null { function pickScrollEl(): HTMLElement | null {
if (document.querySelector('.main-content--compose, .compose-page')) {
return null;
}
// 手机 Feed 整栏滚动(板块 / 排序栏可滚走) // 手机 Feed 整栏滚动(板块 / 排序栏可滚走)
const mobileFeed = document.querySelector<HTMLElement>('.main-content--feed-mobile-scroll'); const mobileFeed = document.querySelector<HTMLElement>('.main-content--feed-mobile-scroll');
if (mobileFeed) return mobileFeed; if (mobileFeed) return mobileFeed;
@@ -23,9 +30,6 @@ function pickScrollEl(): HTMLElement | null {
const page = document.querySelector<HTMLElement>('.page-wrap:not(.page-wrap--feed)'); const page = document.querySelector<HTMLElement>('.page-wrap:not(.page-wrap--feed)');
if (page) return page; if (page) return page;
const compose = document.querySelector<HTMLElement>('.main-content--compose');
if (compose) return compose;
const admin = document.querySelector<HTMLElement>('.admin-main'); const admin = document.querySelector<HTMLElement>('.admin-main');
if (admin) return admin; if (admin) return admin;
@@ -40,6 +44,37 @@ function isTouchDevice(): boolean {
|| navigator.maxTouchPoints > 0; || 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 布局不兼容。) * (浏览器原生 PTR 依赖 document 滚动,与本站 app-shell 布局不兼容。)
@@ -49,6 +84,7 @@ function isTouchDevice(): boolean {
export default function PullToRefresh() { export default function PullToRefresh() {
const [pull, setPull] = useState(0); const [pull, setPull] = useState(0);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [settling, setSettling] = useState(false);
const pullRef = useRef(0); const pullRef = useRef(0);
const refreshingRef = useRef(false); const refreshingRef = useRef(false);
const startYRef = useRef(0); const startYRef = useRef(0);
@@ -75,7 +111,10 @@ export default function PullToRefresh() {
trackingRef.current = false; trackingRef.current = false;
pullingRef.current = false; pullingRef.current = false;
startYRef.current = 0; startYRef.current = 0;
if (!refreshingRef.current) setPull(0); if (!refreshingRef.current) {
setSettling(false);
setPull(0);
}
}; };
const onTouchStart = (e: TouchEvent) => { const onTouchStart = (e: TouchEvent) => {
@@ -87,8 +126,11 @@ export default function PullToRefresh() {
)) { )) {
return; return;
} }
if (isEditableTarget(e.target)) return;
if (isNestedScrollBlocking(e.target, el)) return;
trackingRef.current = true; trackingRef.current = true;
pullingRef.current = false; pullingRef.current = false;
setSettling(false);
startYRef.current = e.touches[0].clientY; startYRef.current = e.touches[0].clientY;
}; };
@@ -109,6 +151,7 @@ export default function PullToRefresh() {
} }
pullingRef.current = true; pullingRef.current = true;
setSettling(false);
setPull(Math.min(PULL_MAX, dy * 0.55)); setPull(Math.min(PULL_MAX, dy * 0.55));
if (e.cancelable) e.preventDefault(); if (e.cancelable) e.preventDefault();
}; };
@@ -121,12 +164,14 @@ export default function PullToRefresh() {
if (shouldRefresh) { if (shouldRefresh) {
setRefreshing(true); setRefreshing(true);
setSettling(true);
setPull(REFRESH_THRESHOLD * 0.7); setPull(REFRESH_THRESHOLD * 0.7);
window.setTimeout(() => { window.setTimeout(() => {
window.location.reload(); window.location.reload();
}, 180); }, 180);
return; return;
} }
setSettling(true);
setPull(0); setPull(0);
}; };
@@ -153,7 +198,17 @@ export default function PullToRefresh() {
const tryBind = () => { const tryBind = () => {
if (cancelled) return; if (cancelled) return;
const next = pickScrollEl(); 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 = () => { const scheduleBind = () => {
@@ -187,8 +242,13 @@ export default function PullToRefresh() {
return ( return (
<div <div
className={`ptr-indicator${ready ? ' ptr-indicator--ready' : ''}${refreshing ? ' ptr-indicator--refreshing' : ''}`} className={[
style={{ transform: `translateY(${offset}px)` }} '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 aria-hidden
> >
<div className="ptr-indicator__chip"> <div className="ptr-indicator__chip">

View File

@@ -2,7 +2,6 @@ import { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react
import { useParams, useNavigate, useOutletContext, useLocation } from 'react-router-dom'; import { useParams, useNavigate, useOutletContext, useLocation } from 'react-router-dom';
import { ArrowLeft, ThumbsUp, Star, Pencil, Pin, History, Lock, MessageSquare, Trash2, Sparkles, Flag, Ban, CircleCheck, CircleHelp, MoreHorizontal } from 'lucide-react'; import { ArrowLeft, ThumbsUp, Star, Pencil, Pin, History, Lock, MessageSquare, Trash2, Sparkles, Flag, Ban, CircleCheck, CircleHelp, MoreHorizontal } from 'lucide-react';
import FeaturedIcon from '@/components/FeaturedIcon'; import FeaturedIcon from '@/components/FeaturedIcon';
import PinnedIcon from '@/components/PinnedIcon';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import BoardBadge from '@/components/BoardBadge'; import BoardBadge from '@/components/BoardBadge';
@@ -600,9 +599,7 @@ export default function PostDetailPage() {
<div className="post-detail-head"> <div className="post-detail-head">
<h1 className="post-detail-title"> <h1 className="post-detail-title">
{post.pinned && ( {post.pinned && (
<span className="post-pin-badge post-pin-badge--icon post-pin-badge--detail" title="置顶"> <span className="post-pin-badge post-pin-badge--detail" title="置顶"></span>
<PinnedIcon size={16} />
</span>
)} )}
{post.featured && <FeaturedIcon className="mr-2" size={18} />} {post.featured && <FeaturedIcon className="mr-2" size={18} />}
{post.status === 'pending' && <Badge variant="orange" className="mr-2 align-middle"></Badge>} {post.status === 'pending' && <Badge variant="orange" className="mr-2 align-middle"></Badge>}

View File

@@ -7,6 +7,7 @@ import {
ArrowLeft, ArrowLeft,
Camera, Camera,
Check, Check,
Coins,
Copy, Copy,
FileText, FileText,
Hash, Hash,
@@ -22,7 +23,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label';
import UserBadges from '../components/UserBadges'; import UserBadges from '../components/UserBadges';
import PointsWalletPanel from '../components/PointsWalletPanel'; import PointsWalletPanel from '../components/PointsWalletPanel';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
@@ -63,10 +64,10 @@ const pwdSchema = (minLen: number) => z.object({
type NickValues = z.infer<typeof nickSchema>; type NickValues = z.infer<typeof nickSchema>;
type SigValues = z.infer<ReturnType<typeof sigSchema>>; type SigValues = z.infer<ReturnType<typeof sigSchema>>;
type PwdValues = z.infer<ReturnType<typeof pwdSchema>>; type PwdValues = z.infer<ReturnType<typeof pwdSchema>>;
type ProfileTab = 'posts' | 'settings' | 'security'; type ProfileTab = 'posts' | 'points' | 'settings' | 'security';
function parseTab(raw: string | null): ProfileTab { function parseTab(raw: string | null): ProfileTab {
if (raw === 'settings' || raw === 'security' || raw === 'posts') return raw; if (raw === 'settings' || raw === 'security' || raw === 'points' || raw === 'posts') return raw;
return 'posts'; return 'posts';
} }
@@ -96,6 +97,8 @@ export default function ProfilePage() {
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
const dragCounter = useRef(0); const dragCounter = useRef(0);
const copyTimer = useRef<ReturnType<typeof setTimeout>>(); const copyTimer = useRef<ReturnType<typeof setTimeout>>();
const sigSectionRef = useRef<HTMLDivElement>(null);
const pendingFocusSig = useRef(false);
const { limits } = useForumLimits(); const { limits } = useForumLimits();
const pageSize = limits.page_size_default > 0 ? limits.page_size_default : 20; const pageSize = limits.page_size_default > 0 ? limits.page_size_default : 20;
@@ -124,6 +127,38 @@ export default function ProfilePage() {
setParams(nextParams, { replace: true }); setParams(nextParams, { replace: true });
}; };
const focusSignatureField = useCallback(() => {
sigSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.setTimeout(() => {
sigSectionRef.current?.querySelector('textarea')?.focus();
}, 80);
}, []);
/** 顶部签名 /「编辑资料」:跳到资料 Tab 并聚焦签名框 */
const goEditSignature = () => {
if (tab !== 'settings') {
pendingFocusSig.current = true;
setTab('settings');
return;
}
focusSignatureField();
};
const goEditProfile = () => {
if (tab !== 'settings') {
setTab('settings');
return;
}
window.scrollTo({ top: 0, behavior: 'smooth' });
};
useEffect(() => {
if (tab !== 'settings' || !pendingFocusSig.current) return;
pendingFocusSig.current = false;
const t = window.setTimeout(focusSignatureField, 40);
return () => window.clearTimeout(t);
}, [tab, focusSignatureField]);
useEffect(() => { useEffect(() => {
if (!authLoading && !user) { if (!authLoading && !user) {
nav(loginPath('/profile')); nav(loginPath('/profile'));
@@ -334,8 +369,9 @@ export default function ProfilePage() {
const tabs: { key: ProfileTab; label: string; count?: number }[] = [ const tabs: { key: ProfileTab; label: string; count?: number }[] = [
{ key: 'posts', label: '我的帖子', count: stats?.post_count }, { key: 'posts', label: '我的帖子', count: stats?.post_count },
{ key: 'settings', label: '资料设置' }, { key: 'points', label: '积分' },
{ key: 'security', label: '安全设置' }, { key: 'settings', label: '资料' },
{ key: 'security', label: '安全' },
]; ];
return ( return (
@@ -415,29 +451,38 @@ export default function ProfilePage() {
> >
</button> </button>
<button
type="button"
className="profile-id-copy"
onClick={goEditProfile}
>
<PenLine size={13} aria-hidden />
</button>
</div> </div>
{user.signature?.trim() ? ( {user.signature?.trim() ? (
<p className="profile-signature">{user.signature}</p> <button
type="button"
className="profile-signature profile-signature--editable"
onClick={goEditSignature}
title="编辑个性签名"
>
<span className="profile-signature-text">{user.signature}</span>
<span className="profile-signature-edit">
<PenLine size={13} aria-hidden />
</span>
</button>
) : ( ) : (
<p className="profile-signature profile-signature--empty"></p> <button
type="button"
className="profile-signature profile-signature--empty profile-signature--editable"
onClick={goEditSignature}
>
<PenLine size={14} aria-hidden />
</button>
)} )}
<dl className="profile-meta-list">
<div>
<dt></dt>
<dd>{user.username}</dd>
</div>
<div>
<dt></dt>
<dd>{user.email || '未设置'}</dd>
</div>
{joinedAt && (
<div>
<dt></dt>
<dd>{joinedAt}</dd>
</div>
)}
</dl>
<p className="profile-avatar-tip"></p>
</div> </div>
{pendingAvatar && ( {pendingAvatar && (
<div className="profile-avatar-actions"> <div className="profile-avatar-actions">
@@ -491,22 +536,19 @@ export default function ProfilePage() {
onConfirm={onCropConfirm} onConfirm={onCropConfirm}
/> />
<PointsWalletPanel />
{user.role === 'admin' && ( {user.role === 'admin' && (
<div className="section-card admin-entry-card"> <div className="admin-entry-bar">
<div className="section-card-title"></div> <span className="admin-entry-bar-label">
<p className="admin-entry-desc"> <Settings size={14} aria-hidden />
</p> </span>
<div className="flex flex-wrap gap-2"> <div className="admin-entry-bar-actions">
<Button variant="outline" onClick={() => nav('/admin/boards')}> <Button size="sm" variant="outline" onClick={() => nav('/admin/boards')}>
<Settings />
</Button> </Button>
<Button onClick={() => nav('/admin/dashboard')}> <Button size="sm" onClick={() => nav('/admin/dashboard')}>
<LayoutDashboard /> <LayoutDashboard size={14} />
</Button> </Button>
</div> </div>
</div> </div>
@@ -522,6 +564,7 @@ export default function ProfilePage() {
className={`profile-tab${tab === t.key ? ' active' : ''}`} className={`profile-tab${tab === t.key ? ' active' : ''}`}
onClick={() => setTab(t.key)} onClick={() => setTab(t.key)}
> >
{t.key === 'points' && <Coins size={14} aria-hidden className="profile-tab-icon" />}
{t.label} {t.label}
{typeof t.count === 'number' && ( {typeof t.count === 'number' && (
<span className="profile-tab-count">{t.count}</span> <span className="profile-tab-count">{t.count}</span>
@@ -565,29 +608,20 @@ export default function ProfilePage() {
</div> </div>
)} )}
{tab === 'points' && (
<div className="profile-panel profile-panel--points">
<PointsWalletPanel />
</div>
)}
{tab === 'settings' && ( {tab === 'settings' && (
<div className="section-card"> <div className="section-card">
<div className="section-card-title"></div> <div className="section-card-title"></div>
<p className="profile-settings-lead">
</p>
<Form {...nickForm}> <Form {...nickForm}>
<form onSubmit={nickForm.handleSubmit(onUpdateNick)} className="profile-form"> <form onSubmit={nickForm.handleSubmit(onUpdateNick)} className="profile-form">
<FormItem>
<FormLabel> ID</FormLabel>
<FormControl>
<Input value={String(user.id)} disabled />
</FormControl>
</FormItem>
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input value={user.username} disabled />
</FormControl>
</FormItem>
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input value={user.email || '未设置'} disabled />
</FormControl>
</FormItem>
<FormField <FormField
control={nickForm.control} control={nickForm.control}
name="nickname" name="nickname"
@@ -603,7 +637,7 @@ export default function ProfilePage() {
/> />
<div className="profile-form-footer"> <div className="profile-form-footer">
<span className="profile-form-hint"> <span className="profile-form-hint">
ID JPG / PNG / GIF / WebP WebP {limits.avatar_max_mb}MB JPG / PNG / GIF / WebP {limits.avatar_max_mb}MB
</span> </span>
<Button type="submit" loading={nickLoading}></Button> <Button type="submit" loading={nickLoading}></Button>
</div> </div>
@@ -612,34 +646,61 @@ export default function ProfilePage() {
<div className="profile-form-divider" /> <div className="profile-form-divider" />
<Form {...sigForm}> <div ref={sigSectionRef} id="profile-signature">
<form onSubmit={sigForm.handleSubmit(onUpdateSig)} className="profile-form"> <Form {...sigForm}>
<FormField <form onSubmit={sigForm.handleSubmit(onUpdateSig)} className="profile-form">
control={sigForm.control} <FormField
name="signature" control={sigForm.control}
render={({ field }) => ( name="signature"
<FormItem> render={({ field }) => (
<FormLabel></FormLabel> <FormItem>
<FormControl> <FormLabel></FormLabel>
<Textarea <FormControl>
rows={3} <Textarea
maxLength={sigMax} rows={3}
placeholder="写一句介绍自己的话,会显示在公开主页" maxLength={sigMax}
{...field} placeholder="写一句介绍自己的话,会显示在公开主页与个人中心顶部"
/> {...field}
</FormControl> />
<FormMessage /> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<div className="profile-form-footer"> />
<span className="profile-form-hint"> <div className="profile-form-footer">
{(sigForm.watch('signature') || '').length}/{sigMax} <span className="profile-form-hint">
</span> {(sigForm.watch('signature') || '').length}/{sigMax}
<Button type="submit" loading={sigLoading}></Button> </span>
<Button type="submit" loading={sigLoading}></Button>
</div>
</form>
</Form>
</div>
<div className="profile-form-divider" />
<div className="section-card-title section-card-title--sub"></div>
<div className="profile-form profile-form--readonly">
<div className="profile-readonly-field">
<Label htmlFor="profile-readonly-id"> ID</Label>
<Input id="profile-readonly-id" value={String(user.id)} disabled />
</div>
<div className="profile-readonly-field">
<Label htmlFor="profile-readonly-username"></Label>
<Input id="profile-readonly-username" value={user.username} disabled />
</div>
<div className="profile-readonly-field">
<Label htmlFor="profile-readonly-email"></Label>
<Input id="profile-readonly-email" value={user.email || '未设置'} disabled />
</div>
{joinedAt && (
<div className="profile-readonly-field">
<Label htmlFor="profile-readonly-joined"></Label>
<Input id="profile-readonly-joined" value={joinedAt} disabled />
</div> </div>
</form> )}
</Form> <p className="profile-form-hint"> ID </p>
</div>
</div> </div>
)} )}

View File

@@ -1512,14 +1512,25 @@ img.site-brand-logo-img {
/* 手机端自定义下拉刷新指示器(原生 PTR 依赖 document 滚动) */ /* 手机端自定义下拉刷新指示器(原生 PTR 依赖 document 滚动) */
.ptr-indicator { .ptr-indicator {
position: fixed; position: fixed;
top: 0; top: env(safe-area-inset-top, 0px);
left: 0; left: 0;
right: 0; right: 0;
z-index: 200; z-index: 90;
display: flex; display: flex;
justify-content: center; justify-content: center;
pointer-events: none; pointer-events: none;
transition: transform 0.12s ease-out; will-change: transform;
}
/* 有顶栏时从顶栏下方露出,避免叠在导航上造成「气泡错位」 */
body:has(.app-header) .ptr-indicator,
body:has(.admin-topbar) .ptr-indicator {
top: calc(var(--j13-header-h) + env(safe-area-inset-top, 0px));
}
/* 仅松手回弹 / 进入刷新态时过渡,拖动跟手不加 transition */
.ptr-indicator--settling {
transition: transform 0.2s ease-out;
} }
.ptr-indicator__chip { .ptr-indicator__chip {
@@ -2297,7 +2308,7 @@ img.site-brand-logo-img {
gap: 3px; gap: 3px;
flex-shrink: 0; flex-shrink: 0;
height: 20px; height: 20px;
padding: 0 7px 0 5px; padding: 0 7px;
border-radius: 4px; border-radius: 4px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@@ -2310,17 +2321,12 @@ img.site-brand-logo-img {
border: 1px solid rgba(220, 38, 38, 0.22); border: 1px solid rgba(220, 38, 38, 0.22);
} }
.post-pin-badge--icon {
width: 20px;
padding: 0;
justify-content: center;
}
.post-pin-badge--detail { .post-pin-badge--detail {
width: 24px;
height: 24px;
margin-right: 8px; margin-right: 8px;
vertical-align: -3px; height: 22px;
padding: 0 8px;
font-size: 12px;
vertical-align: middle;
} }
.dark .post-pin-badge { .dark .post-pin-badge {
@@ -2417,6 +2423,7 @@ img.site-brand-logo-img {
.waline-comment-status--rejected { color: #e11d48; } .waline-comment-status--rejected { color: #e11d48; }
.post-feature-badge { .post-feature-badge {
padding: 0 7px 0 5px;
color: #b45309; color: #b45309;
background: rgba(245, 158, 11, 0.12); background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.22); border: 1px solid rgba(245, 158, 11, 0.22);
@@ -2428,7 +2435,6 @@ img.site-brand-logo-img {
border-color: rgba(251, 191, 36, 0.22); border-color: rgba(251, 191, 36, 0.22);
} }
.post-pin-badge .post-pinned-icon,
.post-feature-badge .post-featured-icon { .post-feature-badge .post-featured-icon {
color: inherit; color: inherit;
vertical-align: 0; vertical-align: 0;
@@ -2531,14 +2537,6 @@ a.post-title:visited {
opacity: 0.8; opacity: 0.8;
} }
.post-pinned-icon {
display: inline-block;
vertical-align: -0.15em;
flex-shrink: 0;
color: var(--j13-green);
stroke-width: 1.75;
}
.post-featured-icon { .post-featured-icon {
display: inline-block; display: inline-block;
vertical-align: -0.15em; vertical-align: -0.15em;
@@ -5789,9 +5787,9 @@ a.waline-comment-author:hover {
"avatar info" "avatar info"
"stats stats"; "stats stats";
align-items: start; align-items: start;
gap: 20px 24px; gap: 16px 20px;
padding: 24px; padding: 20px;
margin-bottom: 16px; margin-bottom: 12px;
background: var(--j13-bg-block); background: var(--j13-bg-block);
border: 1px solid var(--j13-border-light); border: 1px solid var(--j13-border-light);
border-radius: 12px; border-radius: 12px;
@@ -5927,7 +5925,7 @@ a.waline-comment-author:hover {
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-top: 10px; margin-top: 8px;
} }
.profile-id-chip { .profile-id-chip {
@@ -5991,7 +5989,7 @@ a.waline-comment-author:hover {
} }
.profile-signature { .profile-signature {
margin: 12px 0 0; margin: 10px 0 0;
font-size: 13px; font-size: 13px;
line-height: 1.55; line-height: 1.55;
color: var(--color-text-2); color: var(--color-text-2);
@@ -6004,9 +6002,94 @@ a.waline-comment-author:hover {
font-style: italic; font-style: italic;
} }
button.profile-signature--editable {
display: inline-flex;
align-items: flex-start;
gap: 8px;
max-width: 100%;
padding: 6px 8px;
margin-left: -8px;
border: 1px dashed transparent;
border-radius: 8px;
background: transparent;
text-align: left;
cursor: pointer;
font: inherit;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
button.profile-signature--editable:hover,
button.profile-signature--editable:focus-visible {
background: var(--j13-green-bg);
border-color: color-mix(in srgb, var(--j13-green) 35%, transparent);
color: var(--j13-green);
outline: none;
}
button.profile-signature--editable.profile-signature--empty {
align-items: center;
color: var(--color-text-3);
font-style: normal;
}
button.profile-signature--editable.profile-signature--empty:hover,
button.profile-signature--editable.profile-signature--empty:focus-visible {
color: var(--j13-green);
}
.profile-signature-text {
flex: 1;
min-width: 0;
white-space: pre-wrap;
word-break: break-word;
}
.profile-signature-edit {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 3px;
margin-top: 1px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-4);
opacity: 0.85;
}
button.profile-signature--editable:hover .profile-signature-edit,
button.profile-signature--editable:focus-visible .profile-signature-edit {
color: var(--j13-green);
opacity: 1;
}
.profile-settings-lead {
margin: -4px 0 16px;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-3);
}
.section-card-title--sub {
margin-top: 0;
margin-bottom: 12px;
padding-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.profile-readonly-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.profile-form--readonly {
gap: 14px;
}
.profile-form-divider { .profile-form-divider {
height: 1px; height: 1px;
margin: 24px 0; margin: 20px 0;
background: var(--j13-border-light); background: var(--j13-border-light);
} }
@@ -6167,12 +6250,18 @@ button.profile-stat:hover strong {
.profile-tabs { .profile-tabs {
display: flex; display: flex;
gap: 4px; gap: 4px;
margin: 4px 0 16px; margin: 0 0 16px;
padding: 4px; padding: 4px;
border: 1px solid var(--j13-border-light); border: 1px solid var(--j13-border-light);
border-radius: 12px; border-radius: 12px;
background: var(--j13-bg-block); background: var(--j13-bg-block);
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.profile-tabs::-webkit-scrollbar {
display: none;
} }
.profile-tab { .profile-tab {
@@ -6194,6 +6283,11 @@ button.profile-stat:hover strong {
transition: background 0.15s, color 0.15s; transition: background 0.15s, color 0.15s;
} }
.profile-tab-icon {
flex-shrink: 0;
opacity: 0.85;
}
.profile-tab:hover { .profile-tab:hover {
color: var(--color-text-1); color: var(--color-text-1);
background: var(--j13-bg-surface); background: var(--j13-bg-surface);
@@ -6219,6 +6313,10 @@ button.profile-stat:hover strong {
margin-bottom: 24px; margin-bottom: 24px;
} }
.profile-panel--points .points-wallet {
margin-top: 0;
}
.profile-avatar-actions { .profile-avatar-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -6282,6 +6380,12 @@ button.profile-stat:hover strong {
justify-content: center; justify-content: center;
} }
button.profile-signature--editable {
margin-left: 0;
justify-content: center;
text-align: center;
}
.profile-meta-list { .profile-meta-list {
width: 100%; width: 100%;
text-align: left; text-align: left;
@@ -6292,6 +6396,24 @@ button.profile-stat:hover strong {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.profile-tab {
padding: 9px 8px;
font-size: 12px;
}
.admin-entry-bar {
flex-direction: column;
align-items: stretch;
}
.admin-entry-bar-actions {
width: 100%;
}
.admin-entry-bar-actions > * {
flex: 1;
}
.profile-form-footer { .profile-form-footer {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -6350,6 +6472,35 @@ button.profile-stat:hover strong {
background: var(--j13-bg-block-accent); background: var(--j13-bg-block-accent);
} }
/* 个人中心:站长入口紧凑条(避免挡在资料 Tab 前) */
.admin-entry-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
padding: 8px 12px;
border: 1px solid color-mix(in srgb, var(--j13-green) 22%, var(--j13-border-light));
border-radius: 10px;
background: var(--j13-bg-block-accent);
}
.admin-entry-bar-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: var(--j13-green);
}
.admin-entry-bar-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 通用懒加载占位:非 Feed 页勿用首页鱼骨骨架 */ /* 通用懒加载占位:非 Feed 页勿用首页鱼骨骨架 */
.page-loader { .page-loader {
display: flex; display: flex;