feat(analytics): SEO/GEO 看板默认最新优先并展示 Umami 接入状态

默认按发布时间降序、搜索防抖,API 传入 analyticsConfig 供 catalog 拉 Umami。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-17 15:15:26 +08:00
parent 799afd1ea5
commit eba37640e4
3 changed files with 29 additions and 6 deletions
+3
View File
@@ -261,6 +261,7 @@ export function createAdminApp(services) {
const { listAdminSeoGeoPublicationCatalog } = await importMemind('mindspace-seo-geo-admin-catalog.mjs');
const startAt = Number(req.query.startAt);
const endAt = Number(req.query.endAt);
const config = loadMindSpaceConfig ? await loadMindSpaceConfig(pool) : null;
const result = await listAdminSeoGeoPublicationCatalog(pool, {
page: Number(req.query.page) || 1,
pageSize: Number(req.query.pageSize) || 20,
@@ -270,6 +271,8 @@ export function createAdminApp(services) {
endAt: Number.isFinite(endAt) ? endAt : null,
search: typeof req.query.search === 'string' ? req.query.search.trim() : '',
publicHost: process.env.H5_PUBLIC_BASE_URL?.replace(/^https?:\/\//, '').replace(/\/$/, '') || 'm.tkmind.cn',
env: process.env,
analyticsConfig: config?.analytics ?? {},
});
res.json(result);
} catch (error) {
+22 -6
View File
@@ -54,7 +54,8 @@ export function SeoGeoAnalyticsPage() {
const [startDate, setStartDate] = useState(initialRange.startDate);
const [endDate, setEndDate] = useState(initialRange.endDate);
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState<PageSort>('totalViews');
const [searchInput, setSearchInput] = useState('');
const [sortBy, setSortBy] = useState<PageSort>('publishedAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const [rows, setRows] = useState<SeoGeoCatalogRow[]>([]);
@@ -68,6 +69,7 @@ export function SeoGeoAnalyticsPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [openingUmami, setOpeningUmami] = useState(false);
const [umamiStatus, setUmamiStatus] = useState<{ available: boolean; reason: string } | null>(null);
const pageCount = Math.max(1, Math.ceil(total / 20));
@@ -93,11 +95,13 @@ export function SeoGeoAnalyticsPage() {
geoViews: 0,
crawlerViews: 0,
});
setUmamiStatus(result.umami ?? null);
} catch (err) {
setError(err instanceof Error ? err.message : '加载 SEO/GEO 页面目录失败');
setRows([]);
setTotal(0);
setTotals({ totalViews: 0, seoViews: 0, geoViews: 0, crawlerViews: 0 });
setUmamiStatus(null);
} finally {
setLoading(false);
}
@@ -107,6 +111,13 @@ export function SeoGeoAnalyticsPage() {
void load();
}, [load]);
useEffect(() => {
const timer = window.setTimeout(() => {
setSearch(searchInput.trim());
}, 300);
return () => window.clearTimeout(timer);
}, [searchInput]);
useEffect(() => {
setPage(1);
}, [startDate, endDate, search, sortBy, sortOrder]);
@@ -127,7 +138,12 @@ export function SeoGeoAnalyticsPage() {
<div className="admin-page-head">
<h2>SEO / GEO </h2>
<p className="muted">
线 URL访 = SEO / GEO referrer User-Agent
线 URL访 = SEO / GEO Umami MindSpace /u/ MySQL
{umamiStatus && !umamiStatus.available && umamiStatus.reason
? ` 当前 Umami 未接入(${umamiStatus.reason}),MindSpace 直链流量可能偏低。`
: umamiStatus?.available
? ' Umami 已接入。'
: ''}
</p>
</div>
@@ -156,20 +172,20 @@ export function SeoGeoAnalyticsPage() {
<span> / / URL</span>
<input
type="search"
value={search}
placeholder="例如 john、sitemap、问卷"
onChange={(e) => setSearch(e.target.value)}
value={searchInput}
placeholder="例如 john、daily-news、0917"
onChange={(e) => setSearchInput(e.target.value)}
/>
</label>
<label className="admin-form-row">
<span></span>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as PageSort)}>
<option value="publishedAt"></option>
<option value="totalViews">访</option>
<option value="seoViews">SEO </option>
<option value="geoViews">GEO </option>
<option value="crawlerViews">访</option>
<option value="lifetimeViewCount">访</option>
<option value="publishedAt"></option>
<option value="pageTitle"></option>
<option value="ownerLabel"></option>
</select>
+4
View File
@@ -666,6 +666,10 @@ export type SeoGeoCatalogResult = {
geoViews: number;
crawlerViews: number;
};
umami?: {
available: boolean;
reason: string;
};
};
export async function getSeoGeoPublicationCatalog(