feat(analytics): SEO/GEO 看板默认最新优先并展示 Umami 接入状态
默认按发布时间降序、搜索防抖,API 传入 analyticsConfig 供 catalog 拉 Umami。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -666,6 +666,10 @@ export type SeoGeoCatalogResult = {
|
||||
geoViews: number;
|
||||
crawlerViews: number;
|
||||
};
|
||||
umami?: {
|
||||
available: boolean;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getSeoGeoPublicationCatalog(
|
||||
|
||||
Reference in New Issue
Block a user