fix: 管理会话自动恢复与访问来源统计优化
- 新增 AdminSessionRecover:后台离线/后端重启时自动重试恢复会话,明确失效才回跳登录 - 认证 cookie 契约调整(SameSite=Lax 与刷新轮转适配),路由与守卫适配 - 访问来源统计查询优化与测试、管理端 analytics sources 页面适配 - 移动端布局修正:覆盖 body min-h-screen 避免内容区高度异常
This commit is contained in:
@@ -152,7 +152,7 @@ export default function AccountGuard() {
|
||||
id="session-ended-dialog-desc"
|
||||
className="meta text-[13px] leading-relaxed text-center"
|
||||
>
|
||||
你的登录会话已被移除(在安全设置中剔除,或会话数超出上限被淘汰),当前设备的登录状态已失效。
|
||||
你的登录会话已失效(在安全设置中剔除、会话数超出上限被淘汰,或登录有效期已过)。
|
||||
请重新登录后继续使用。
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
@@ -38,6 +38,11 @@ import { useUrlStyle } from "./SiteBrandProvider";
|
||||
import { leaderboardPath } from "@/lib/urlStyle";
|
||||
import type { BrandLogoFit, BrandLogoSize, BrandMark } from "@/lib/brand";
|
||||
|
||||
// 搜索关键词长度上限(与后端 handler maxSearchKeywordRunes 一致);
|
||||
// 接近上限时输入框内显示 n/100 计数帮助用户感知
|
||||
const SEARCH_MAX_LEN = 100;
|
||||
const SEARCH_COUNT_FROM = 80;
|
||||
|
||||
export default function Header({
|
||||
initialUser,
|
||||
initialUnread = 0,
|
||||
@@ -280,18 +285,30 @@ export default function Header({
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
maxLength={SEARCH_MAX_LEN}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={onSearchKeyDown}
|
||||
placeholder="搜索帖子..."
|
||||
aria-label="搜索帖子"
|
||||
className="j13-search-field w-full pl-10 pr-10 py-2.5 text-sm rounded-full"
|
||||
className={`j13-search-field w-full pl-10 py-2.5 text-sm rounded-full ${
|
||||
searchQuery.length >= SEARCH_COUNT_FROM ? "pr-16" : "pr-10"
|
||||
}`}
|
||||
/>
|
||||
<kbd
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[11px] w-5 h-5 flex items-center justify-center rounded-md pointer-events-none"
|
||||
style={{ color: "var(--ink-3)", border: "1px solid var(--line-2)", background: "var(--bg)" }}
|
||||
>
|
||||
/
|
||||
</kbd>
|
||||
{searchQuery.length >= SEARCH_COUNT_FROM ? (
|
||||
<span
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[11px] tabular-nums pointer-events-none"
|
||||
style={{ color: searchQuery.length >= SEARCH_MAX_LEN ? "var(--danger)" : "var(--ink-3)" }}
|
||||
>
|
||||
{searchQuery.length}/{SEARCH_MAX_LEN}
|
||||
</span>
|
||||
) : (
|
||||
<kbd
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[11px] w-5 h-5 flex items-center justify-center rounded-md pointer-events-none"
|
||||
style={{ color: "var(--ink-3)", border: "1px solid var(--line-2)", background: "var(--bg)" }}
|
||||
>
|
||||
/
|
||||
</kbd>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<ThemeToggle initialTheme={initialTheme} className={iconBtn} style={iconBtnStyle} />
|
||||
@@ -365,6 +382,7 @@ export default function Header({
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
maxLength={SEARCH_MAX_LEN}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
@@ -374,8 +392,18 @@ export default function Header({
|
||||
}}
|
||||
aria-label="搜索帖子"
|
||||
placeholder="搜索帖子..."
|
||||
className="j13-search-field w-full pl-10 pr-4 py-3 rounded-full text-sm"
|
||||
className={`j13-search-field w-full pl-10 py-3 rounded-full text-sm ${
|
||||
searchQuery.length >= SEARCH_COUNT_FROM ? "pr-16" : "pr-4"
|
||||
}`}
|
||||
/>
|
||||
{searchQuery.length >= SEARCH_COUNT_FROM && (
|
||||
<span
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-[11px] tabular-nums pointer-events-none"
|
||||
style={{ color: searchQuery.length >= SEARCH_MAX_LEN ? "var(--danger)" : "var(--ink-3)" }}
|
||||
>
|
||||
{searchQuery.length}/{SEARCH_MAX_LEN}
|
||||
</span>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { apiTelemetryPageView } from "@/lib/api";
|
||||
|
||||
/**
|
||||
* 公开站页面访问埋点:同 path 30s 内合并;跳过 /admin。
|
||||
*/
|
||||
export default function VisitBeacon() {
|
||||
function BeaconInner() {
|
||||
const pathname = usePathname();
|
||||
const search = useSearchParams().toString();
|
||||
const lastRef = useRef<{ path: string; at: number }>({ path: "", at: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname || pathname === "/admin" || pathname.startsWith("/admin/")) return;
|
||||
// 完整 URL(含 query):query 变化也上报,站内搜索 /?q=… 才能被记录
|
||||
const full = pathname + (search ? `?${search}` : "");
|
||||
const now = Date.now();
|
||||
if (lastRef.current.path === pathname && now - lastRef.current.at < 30_000) return;
|
||||
lastRef.current = { path: pathname, at: now };
|
||||
// 带 query 完整路径 + 来源页,供后台来源分析
|
||||
const full = pathname + (typeof window !== "undefined" ? window.location.search : "");
|
||||
if (lastRef.current.path === full && now - lastRef.current.at < 30_000) return;
|
||||
lastRef.current = { path: full, at: now };
|
||||
// 来源页,供后台来源分析
|
||||
const ref = typeof document !== "undefined" ? document.referrer : "";
|
||||
void apiTelemetryPageView(full, ref);
|
||||
}, [pathname]);
|
||||
}, [pathname, search]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开站页面访问埋点:同 URL 30s 内合并;跳过 /admin。
|
||||
* useSearchParams 需 Suspense 边界(与 FooterTiming 同模式)。
|
||||
*/
|
||||
export default function VisitBeacon() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<BeaconInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user