{region.region}
- {region.accounts} 家客户
+ {region.accounts} {t['dashboardFinancial.label.customers']}
${region.revenue.toLocaleString()} ({region.percentage}%)
@@ -586,8 +652,7 @@ export default function DashboardFinancialPage() {
}}
>
- {(analyticsData?.currencyBreakdown || []).map(
- (curr: CurrencyBreakdownItem) => (
+ {analyticsData.currencyBreakdown.map((curr: CurrencyBreakdownItem) => (
- {curr.count} 笔订单
+ {curr.count} {t['dashboardFinancial.label.orders']}
- {curr.percentage}% 占比
+
+ {curr.percentage}% {t['dashboardFinancial.label.share']}
+
))}
@@ -631,14 +698,14 @@ export default function DashboardFinancialPage() {
}}
>
- {(analyticsData?.agingAnalysis || []).map(
- (aging: AgingAnalysisItem) => {
+ {analyticsData.agingAnalysis.map((aging: AgingAnalysisItem) => {
const tagColor =
aging.riskLevel === 'low'
? 'green'
: aging.riskLevel === 'medium'
? 'orange'
: 'red';
+ const riskKey = `dashboardFinancial.risk.${aging.riskLevel}`;
return (
{aging.range}
- 关联 {aging.invoiceCount} 笔开票账单
+ {t['dashboardFinancial.label.linkedInvoices']}{' '}
+ {aging.invoiceCount}
${aging.amount.toLocaleString()}
-
- {aging.riskLevel === 'low'
- ? '安全正常'
- : aging.riskLevel === 'medium'
- ? '关注催收'
- : '坏账风险'}
-
+ {t[riskKey] || aging.riskLevel}
);
@@ -690,7 +752,7 @@ export default function DashboardFinancialPage() {
diff --git a/admin/src/pages/dashboard/financial/locale/index.ts b/admin/src/pages/dashboard/financial/locale/index.ts
index 74f5359..01e0f0b 100644
--- a/admin/src/pages/dashboard/financial/locale/index.ts
+++ b/admin/src/pages/dashboard/financial/locale/index.ts
@@ -51,6 +51,19 @@ const i18n = {
'dashboardFinancial.status.issued': 'Issued (Pending)',
'dashboardFinancial.status.refunded': 'Refunded',
'dashboardFinancial.export.success': 'Reconciliation ledger exported successfully',
+ 'dashboardFinancial.export.unavailable': 'Export is not available yet',
+ 'dashboardFinancial.status.loadFailed': 'Failed to load financial analytics',
+ 'dashboardFinancial.label.accounts': 'accounts',
+ 'dashboardFinancial.label.mrrContribution': 'MRR contribution',
+ 'dashboardFinancial.label.seatUtilization': 'Seat utilization',
+ 'dashboardFinancial.label.customers': 'customers',
+ 'dashboardFinancial.label.orders': 'orders',
+ 'dashboardFinancial.label.share': 'share',
+ 'dashboardFinancial.label.linkedInvoices': 'Linked invoices',
+ 'dashboardFinancial.risk.low': 'Healthy',
+ 'dashboardFinancial.risk.medium': 'Watch collection',
+ 'dashboardFinancial.risk.high': 'Bad-debt risk',
+ 'dashboardFinancial.currency.settlement': 'settlement',
},
'zh-CN': {
'dashboardFinancial.title': '全球财务与商业化运营驾驶舱',
@@ -104,6 +117,19 @@ const i18n = {
'dashboardFinancial.status.issued': '已开具 (待核销)',
'dashboardFinancial.status.refunded': '已退款',
'dashboardFinancial.export.success': '财务对账流水报表已导出',
+ 'dashboardFinancial.export.unavailable': '暂不可导出',
+ 'dashboardFinancial.status.loadFailed': '财务分析数据加载失败',
+ 'dashboardFinancial.label.accounts': '账套',
+ 'dashboardFinancial.label.mrrContribution': 'MRR 贡献',
+ 'dashboardFinancial.label.seatUtilization': '席位激活率',
+ 'dashboardFinancial.label.customers': '家客户',
+ 'dashboardFinancial.label.orders': '笔订单',
+ 'dashboardFinancial.label.share': '占比',
+ 'dashboardFinancial.label.linkedInvoices': '关联开票账单',
+ 'dashboardFinancial.risk.low': '安全正常',
+ 'dashboardFinancial.risk.medium': '关注催收',
+ 'dashboardFinancial.risk.high': '坏账风险',
+ 'dashboardFinancial.currency.settlement': '结算',
},
'ar-SA': {
'dashboardFinancial.title': 'لوحة تحليلات المالية والعمليات',
@@ -153,6 +179,19 @@ const i18n = {
'dashboardFinancial.status.issued': 'صادر (معلق)',
'dashboardFinancial.status.refunded': 'مسترد',
'dashboardFinancial.export.success': 'تم تصدير تقرير التسوية بنجاح',
+ 'dashboardFinancial.export.unavailable': 'التصدير غير متاح حالياً',
+ 'dashboardFinancial.status.loadFailed': 'فشل تحميل التحليلات المالية',
+ 'dashboardFinancial.label.accounts': 'حسابات',
+ 'dashboardFinancial.label.mrrContribution': 'مساهمة الإيرادات الشهرية',
+ 'dashboardFinancial.label.seatUtilization': 'معدل تفعيل المقاعد',
+ 'dashboardFinancial.label.customers': 'عملاء',
+ 'dashboardFinancial.label.orders': 'طلبات',
+ 'dashboardFinancial.label.share': 'الحصة',
+ 'dashboardFinancial.label.linkedInvoices': 'فواتير مرتبطة',
+ 'dashboardFinancial.risk.low': 'آمن',
+ 'dashboardFinancial.risk.medium': 'مراقبة التحصيل',
+ 'dashboardFinancial.risk.high': 'مخاطر الديون المعدومة',
+ 'dashboardFinancial.currency.settlement': 'تسوية',
},
};
diff --git a/admin/src/pages/dashboard/radar-map/index.tsx b/admin/src/pages/dashboard/radar-map/index.tsx
index 59599af..e7e74df 100644
--- a/admin/src/pages/dashboard/radar-map/index.tsx
+++ b/admin/src/pages/dashboard/radar-map/index.tsx
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
Card,
+ Empty,
Grid,
Spin,
Tag,
@@ -63,8 +64,21 @@ interface ApiResponse
{
data?: T;
}
+/** 雷达热力大屏聚合接口:国家分布来自服务端 countryDistribution(同源 getCountryDistribution) */
const ANALYTICS_API = '/api/admin/crm/radar/analytics';
+const EMPTY_ANALYTICS: RadarAnalyticsData = {
+ countryDistribution: [],
+ hourlyStats: [],
+ deviceStats: [],
+ summary: {
+ totalVisitors: 0,
+ avgStayDurationMs: 0,
+ topCountry: '',
+ countriesCount: 0,
+ },
+};
+
function formatStayTime(ms: number, unitSeconds: string): string {
if (ms <= 0) {
return `0 ${unitSeconds}`;
@@ -77,17 +91,8 @@ export default function RadarMapDashboard(): React.ReactElement {
const t = useLocale(locale);
const [loading, setLoading] = useState(false);
const [filterCardId, setFilterCardId] = useState('');
- const [analyticsData, setAnalyticsData] = useState({
- countryDistribution: [],
- hourlyStats: [],
- deviceStats: [],
- summary: {
- totalVisitors: 0,
- avgStayDurationMs: 0,
- topCountry: '-',
- countriesCount: 0,
- },
- });
+ const [analyticsData, setAnalyticsData] =
+ useState(EMPTY_ANALYTICS);
const fetchData = useCallback(
async (cardId?: string) => {
@@ -100,11 +105,26 @@ export default function RadarMapDashboard(): React.ReactElement {
});
if (res.data && res.data.code === 1000 && res.data.data) {
- setAnalyticsData(res.data.data);
+ const payload = res.data.data;
+ setAnalyticsData({
+ countryDistribution: Array.isArray(payload.countryDistribution)
+ ? payload.countryDistribution
+ : [],
+ hourlyStats: Array.isArray(payload.hourlyStats) ? payload.hourlyStats : [],
+ deviceStats: Array.isArray(payload.deviceStats) ? payload.deviceStats : [],
+ summary: {
+ totalVisitors: Number(payload.summary?.totalVisitors) || 0,
+ avgStayDurationMs: Number(payload.summary?.avgStayDurationMs) || 0,
+ topCountry: payload.summary?.topCountry || '',
+ countriesCount: Number(payload.summary?.countriesCount) || 0,
+ },
+ });
} else {
+ setAnalyticsData(EMPTY_ANALYTICS);
Message.warning(res.data?.message || t['radarMap.status.loadFailed']);
}
} catch (err: unknown) {
+ setAnalyticsData(EMPTY_ANALYTICS);
const errorMessage =
err instanceof Error ? err.message : t['radarMap.status.loadFailed'];
Message.error(errorMessage);
@@ -151,7 +171,7 @@ export default function RadarMapDashboard(): React.ReactElement {
dataIndex: 'country',
render: (value: string) => (
- {value || 'Unknown'}
+ {value || t['radarMap.country.unknown']}
),
},
@@ -197,8 +217,9 @@ export default function RadarMapDashboard(): React.ReactElement {
},
];
- // 时段峰值小时 (前 6 个高峰时段展示)
+ // 时段峰值小时 (前 6 个有访问的高峰时段)
const activePeakHours = [...(analyticsData.hourlyStats || [])]
+ .filter((item) => item.visitorCount > 0)
.sort((a, b) => b.visitorCount - a.visitorCount)
.slice(0, 6);
@@ -230,6 +251,14 @@ export default function RadarMapDashboard(): React.ReactElement {
};
const top10Countries = (analyticsData.countryDistribution || []).slice(0, 10);
+ const activeDeviceStats = (analyticsData.deviceStats || []).filter(
+ (item) => item.count > 0
+ );
+ const topCountryLabel =
+ analyticsData.summary.topCountry &&
+ analyticsData.summary.topCountry !== '-'
+ ? analyticsData.summary.topCountry
+ : t['radarMap.kpi.topCountry.empty'];
return (
@@ -392,7 +421,7 @@ export default function RadarMapDashboard(): React.ReactElement {
whiteSpace: 'nowrap',
}}
>
- {analyticsData.summary.topCountry}
+ {topCountryLabel}
@@ -423,11 +452,7 @@ export default function RadarMapDashboard(): React.ReactElement {
data={top10Countries}
pagination={false}
noDataElement={
-
-
- {t['radarMap.status.empty']}
-
-
+
}
/>
@@ -447,7 +472,7 @@ export default function RadarMapDashboard(): React.ReactElement {
}}
>
- {(analyticsData.deviceStats || []).map((item) => {
+ {activeDeviceStats.map((item) => {
const color = getChannelColor(item.type);
const name = getChannelName(item.type);
return (
@@ -474,10 +499,8 @@ export default function RadarMapDashboard(): React.ReactElement {
);
})}
- {(!analyticsData.deviceStats || analyticsData.deviceStats.length === 0) && (
-
- {t['radarMap.status.empty']}
-
+ {activeDeviceStats.length === 0 && (
+
)}
@@ -496,6 +519,9 @@ export default function RadarMapDashboard(): React.ReactElement {
| undefined): WorkplaceKpi {
+ if (!raw) {
+ return { ...EMPTY_KPI };
+ }
+ return {
+ tapCount: toNonNegNumber(raw.tapCount),
+ viewCount: toNonNegNumber(raw.viewCount),
+ newLeadCount: toNonNegNumber(raw.newLeadCount),
+ conversionRate: toNonNegNumber(raw.conversionRate),
+ tapDelta: Number.isFinite(Number(raw.tapDelta)) ? Number(raw.tapDelta) : 0,
+ viewDelta: Number.isFinite(Number(raw.viewDelta)) ? Number(raw.viewDelta) : 0,
+ newLeadDelta: Number.isFinite(Number(raw.newLeadDelta))
+ ? Number(raw.newLeadDelta)
+ : 0,
+ };
+}
+
function Workplace(): React.ReactElement {
const t = useLocale(locale);
const history = useHistory();
@@ -237,13 +262,21 @@ function Workplace(): React.ReactElement {
OVERVIEW_API,
{ feedLimit: 15 }
);
- if (payload.data?.kpis) {
- setOverview({
- kpis: payload.data.kpis,
- radarFeed: payload.data.radarFeed || [],
- });
+ if (payload.code !== 1000 || !payload.data?.kpis) {
+ setOverview({ kpis: EMPTY_KPI, radarFeed: [] });
+ if (payload.message) {
+ Message.error(payload.message);
+ }
+ return;
}
+ setOverview({
+ kpis: normalizeKpis(payload.data.kpis),
+ radarFeed: Array.isArray(payload.data.radarFeed)
+ ? payload.data.radarFeed
+ : [],
+ });
} catch (error: unknown) {
+ setOverview({ kpis: EMPTY_KPI, radarFeed: [] });
Message.error(resolveErrorMessage(error, t['workplace.request.fail']));
} finally {
setLoading(false);
@@ -324,11 +357,11 @@ function Workplace(): React.ReactElement {
const kpis = overview.kpis;
- // 渠道分布统计计算
+ // 渠道分布:仅由 radarFeed 聚合;无数据时全 0,不回退演示假数
const channelStats = useMemo(() => {
const total = overview.radarFeed.length;
if (total === 0) {
- return { nfc: 50, qr: 30, link: 20 };
+ return { nfc: 0, qr: 0, link: 0, total: 0 };
}
let nfc = 0;
let qr = 0;
@@ -345,9 +378,21 @@ function Workplace(): React.ReactElement {
nfc: nfcPct,
qr: qrPct,
link: linkPct,
+ total,
};
}, [overview.radarFeed]);
+ const topAlert = useMemo(() => {
+ const highIntent = overview.radarFeed
+ .map((item) => ({ item, score: computeIntentScore(item) }))
+ .filter((entry) => entry.score >= 70)
+ .sort(
+ (a, b) =>
+ b.score - a.score || b.item.stayDurationMs - a.item.stayDurationMs
+ );
+ return highIntent[0] || null;
+ }, [overview.radarFeed]);
+
const interactCount = useMemo(
() => overview.radarFeed.filter((f) => f.stayDurationMs > 10000).length,
[overview.radarFeed]
@@ -508,7 +553,7 @@ function Workplace(): React.ReactElement {