本次将此前被列入退役名单的运营分析相关模块恢复为可用,并新增「模块化库存监控」
一级菜单及其两个子页面。
主要变更:
1. 恢复启用:商品监控、激励看板、营销活动、推广模块、商品分析
- retiredFeatures 移出对应退役名单与清表语句,改为正常保留
- 补齐后端路由、前端页面与 API 封装、侧边栏配置、桌面客户端能力项
- 同步更新 retiredFeatures / retiredRoutes / retiredNavigation / desktopSessionService 测试
2. 新增「模块化库存监控」一级菜单
- 配件库存监控:仅取「简尚-崔桥工厂半成品包装项目仓」SKU 的实时库存、
当日入库/出库、近 7 日日均发货量、库存可用天数与预警(库存可用天数 ≤ 7 预警)
- MKH-BOM资料:按商品编码展示配件明细、数量、仓位、库存,支持 A4 纵向打印
3. 新增产品资料系统数据库(PostgreSQL)连接模块:读取系统库 database_configs 中
name = product_information_database 的配置,凭据只从配置读取,不落代码
4. 新增系统设置页配置项:商品标签配置、部门商品标签配置、部门SKU负责人配置
5. 旧 /api/dashboard 接口迁移至 /api/productAnalysis,仪表盘预览页改用新接口,
相应移除 dashboard 路由模块及其测试
6. 修复 retiredRoutes 测试在 Windows 下挂起超时的问题:该测试用
parent.filename.endsWith('/api/planningUpload.js') 判断是否注入 mock,
而 Windows 的 filename 为反斜杠路径,导致判断恒为 false、mock 全部失效、
真实模块去连中央系统库而挂起(Linux CI 正常)。改为平台无关的路径匹配。
7. .gitignore 忽略本地验证产物(outputs/、.git-status.txt)
验证:
- 前端:lint / test(16 项)/ build(含 planning entry 校验)全部通过
- 后端:语法检查 149 个文件通过;测试 38/39 通过
- 已知未通过项:resilientSecretStore.test.js 断言密钥备份文件权限为 0o600,
Windows 下 chmod 无法表达该权限(实际为 0o666),属既有的平台相关失败,
与本次改动无关,本次未修改该文件
- 说明:本机 frontend/ 下存在未纳入版本库的旧依赖备份目录 node_modules_old_*,
会污染 ESLint 扫描(十余万条第三方报错),故 lint 步骤以
--ignore-pattern node_modules_old_*/** 排除后执行,其余步骤与仓库 check 一致
未提交内容:本地验证截图 outputs/ 与 .git-status.txt 已被 .gitignore 排除。
599 lines
25 KiB
JavaScript
599 lines
25 KiB
JavaScript
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 (
|
||
<>
|
||
<span>{parts.main}</span>
|
||
{parts.decimal ? <small>{parts.decimal}</small> : 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 (
|
||
<div className="development-style-preview">
|
||
<main className="preview-workbench">
|
||
<section className="preview-hero-grid">
|
||
<section className="preview-overview-panel">
|
||
<header className="preview-overview-header">
|
||
<div className="preview-overview-title">
|
||
<span className="preview-overview-title__mark" />
|
||
<h1>数据概览</h1>
|
||
</div>
|
||
<span className="preview-overview-time">
|
||
{dashboard?.latestDate ? `统计日期 ${dashboard.startDate} 至 ${dashboard.latestDate}` : '统计日期加载中'}
|
||
{dashboardLoading && dashboard ? '(更新中)' : ''}
|
||
</span>
|
||
<div className="preview-overview-actions">
|
||
<Select
|
||
className="preview-shop-select"
|
||
value={shopName}
|
||
options={shopOptions}
|
||
loading={shopOptionsLoading}
|
||
onChange={handleShopChange}
|
||
showSearch
|
||
optionFilterProp="label"
|
||
placeholder="选择店铺"
|
||
aria-label="筛选店铺"
|
||
popupMatchSelectWidth={260}
|
||
/>
|
||
<div className="preview-period-selector">
|
||
<SycmSegmentedSwitch
|
||
items={['日', '周', '月']}
|
||
activeKey={periodPickerOpen ? periodPickerType : activePeriod}
|
||
onChange={handlePeriodChange}
|
||
className="preview-period-switch"
|
||
/>
|
||
<DatePicker
|
||
className="preview-period-picker-anchor"
|
||
popupClassName="preview-period-picker-popup"
|
||
picker={PERIOD_PICKER_MAP[periodPickerType]}
|
||
value={periodPickerValue}
|
||
open={periodPickerOpen}
|
||
onChange={handlePeriodDateChange}
|
||
onOpenChange={(open) => {
|
||
if (!open) setPeriodPickerOpen(false);
|
||
}}
|
||
allowClear={false}
|
||
inputReadOnly
|
||
disabledDate={(current) => current?.isAfter(dayjs(), 'day')}
|
||
/>
|
||
</div>
|
||
<RangePicker
|
||
className={`preview-custom-date${activePeriod === '自定义' ? ' is-active' : ''}`}
|
||
value={displayedDateRange}
|
||
onChange={handleCustomDateChange}
|
||
allowClear
|
||
format="YYYY-MM-DD"
|
||
placeholder={['开始日期', '结束日期']}
|
||
suffixIcon={<CalendarOutlined />}
|
||
disabledDate={(current, info) => {
|
||
if (current?.isAfter(dayjs(), 'day')) return true;
|
||
return info?.from
|
||
? Math.abs(current.startOf('day').diff(info.from.startOf('day'), 'day')) > 89
|
||
: false;
|
||
}}
|
||
/>
|
||
<button type="button" className="preview-primary-action"><FullscreenOutlined />全部指标</button>
|
||
</div>
|
||
</header>
|
||
{dashboardError ? <Alert type="error" showIcon message={dashboardError} className="preview-dashboard-alert" /> : null}
|
||
|
||
<div className="preview-overview-body">
|
||
<section className="preview-metrics" aria-label="核心经营指标">
|
||
{dashboardLoading && !dashboard ? <Skeleton active paragraph={{ rows: 8 }} /> : displayedOverviewMetrics.map((item, index) => (
|
||
<SycmDashboardMetric
|
||
key={item.label}
|
||
label={item.label}
|
||
value={renderValueParts(item.value)}
|
||
trend={item.change === '无可比' ? item.change : `${formatToneIcon(item.tone)} ${item.change}`}
|
||
trendTone={item.tone}
|
||
footerLabel="上一周期"
|
||
footerValue={item.compare}
|
||
selected={selectedMetricIndex === index}
|
||
className={`preview-metric-card${index === 0 ? ' is-featured' : ''}`}
|
||
slotPrefix="preview-metric-card"
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-pressed={selectedMetricIndex === index}
|
||
onClick={() => setSelectedMetricIndex(index)}
|
||
onKeyDown={activateWithKeyboard(() => setSelectedMetricIndex(index))}
|
||
/>
|
||
))}
|
||
</section>
|
||
|
||
<SycmRealtimeTrendChart
|
||
aria-label="选定周期成交金额趋势"
|
||
className="preview-realtime-chart-card"
|
||
legendClassName="preview-realtime-legend"
|
||
plotClassName="preview-realtime-chart"
|
||
gridClassName="preview-realtime-chart__grid"
|
||
yAxisClassName="preview-realtime-chart__y-axis"
|
||
areaClassName="preview-realtime-chart__area"
|
||
lineClassName="preview-realtime-chart__line"
|
||
axisClassName="preview-realtime-chart__axis"
|
||
gradientId="previewRealtimeArea"
|
||
legendItems={[{ label: '成交金额', tone: 'today' }]}
|
||
yAxisLabels={trendChart.yLabels}
|
||
xAxisLabels={trendChart.xLabels}
|
||
primaryAreaPath={trendChart.areaPath}
|
||
primaryPath={trendChart.path}
|
||
tooltipItems={trendChart.points}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
</section>
|
||
|
||
<section className="preview-growth-section">
|
||
<header className="preview-growth-section__header">
|
||
<h2>增长因子</h2>
|
||
</header>
|
||
<div className="preview-growth-grid">
|
||
{displayedGrowthFactors.map((item, index) => (
|
||
<SycmGrowthFactorCard
|
||
key={item.label}
|
||
title={item.label}
|
||
value={renderValueParts(item.value)}
|
||
selected={selectedGrowthIndex === index}
|
||
className="preview-growth-card"
|
||
slotPrefix="preview-growth-card"
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-pressed={selectedGrowthIndex === index}
|
||
onClick={() => setSelectedGrowthIndex(index)}
|
||
onKeyDown={activateWithKeyboard(() => setSelectedGrowthIndex(index))}
|
||
sparkline={(
|
||
<SycmSparkline
|
||
data={item.series}
|
||
seriesName={item.label}
|
||
aria-label={`${item.label}近7日趋势`}
|
||
/>
|
||
)}
|
||
rows={[
|
||
<>较上一周期 <b>{item.change}</b></>,
|
||
<>上一周期 <em>{item.benchmark}</em></>,
|
||
]}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
<div className="preview-ad-grid">
|
||
<SycmInsightNotice
|
||
className="preview-insight-card"
|
||
slotPrefix="preview-insight-card"
|
||
icon={<BulbOutlined />}
|
||
title="拼多多核心经营数据"
|
||
description="支付金额、支付订单数、支付件数、访客和收藏数据均来自拼多多商品日维度业务表。"
|
||
/>
|
||
</div>
|
||
|
||
<div className="preview-business-grid">
|
||
<SycmMetricStrip className="preview-ad-stats" slotPrefix="preview-ad-stats" items={displayedAdStats} />
|
||
|
||
<SycmSimpleTable
|
||
className="preview-table-card"
|
||
slotPrefix="preview-table-card"
|
||
title="经营效率详情"
|
||
extra={dashboard?.latestDate || '加载中'}
|
||
columns={adColumns}
|
||
rows={displayedAdRows}
|
||
rowKey="scene"
|
||
selectedRowKey={selectedAdScene}
|
||
onRowClick={(_, key) => setSelectedAdScene(key)}
|
||
/>
|
||
|
||
<section className="preview-suggestion-module" aria-label="投放建议轮播">
|
||
<header>
|
||
<h2>投放建议</h2>
|
||
<div className="preview-suggestion-pager">
|
||
<span>{suggestionIndex + 1}/{suggestions.length}</span>
|
||
<button type="button" aria-label="上一条投放建议" onClick={() => moveSuggestion(-1)}><LeftOutlined /></button>
|
||
<button type="button" aria-label="下一条投放建议" onClick={() => moveSuggestion(1)}><RightOutlined /></button>
|
||
</div>
|
||
</header>
|
||
<div className="preview-suggestion-carousel">
|
||
<SycmRecommendationCard
|
||
key={suggestion.title}
|
||
className="preview-suggestion-card"
|
||
slotPrefix="preview-suggestion-card"
|
||
tone={suggestion.tone}
|
||
tag={suggestion.tag}
|
||
title={suggestion.title}
|
||
description={suggestion.desc}
|
||
actions={[
|
||
{
|
||
label: '有用',
|
||
selected: recommendationFeedback[suggestion.title] === '有用',
|
||
onClick: () => setRecommendationFeedback((current) => ({ ...current, [suggestion.title]: '有用' })),
|
||
},
|
||
{
|
||
label: '无用',
|
||
selected: recommendationFeedback[suggestion.title] === '无用',
|
||
onClick: () => setRecommendationFeedback((current) => ({ ...current, [suggestion.title]: '无用' })),
|
||
},
|
||
{
|
||
label: suggestion.action,
|
||
primary: true,
|
||
selected: recommendationFeedback[suggestion.title] === suggestion.action,
|
||
onClick: () => setRecommendationFeedback((current) => ({ ...current, [suggestion.title]: suggestion.action })),
|
||
},
|
||
]}
|
||
/>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="preview-card preview-journey-card">
|
||
<div className="preview-card__header">
|
||
<div>
|
||
<h2>客户旅程</h2>
|
||
<p>从潜客到支付的漏斗式业务模块,保持横向扫描效率</p>
|
||
</div>
|
||
<RiseOutlined />
|
||
</div>
|
||
<div className="preview-journey">
|
||
{journeySteps.map((step, index) => (
|
||
<article
|
||
key={step.label}
|
||
className={selectedJourney === step.label ? 'is-selected' : undefined}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-pressed={selectedJourney === step.label}
|
||
onClick={() => setSelectedJourney(step.label)}
|
||
onKeyDown={activateWithKeyboard(() => setSelectedJourney(step.label))}
|
||
>
|
||
<span>{step.label}</span>
|
||
<strong>{step.value}</strong>
|
||
<em>{step.sub}</em>
|
||
{index < journeySteps.length - 1 ? <b aria-hidden="true" /> : null}
|
||
</article>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="preview-guidelines" aria-label="UI 规范基准">
|
||
{guidelineItems.map((item) => {
|
||
const Icon = item.icon;
|
||
return (
|
||
<div
|
||
key={item.label}
|
||
className={selectedGuideline === item.label ? 'is-selected' : undefined}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-pressed={selectedGuideline === item.label}
|
||
onClick={() => setSelectedGuideline(item.label)}
|
||
onKeyDown={activateWithKeyboard(() => setSelectedGuideline(item.label))}
|
||
>
|
||
<Icon />
|
||
<span>{item.label}</span>
|
||
<strong>{item.value}</strong>
|
||
</div>
|
||
);
|
||
})}
|
||
</section>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DevelopmentStylePreview;
|