数据概览
增长因子
投放建议
客户旅程
从潜客到支付的漏斗式业务模块,保持横向扫描效率
import React, { useEffect, useState } from 'react'; import { AppstoreOutlined, BarChartOutlined, BulbOutlined, CalendarOutlined, LineChartOutlined, RiseOutlined, FullscreenOutlined, LeftOutlined, RightOutlined, TableOutlined, } from '@ant-design/icons'; import { Alert, DatePicker, Select, Skeleton } from 'antd'; import dayjs from 'dayjs'; import { SycmDashboardMetric, SycmGrowthFactorCard, SycmInsightNotice, SycmMetricStrip, SycmRecommendationCard, SycmRealtimeTrendChart, SycmSegmentedSwitch, SycmSimpleTable, SycmSparkline, } from '../../components/Common'; import { getDevelopmentDashboard, getProductAnalysisMeta } from '../../api/productAnalysis'; import './stylePreview.css'; const overviewMetrics = [ { key: 'payAmount', label: '支付金额', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up', featured: true }, { key: 'payOrderCount', label: '支付订单数', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, { key: 'visitorCount', label: '访客数', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, { key: 'buyerCount', label: '支付买家数', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, { key: 'conversionRate', label: '支付转化率', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, { key: 'payGoodsQty', label: '支付件数', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, { key: 'favoriteCount', label: '收藏人数', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, { key: 'goodsPv', label: '商品浏览量', value: '暂无数据', change: '无可比', compare: '暂无数据', tone: 'up' }, ]; const topOverviewMetrics = overviewMetrics.slice(0, 8); const growthFactors = [ { key: 'payAmount', label: '支付金额', trendKey: 'payAmount' }, { key: 'payOrderCount', label: '支付订单数', trendKey: 'payOrderCount' }, { key: 'payGoodsQty', label: '支付件数', trendKey: 'payGoodsQty' }, { key: 'favoriteCount', label: '收藏人数', trendKey: 'favoriteCount' }, ]; const adColumns = [ { key: 'scene', title: '经营指标', primary: true }, { key: 'spend', title: '当前周期' }, { key: 'click', title: '上一周期' }, { key: 'roi', title: '环比变化' }, ]; const suggestions = [ { tag: '经营提示', title: '关注核心经营指标的周期变化', desc: '结合支付金额、支付订单数、支付件数与访客变化,判断当前周期的增长质量。', action: '查看指标', tone: 'default', }, ]; const guidelineItems = [ { icon: AppstoreOutlined, label: '白底卡片', value: '1px 边框 + 轻阴影' }, { icon: BarChartOutlined, label: '12px 密度', value: '表格/辅助信息优先 12px' }, { icon: LineChartOutlined, label: '业务图表', value: '3px 线宽 + 弱网格' }, { icon: TableOutlined, label: '模块即规范', value: '新子页面以本页验收' }, ]; const formatToneIcon = (tone) => (tone === 'down' ? '↓' : '↑'); const formatValueParts = (value) => { const match = value.match(/^(.+?\.)(\d+%?)$/); return match ? { main: match[1], decimal: match[2] } : { main: value, decimal: '' }; }; const renderValueParts = (value) => { const parts = formatValueParts(value); return ( <> {parts.main} {parts.decimal ? {parts.decimal} : null} > ); }; const PERIOD_MAP = { 日: 'day', 周: 'week', 月: 'month' }; const PERIOD_PICKER_MAP = { 日: 'date', 周: 'week', 月: 'month' }; const { RangePicker } = DatePicker; const DASHBOARD_REQUEST_TIMEOUT_MS = Math.max( Number(import.meta.env.VITE_DASHBOARD_REQUEST_TIMEOUT_MS) || Number(import.meta.env.VITE_API_REQUEST_TIMEOUT_MS) || 180000, 15000 ); const numberFormatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }); const moneyFormatter = new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const formatDashboardValue = (value, format) => { if (value === null || value === undefined || !Number.isFinite(Number(value))) return '暂无数据'; if (format === 'money') return moneyFormatter.format(Number(value)); if (format === 'percent') return `${Number(value).toFixed(2)}%`; return numberFormatter.format(Number(value)); }; const formatChange = (value) => ( value === null || value === undefined || !Number.isFinite(Number(value)) ? '无可比' : `${Math.abs(Number(value)).toFixed(2)}%` ); const formatShortDate = (value) => (value ? value.slice(5) : '暂无'); const formatCompactMoney = (value) => { if (!Number.isFinite(Number(value))) return '暂无数据'; const amount = Number(value); return amount >= 10000 ? `${(amount / 10000).toFixed(2)}万` : moneyFormatter.format(amount); }; const resolvePeriodDateRange = (value, period) => { const selected = value.startOf('day'); if (period === '月') { return [selected.startOf('month'), selected.endOf('month').startOf('day')]; } if (period === '周') { const weekday = selected.day(); const monday = selected.add(weekday === 0 ? -6 : 1 - weekday, 'day'); return [monday, monday.add(6, 'day')]; } return [selected, selected]; }; const buildTrendChart = (rows = []) => { if (!rows.length) { return { xLabels: [], yLabels: [], path: '', areaPath: '', points: [] }; } const displayed = rows.length <= 6 ? rows : Array.from({ length: 6 }, (_, index) => rows[Math.round((rows.length - 1) * index / 5)]); const maxValue = Math.max(...displayed.map((row) => Number(row.payAmount) || 0), 1); const points = displayed.map((row, index) => { const x = displayed.length === 1 ? 356 : 76 + (560 * index) / (displayed.length - 1); const y = 392 - ((Number(row.payAmount) || 0) / maxValue) * 300; return { ...row, x, primaryY: y, compareY: null, primaryValue: formatCompactMoney(row.payAmount), compareValue: '无对比', label: row.date }; }); const path = points.map((point, index) => `${index ? 'L' : 'M'}${point.x.toFixed(1)} ${point.primaryY.toFixed(1)}`).join(' '); return { xLabels: displayed.map((row) => formatShortDate(row.date)), yLabels: [1, 0.75, 0.5, 0.25].map((ratio, index) => ({ label: formatCompactMoney(maxValue * ratio), y: 92 + index * 78 })), path, areaPath: path ? `${path} L${points.at(-1).x.toFixed(1)} 392 L${points[0].x.toFixed(1)} 392 Z` : '', points, }; }; const activateWithKeyboard = (callback) => (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); callback(); } }; const DevelopmentStylePreview = () => { const [suggestionIndex, setSuggestionIndex] = useState(0); const [selectedMetricIndex, setSelectedMetricIndex] = useState(0); const [selectedGrowthIndex, setSelectedGrowthIndex] = useState(0); const [activePeriod, setActivePeriod] = useState('周'); const [periodPickerType, setPeriodPickerType] = useState('周'); const [periodPickerOpen, setPeriodPickerOpen] = useState(false); const [customStartDate, setCustomStartDate] = useState(''); const [customEndDate, setCustomEndDate] = useState(''); const [selectedAdScene, setSelectedAdScene] = useState('支付金额'); const [recommendationFeedback, setRecommendationFeedback] = useState({}); const [selectedJourney, setSelectedJourney] = useState('曝光人数'); const [selectedGuideline, setSelectedGuideline] = useState(guidelineItems[0].label); const [dashboard, setDashboard] = useState(null); const [dashboardLoading, setDashboardLoading] = useState(true); const [dashboardError, setDashboardError] = useState(''); const [shopName, setShopName] = useState(''); const [shopOptions, setShopOptions] = useState([{ value: '', label: '全部店铺' }]); const [shopOptionsLoading, setShopOptionsLoading] = useState(true); useEffect(() => { let active = true; getProductAnalysisMeta().then((response) => { if (!active) return; const shops = Array.isArray(response?.data?.shops) ? response.data.shops : []; setShopOptions([ { value: '', label: '全部店铺' }, ...shops.map((name) => ({ value: name, label: name })), ]); }).catch(() => { if (active) setShopOptions([{ value: '', label: '全部店铺' }]); }).finally(() => { if (active) setShopOptionsLoading(false); }); return () => { active = false; }; }, []); useEffect(() => { const controller = new AbortController(); const params = customStartDate && customEndDate ? { startDate: customStartDate, endDate: customEndDate, shopName: shopName || undefined } : { period: PERIOD_MAP[activePeriod] || 'week', shopName: shopName || undefined }; getDevelopmentDashboard(params, { signal: controller.signal, timeout: DASHBOARD_REQUEST_TIMEOUT_MS }).then((response) => { const payload = response?.data || {}; setDashboard(payload); }).catch((error) => { if (error?.code !== 'ERR_CANCELED') { setDashboardError(error?.response?.data?.error || error.message || '首页数据加载失败'); } }).finally(() => { if (!controller.signal.aborted) setDashboardLoading(false); }); return () => controller.abort(); }, [activePeriod, customStartDate, customEndDate, shopName]); const applyDateRange = (startDate, endDate, period) => { setDashboardLoading(true); setDashboardError(''); setActivePeriod(period); setCustomStartDate(startDate.format('YYYY-MM-DD')); setCustomEndDate(endDate.format('YYYY-MM-DD')); setPeriodPickerOpen(false); }; const handlePeriodChange = (period) => { setPeriodPickerType(period); setPeriodPickerOpen(true); }; const handlePeriodDateChange = (value) => { if (!value) { setPeriodPickerOpen(false); return; } const [startDate, endDate] = resolvePeriodDateRange(value, periodPickerType); applyDateRange(startDate, endDate, periodPickerType); }; const handleCustomDateChange = (value) => { if (!Array.isArray(value) || value.length !== 2 || !value[0] || !value[1]) { setDashboardLoading(true); setDashboardError(''); setActivePeriod('周'); setPeriodPickerType('周'); setCustomStartDate(''); setCustomEndDate(''); return; } applyDateRange(value[0], value[1], '自定义'); }; const handleShopChange = (value) => { setShopName(value || ''); setDashboardLoading(true); setDashboardError(''); }; const displayedDateRange = customStartDate && customEndDate ? [dayjs(customStartDate), dayjs(customEndDate)] : dashboard?.startDate && dashboard?.latestDate ? [dayjs(dashboard.startDate), dayjs(dashboard.latestDate)] : null; const periodPickerValue = customStartDate || dashboard?.latestDate ? dayjs(customStartDate || dashboard.latestDate) : null; const displayedOverviewMetrics = topOverviewMetrics.map((fallback) => { const metric = dashboard?.metrics?.find((item) => item.key === fallback.key); if (!metric) return fallback; const tone = Number(metric.change) < 0 ? 'down' : 'up'; return { ...fallback, label: metric.label || fallback.label, value: formatDashboardValue(metric.value, metric.format), compare: formatDashboardValue(metric.previous, metric.format), change: formatChange(metric.change), tone, }; }); const trendChart = buildTrendChart(dashboard?.trend || []); const dashboardMetricMap = new Map((dashboard?.metrics || []).map((item) => [item.key, item])); const displayedGrowthFactors = growthFactors.map((item) => { const metric = dashboardMetricMap.get(item.key) || {}; return { ...item, value: formatDashboardValue(metric.value, metric.format), change: formatChange(metric.change), benchmark: formatDashboardValue(metric.previous, metric.format), series: (dashboard?.trend || []).map((row) => ({ label: row.date, value: Number(row[item.trendKey]) || 0, display: formatDashboardValue(row[item.trendKey], metric.format), })), }; }); const efficiencyMetricKeys = ['payAmount', 'payOrderCount', 'payGoodsQty']; const displayedAdStats = efficiencyMetricKeys.map((key) => { const metric = dashboardMetricMap.get(key) || {}; const tone = Number(metric.change) < 0 ? 'down' : 'up'; const change = formatChange(metric.change); return { label: metric.label || key, value: formatDashboardValue(metric.value, metric.format), trend: change === '无可比' ? change : `${formatToneIcon(tone)} ${change}`, tone, }; }); const displayedAdRows = efficiencyMetricKeys.map((key) => { const metric = dashboardMetricMap.get(key) || {}; const tone = Number(metric.change) < 0 ? 'down' : 'up'; const change = formatChange(metric.change); return { scene: metric.label || key, spend: formatDashboardValue(metric.value, metric.format), click: formatDashboardValue(metric.previous, metric.format), roi: change === '无可比' ? change : `${formatToneIcon(tone)} ${change}`, }; }); const currentTotals = (dashboard?.trend || []).reduce((result, row) => ({ impressionCount: result.impressionCount + (Number(row.impressionCount) || 0), }), { impressionCount: 0 }); const journeySteps = [ { label: '曝光人数', value: formatDashboardValue(currentTotals.impressionCount, 'integer'), sub: '商品曝光人数' }, { label: '访客人数', value: formatDashboardValue(dashboardMetricMap.get('visitorCount')?.value, 'integer'), sub: '进入商品详情' }, { label: '商品浏览', value: formatDashboardValue(dashboardMetricMap.get('goodsPv')?.value, 'integer'), sub: '商品浏览次数' }, { label: '收藏人数', value: formatDashboardValue(dashboardMetricMap.get('favoriteCount')?.value, 'integer'), sub: '产生收藏行为' }, { label: '支付买家', value: formatDashboardValue(dashboardMetricMap.get('buyerCount')?.value, 'integer'), sub: '完成支付买家' }, ]; const suggestion = suggestions[suggestionIndex]; const moveSuggestion = (offset) => { setSuggestionIndex((current) => (current + offset + suggestions.length) % suggestions.length); }; return (
从潜客到支付的漏斗式业务模块,保持横向扫描效率