diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json
index 98ddeb1..77dd792 100644
--- a/.axhub/make/sidebar-tree.json
+++ b/.axhub/make/sidebar-tree.json
@@ -1,7 +1,19 @@
{
"version": 1,
- "updatedAt": "2026-07-15T00:52:00.000Z",
+ "updatedAt": "2026-07-17T03:32:12.024Z",
"prototypes": [
+ {
+ "id": "item-prototypes-oneos-web-approval",
+ "kind": "item",
+ "title": "oneos web approval",
+ "itemKey": "prototypes/oneos-web-approval"
+ },
+ {
+ "id": "item-prototypes-oneos-web-h2-station-analysis",
+ "kind": "item",
+ "title": "oneos web h2 station analysis",
+ "itemKey": "prototypes/oneos-web-h2-station-analysis"
+ },
{
"id": "folder-1783875936186-27raza",
"kind": "folder",
diff --git a/src/prototypes/oneos-prototype-nav/prototype-registry.json b/src/prototypes/oneos-prototype-nav/prototype-registry.json
index e9a1e0e..8ccba1f 100644
--- a/src/prototypes/oneos-prototype-nav/prototype-registry.json
+++ b/src/prototypes/oneos-prototype-nav/prototype-registry.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updatedAt": "2026-07-16T01:06:15.895Z",
+ "updatedAt": "2026-07-20T01:48:32.898Z",
"prototypes": {
"oneos-web-workbench": {
"title": "工作台",
@@ -3879,11 +3879,11 @@
"changelog": []
},
"oneos-web-h2-station-analysis": {
- "title": "oneos-web-h2-station-analysis",
- "version": "v1.6",
- "revision": 6,
- "lastUpdated": "2026-07-13T07:20:12.550Z",
- "contentHash": "a9a6164ccd59111309617d0c4421bd809d07d84a496ad7552bb0af57a4afa463",
+ "title": "oneos web h2 station analysis",
+ "version": "v1.7",
+ "revision": 7,
+ "lastUpdated": "2026-07-20T01:48:32.897Z",
+ "contentHash": "a34c72c7929aff96697f049c4f730fb0bb7cc3e5e1c7c76513c2cd4e6e55e234",
"trackedFiles": [
"index.tsx",
"pages/01-加氢站分析.jsx",
@@ -3891,6 +3891,18 @@
"shared/h2-analysis-mock.js"
],
"changelog": [
+ {
+ "version": "v1.7",
+ "date": "2026-07-20",
+ "time": "09:48",
+ "summary": "更新页面逻辑与交互",
+ "files": [
+ "index.tsx",
+ "pages/01-加氢站分析.jsx",
+ "shared/h2-analysis-charts.jsx",
+ "shared/h2-analysis-mock.js"
+ ]
+ },
{
"version": "v1.6",
"date": "2026-07-13",
@@ -4229,6 +4241,20 @@
}
},
"recentUpdates": [
+ {
+ "prototypeId": "oneos-web-h2-station-analysis",
+ "title": "oneos web h2 station analysis",
+ "version": "v1.7",
+ "date": "2026-07-20",
+ "time": "09:48",
+ "summary": "更新页面逻辑与交互",
+ "files": [
+ "index.tsx",
+ "pages/01-加氢站分析.jsx",
+ "shared/h2-analysis-charts.jsx",
+ "shared/h2-analysis-mock.js"
+ ]
+ },
{
"prototypeId": "oneos-web-approval-initiated",
"title": "我发起的",
@@ -4805,22 +4831,6 @@
"pages/04-站点周报统计.jsx",
"styles/h2-station-vm-filter.css"
]
- },
- {
- "prototypeId": "oneos-web-h2-station",
- "title": "加氢站管理",
- "version": "v1.33",
- "date": "2026-07-13",
- "time": "13:09",
- "summary": "更新页面样式",
- "files": [
- "index.tsx",
- "pages/01-加氢订单.jsx",
- "pages/02-加氢记录.jsx",
- "pages/03-站点信息.jsx",
- "pages/04-站点周报统计.jsx",
- "styles/h2-station-vm-filter.css"
- ]
}
]
}
diff --git a/src/prototypes/oneos-web-h2-station-analysis/index.tsx b/src/prototypes/oneos-web-h2-station-analysis/index.tsx
new file mode 100644
index 0000000..aa21da7
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-analysis/index.tsx
@@ -0,0 +1,69 @@
+/**
+ * @name 加氢站分析
+ * 数据分析 — 加氢站新增 / 存量 / 经营风险分析
+ */
+import '../../common/oneosWebLegacy/legacyGlobals';
+import React, { useEffect } from 'react';
+import * as antd from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import isoWeek from 'dayjs/plugin/isoWeek';
+import updateLocale from 'dayjs/plugin/updateLocale';
+import { ConfigProvider } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+import 'antd/dist/reset.css';
+import '../vehicle-management/style.css';
+import '../lease-contract-management/styles/lease-contract.css';
+import '../oneos-web-h2-station/styles/h2-station-vm-filter.css';
+import { clearHostPrototypeRouteInfo } from '../../common/useHashPage';
+import H2AnalysisPage from './pages/01-加氢站分析.jsx';
+
+dayjs.extend(isoWeek);
+dayjs.extend(updateLocale);
+dayjs.locale('zh-cn');
+dayjs.updateLocale('zh-cn', { weekStart: 1 });
+
+const vmTheme = {
+ token: {
+ colorPrimary: '#32a06e',
+ colorLink: '#32a06e',
+ colorLinkHover: '#3fb87c',
+ borderRadius: 8,
+ fontFamily:
+ 'Inter, -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif',
+ fontSize: 14,
+ colorText: '#18181b',
+ colorTextSecondary: '#52525b',
+ colorBorder: '#e5e7eb',
+ colorBgContainer: '#ffffff',
+ },
+ components: {
+ Table: {
+ headerBg: '#f6f7f7',
+ headerColor: '#71717a',
+ rowHoverBg: '#f6f7f7',
+ borderColor: '#e5e7eb',
+ cellPaddingBlock: 8,
+ cellPaddingInline: 12,
+ },
+ Card: {
+ borderRadiusLG: 12,
+ },
+ },
+};
+
+window.React = React;
+window.antd = antd;
+(window as Window & { dayjs: typeof dayjs }).dayjs = dayjs;
+
+export default function OneosWebH2StationAnalysis() {
+ useEffect(() => {
+ clearHostPrototypeRouteInfo();
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/prototypes/oneos-web-h2-station-analysis/pages/01-加氢站分析.jsx b/src/prototypes/oneos-web-h2-station-analysis/pages/01-加氢站分析.jsx
new file mode 100644
index 0000000..148f980
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-analysis/pages/01-加氢站分析.jsx
@@ -0,0 +1,821 @@
+// 【重要】必须使用 const Component 作为组件变量名
+// 数据分析 — 加氢站分析(新增 / 存量 / 经营风险)
+
+import { TablePagination, COMPACT_PAGE_SIZE_OPTIONS } from '../../../common/TablePagination';
+import { OperationActions } from '../../../common/OperationActions';
+import {
+ H2StationViewModal,
+ H2_STATION_VIEW_MODAL_STYLES,
+ h2WeeklyResolveStationViewRecord
+} from '../../oneos-web-h2-station/shared/h2-station-view-shared.jsx';
+import {
+ H2_ANALYSIS_REGION_OPTIONS,
+ H2_ANALYSIS_MOCK_STATIONS,
+ h2AnalysisBuildTrendPeriods,
+ h2AnalysisGetStockSummary,
+ h2AnalysisGetProvinceStock,
+ h2AnalysisFilterNewStations,
+ h2AnalysisFilterStockStations,
+ h2AnalysisFilterRiskStations,
+ h2AnalysisGetNewSummary,
+ h2AnalysisGetNewProvinceTop,
+ h2AnalysisGetRiskSummary,
+ h2AnalysisGetBalanceDistribution,
+ h2AnalysisGetPriceDistribution,
+ h2AnalysisGetContractBuckets,
+ h2AnalysisGetLowBalanceTop,
+ h2AnalysisGetStationType,
+ h2AnalysisGetBalanceStatus,
+ h2AnalysisGetContractDays,
+ h2AnalysisGetRiskTypes,
+ h2AnalysisFormatRegion,
+ h2AnalysisFormatYuan,
+ h2AnalysisStartOfWeek,
+ h2AnalysisEndOfWeek
+} from '../shared/h2-analysis-mock.js';
+import {
+ H2AnalysisDonutChart,
+ H2AnalysisHorizontalStackedBar,
+ H2AnalysisStackedBarTrend,
+ H2AnalysisLineTrend,
+ H2AnalysisSegmentBar,
+ H2AnalysisHorizontalBar,
+ H2AnalysisChartCard,
+ H2AnalysisLegendDot,
+ H2_ANALYSIS_CHART_COLORS
+} from '../shared/h2-analysis-charts.jsx';
+
+var ONEOS_ANT_TABLE_GLOBAL_FIX = [
+ '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }',
+ '.ant-table-container .ant-table-body { margin-top: 0 !important; }'
+];
+
+var H2_ANALYSIS_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([
+ '.vm-page.lc-page.h2-analysis-page { font-family: var(--vm-font); font-size: 0.875rem; line-height: 1.5; color: var(--ln-body); height: 100vh; overflow: auto; box-sizing: border-box; background: #f8fafc; }',
+ '.h2-analysis-page .h2-analysis-body { padding: 0 20px 24px; min-width: 0; }',
+ '.h2-analysis-page .h2-analysis-head { padding: 16px 20px 0; }',
+ '.h2-analysis-page .h2-analysis-head__title { margin: 0 0 4px; font-size: 1.125rem; font-weight: 700; color: #0f172a; }',
+ '.h2-analysis-page .h2-analysis-head__sub { margin: 0; font-size: 0.8125rem; color: #64748b; }',
+ '.h2-analysis-page .vm-filter-card { margin-bottom: 16px; }',
+ '.h2-analysis-page .vm-kpi-row { display: grid; gap: 12px; margin-bottom: 16px; }',
+ '.h2-analysis-page .vm-kpi-row--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }',
+ '.h2-analysis-page .vm-kpi-row--6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }',
+ '.h2-analysis-page .vm-kpi-row--5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }',
+ '@media (max-width: 1400px) { .h2-analysis-page .vm-kpi-row--6 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .h2-analysis-page .vm-kpi-row--5 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }',
+ '@media (max-width: 960px) { .h2-analysis-page .vm-kpi-row--4, .h2-analysis-page .vm-kpi-row--6, .h2-analysis-page .vm-kpi-row--5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }',
+ '.h2-analysis-page .vm-kpi-card { cursor: default; }',
+ '.h2-analysis-page .vm-kpi-card.is-clickable { cursor: pointer; }',
+ '.h2-analysis-page .vm-kpi-card.is-active { border-color: #86efac; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.15); }',
+ '.h2-analysis-page .h2-analysis-kpi-warn .vm-kpi-value { color: #d97706; }',
+ '.h2-analysis-page .h2-analysis-kpi-danger .vm-kpi-value { color: #dc2626; }',
+ '.h2-analysis-page .h2-analysis-chart-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }',
+ '.h2-analysis-page .h2-analysis-chart-grid--wide { grid-template-columns: 1fr; }',
+ '@media (max-width: 1100px) { .h2-analysis-page .h2-analysis-chart-grid { grid-template-columns: 1fr; } }',
+ '.h2-analysis-chart-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }',
+ '.h2-analysis-chart-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; flex-wrap: wrap; }',
+ '.h2-analysis-chart-head__title { font-size: 13px; font-weight: 700; color: #334155; }',
+ '.h2-analysis-chart-legend { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }',
+ '.h2-analysis-legend-inline { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: #64748b; }',
+ '.h2-analysis-chart-body { padding: 12px 14px 14px; min-height: 240px; }',
+ '.h2-analysis-chart-body.is-compact { min-height: 200px; }',
+ '.h2-analysis-chart-svg { display: block; width: 100%; height: auto; }',
+ '.h2-analysis-axis-label { font-size: 10px; fill: #94a3b8; }',
+ '.h2-analysis-axis-tick { font-size: 10px; fill: #94a3b8; font-variant-numeric: tabular-nums; }',
+ '.h2-analysis-bar-label { font-size: 11px; fill: #475569; font-weight: 500; }',
+ '.h2-analysis-bar-label-sm { font-size: 10px; fill: #475569; }',
+ '.h2-analysis-bar-val { font-size: 10px; fill: #64748b; font-variant-numeric: tabular-nums; }',
+ '.h2-analysis-donut-wrap { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; justify-content: center; }',
+ '.h2-analysis-donut-svg { width: min(200px, 42%); flex-shrink: 0; }',
+ '.h2-analysis-donut-center-val { font-size: 22px; font-weight: 700; fill: #0f172a; }',
+ '.h2-analysis-donut-center-label { font-size: 11px; fill: #94a3b8; }',
+ '.h2-analysis-donut-legend { display: flex; flex-direction: column; gap: 8px; min-width: 140px; }',
+ '.h2-analysis-legend-item { display: flex; align-items: center; gap: 8px; border: none; background: none; padding: 4px 0; cursor: pointer; text-align: left; opacity: 0.55; transition: opacity 0.2s; }',
+ '.h2-analysis-legend-item.is-active, .h2-analysis-legend-item:hover { opacity: 1; }',
+ '.h2-analysis-legend-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }',
+ '.h2-analysis-legend-label { font-size: 12px; color: #475569; flex: 1; }',
+ '.h2-analysis-legend-val { font-size: 12px; color: #0f172a; font-weight: 600; font-variant-numeric: tabular-nums; }',
+ '.h2-analysis-page .h2-analysis-filter-hint { margin: -8px 0 12px; padding: 8px 12px; border-radius: 8px; background: #ecfdf5; border: 1px solid #bbf7d0; font-size: 12px; color: #047857; display: flex; align-items: center; justify-content: space-between; gap: 8px; }',
+ '.h2-analysis-page .h2-analysis-filter-hint button { border: none; background: none; color: #059669; font-weight: 600; cursor: pointer; font-size: 12px; }',
+ '.h2-analysis-page .h2-analysis-main-tabs .ant-tabs-nav { margin-bottom: 16px !important; padding: 0 20px; background: #fff; border-bottom: 1px solid #e2e8f0; }',
+ '.h2-analysis-page .h2-analysis-tab-panel { padding: 0 20px 8px; }',
+ '.h2-analysis-page .h2-analysis-main-tabs .ant-tabs-tab { font-size: 14px !important; font-weight: 600 !important; padding: 14px 4px !important; }',
+ '.h2-analysis-page .h2-analysis-type-tag.ant-tag { margin: 0; border-radius: 6px; font-size: 0.75rem; font-weight: 500; }',
+ '.h2-analysis-page .h2-analysis-risk-tag.ant-tag { margin: 2px 4px 2px 0; border-radius: 6px; font-size: 0.6875rem; }',
+ '.h2-analysis-page .vm-table-section { min-width: 0; }'
+].concat(H2_STATION_VIEW_MODAL_STYLES));
+
+function h2AnalysisEnsureDayjs() {
+ return window.dayjs || null;
+}
+
+function h2AnalysisFormatWeekRange(weekValue) {
+ var start = h2AnalysisStartOfWeek(weekValue);
+ var end = h2AnalysisEndOfWeek(weekValue);
+ if (!start || !end) return '';
+ return start.format('YYYY-MM-DD') + ' ~ ' + end.format('YYYY-MM-DD');
+}
+
+function h2AnalysisRenderKpiCard(meta, value, extra, opts) {
+ var options = opts || {};
+ var cls = 'vm-kpi-card' + (options.clickable ? ' is-clickable' : '') + (options.active ? ' is-active' : '') + (options.warn ? ' h2-analysis-kpi-warn' : '') + (options.danger ? ' h2-analysis-kpi-danger' : '');
+ return React.createElement('div', {
+ className: cls,
+ key: meta.key,
+ onClick: options.onClick,
+ role: options.clickable ? 'button' : undefined
+ },
+ React.createElement('div', { className: 'vm-kpi-title' }, meta.title),
+ React.createElement('div', { className: 'vm-kpi-value' }, value),
+ extra ? React.createElement('div', { className: 'vm-kpi-desc' }, extra) : null
+ );
+}
+
+function h2AnalysisToggleChartFilter(current, next) {
+ if (!next) return null;
+ if (current && current.type === next.type && current.value === next.value) return null;
+ return next;
+}
+
+function h2AnalysisRenderFilterHint(chartFilter, onClear) {
+ if (!chartFilter) return null;
+ var label = chartFilter.label || chartFilter.value || '';
+ return React.createElement('div', { className: 'h2-analysis-filter-hint' },
+ React.createElement('span', null, '图表筛选:', React.createElement('strong', null, label)),
+ React.createElement('button', { type: 'button', onClick: onClear }, '清除')
+ );
+}
+
+function h2AnalysisRenderCommonFilters(state, setState) {
+ var Select = window.antd && window.antd.Select;
+ var Cascader = window.antd && window.antd.Cascader;
+ var DatePicker = window.antd && window.antd.DatePicker;
+ if (!Select || !Cascader) return null;
+ var dayjs = h2AnalysisEnsureDayjs();
+ return React.createElement('div', { className: 'vm-filter-card lc-filter-card' },
+ React.createElement('div', { className: 'vm-filter-grid lc-filter-grid' },
+ state.showWeek ? React.createElement('div', { className: 'vm-filter-field' },
+ React.createElement('label', { className: 'vm-filter-label' }, '统计周期'),
+ DatePicker && dayjs ? React.createElement(DatePicker, {
+ picker: 'week',
+ value: state.weekValue,
+ onChange: function (v) { setState(Object.assign({}, state, { weekValue: v, chartFilter: null, page: 1 })); },
+ style: { width: '100%' },
+ allowClear: false
+ }) : null
+ ) : null,
+ React.createElement('div', { className: 'vm-filter-field' },
+ React.createElement('label', { className: 'vm-filter-label' }, '省市'),
+ React.createElement(Cascader, {
+ options: H2_ANALYSIS_REGION_OPTIONS,
+ value: state.region && state.region.length ? state.region : undefined,
+ onChange: function (v) { setState(Object.assign({}, state, { region: v || [], chartFilter: null, page: 1 })); },
+ placeholder: '全部',
+ changeOnSelect: true,
+ allowClear: true,
+ style: { width: '100%' }
+ })
+ ),
+ React.createElement('div', { className: 'vm-filter-field' },
+ React.createElement('label', { className: 'vm-filter-label' }, '站点类型'),
+ React.createElement(Select, {
+ value: state.stationType || undefined,
+ onChange: function (v) { setState(Object.assign({}, state, { stationType: v || '', chartFilter: null, page: 1 })); },
+ placeholder: '全部',
+ allowClear: true,
+ style: { width: '100%' },
+ options: [{ value: '签约', label: '签约' }, { value: '普通', label: '普通' }]
+ })
+ ),
+ React.createElement('div', { className: 'vm-filter-field' },
+ React.createElement('label', { className: 'vm-filter-label' }, '营业状态'),
+ React.createElement(Select, {
+ value: state.businessStatus || undefined,
+ onChange: function (v) { setState(Object.assign({}, state, { businessStatus: v || '', chartFilter: null, page: 1 })); },
+ placeholder: '全部',
+ allowClear: true,
+ style: { width: '100%' },
+ options: [
+ { value: '营业中', label: '营业中' },
+ { value: '暂停营业', label: '暂停营业' },
+ { value: '停止营业', label: '停止营业' }
+ ]
+ })
+ )
+ )
+ );
+}
+
+function H2AnalysisNewTab(props) {
+ var stations = props.stations;
+ var viewStation = props.viewStation;
+ var state = props.state;
+ var setState = props.setState;
+ var Tag = window.antd && window.antd.Tag;
+ var Table = window.antd && window.antd.Table;
+ var summary = h2AnalysisGetNewSummary(stations, state.weekValue);
+ var provinceTop = h2AnalysisGetNewProvinceTop(stations, state.weekValue, 10);
+ var periods = h2AnalysisBuildTrendPeriods();
+ var filtered = h2AnalysisFilterNewStations(stations, {
+ weekValue: state.weekValue,
+ region: state.region,
+ stationType: state.stationType,
+ businessStatus: state.businessStatus,
+ chartFilter: state.chartFilter
+ });
+ var typeSegments = [
+ { key: '签约', label: '签约', value: summary.newSigned, color: H2_ANALYSIS_CHART_COLORS.signed },
+ { key: '普通', label: '普通', value: summary.newNormal, color: H2_ANALYSIS_CHART_COLORS.normal }
+ ];
+ var barItems = provinceTop.map(function (p) {
+ return { province: p.province, signed: p.signed, normal: p.normal };
+ });
+ var pageData = filtered.slice((state.page - 1) * state.pageSize, state.page * state.pageSize);
+
+ var columns = [
+ { title: '省市', dataIndex: 'region', width: 140, render: function (_v, r) { return h2AnalysisFormatRegion(r); } },
+ { title: '站点名称', dataIndex: 'name', ellipsis: true },
+ {
+ title: '站点类型', dataIndex: 'stationType', width: 88,
+ render: function (_v, r) {
+ var t = h2AnalysisGetStationType(r);
+ return Tag ? React.createElement(Tag, {
+ className: 'h2-analysis-type-tag',
+ color: t === '签约' ? 'green' : 'gold'
+ }, t) : t;
+ }
+ },
+ { title: '创建时间', dataIndex: 'createdAt', width: 168 },
+ {
+ title: '营业状态', dataIndex: 'businessStatus', width: 96,
+ render: function (v) {
+ var color = v === '营业中' ? 'success' : v === '暂停营业' ? 'warning' : 'default';
+ return Tag ? React.createElement(Tag, { color: color }, v) : v;
+ }
+ },
+ {
+ title: '操作', key: 'action', width: 88, fixed: 'right',
+ render: function (_v, r) {
+ return React.createElement(OperationActions, {
+ actions: [{ key: 'view', label: '查看', onClick: function () { viewStation(r); } }]
+ });
+ }
+ }
+ ];
+
+ return React.createElement('div', { className: 'h2-analysis-tab-panel' },
+ h2AnalysisRenderCommonFilters(Object.assign({}, state, { showWeek: true }), setState),
+ h2AnalysisRenderFilterHint(state.chartFilter, function () { setState(Object.assign({}, state, { chartFilter: null, page: 1 })); }),
+ React.createElement('div', { className: 'vm-kpi-row vm-kpi-row--4' },
+ h2AnalysisRenderKpiCard({ key: 'newTotal', title: '本期新增站点数' }, summary.newTotal, h2AnalysisFormatWeekRange(state.weekValue)),
+ h2AnalysisRenderKpiCard({ key: 'newSigned', title: '新增签约站点数' }, summary.newSigned),
+ h2AnalysisRenderKpiCard({ key: 'newNormal', title: '新增普通站点数' }, summary.newNormal),
+ h2AnalysisRenderKpiCard({ key: 'newProv', title: '新增覆盖省份数' }, summary.newProvinceCount)
+ ),
+ React.createElement('div', { className: 'h2-analysis-chart-grid' },
+ React.createElement(H2AnalysisChartCard, {
+ title: '新增站点类型占比',
+ compact: true
+ },
+ React.createElement(H2AnalysisDonutChart, {
+ segments: typeSegments,
+ centerLabel: '新增',
+ centerValue: summary.newTotal,
+ activeKey: state.chartFilter && state.chartFilter.type === 'stationType' ? state.chartFilter.value : null,
+ onSegmentClick: function (seg) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, {
+ type: 'stationType', value: seg.key, label: seg.label
+ }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, {
+ title: '新增站点省份 Top10',
+ legend: React.createElement(React.Fragment, null,
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.signed, '签约'),
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.normal, '普通')
+ )
+ },
+ barItems.length ? React.createElement(H2AnalysisHorizontalStackedBar, {
+ items: barItems,
+ keys: [
+ { key: 'signed', color: H2_ANALYSIS_CHART_COLORS.signed, label: '签约' },
+ { key: 'normal', color: H2_ANALYSIS_CHART_COLORS.normal, label: '普通' }
+ ],
+ activeFilter: state.chartFilter && state.chartFilter.type === 'province' ? { province: state.chartFilter.value } : null,
+ onBarClick: function (payload) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, {
+ type: 'province', value: payload.province, label: payload.province
+ }),
+ page: 1
+ }));
+ }
+ }) : React.createElement('div', { style: { padding: 40, textAlign: 'center', color: '#94a3b8' } }, '本期暂无新增')
+ )
+ ),
+ React.createElement('div', { className: 'h2-analysis-chart-grid h2-analysis-chart-grid--wide' },
+ React.createElement(H2AnalysisChartCard, {
+ title: '近12期新增站点趋势',
+ legend: React.createElement(React.Fragment, null,
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.signed, '新增签约'),
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.normal, '新增普通')
+ )
+ },
+ React.createElement(H2AnalysisStackedBarTrend, {
+ periods: periods,
+ activePeriod: state.chartFilter && state.chartFilter.type === 'period' ? state.chartFilter.periodIndex : null,
+ onBarClick: function (idx, p) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, {
+ type: 'period', periodIndex: idx, label: '第' + p.period + '期 (' + p.label + ')'
+ }),
+ page: 1
+ }));
+ }
+ })
+ )
+ ),
+ React.createElement('div', { className: 'vm-table-section' },
+ React.createElement('div', { className: 'vm-table-toolbar' },
+ React.createElement('span', { className: 'vm-table-toolbar__title' }, '新增站点明细'),
+ React.createElement('span', { className: 'vm-table-toolbar__meta' }, '共 ' + filtered.length + ' 条')
+ ),
+ Table ? React.createElement(Table, {
+ rowKey: 'id',
+ size: 'small',
+ columns: columns,
+ dataSource: pageData,
+ pagination: false,
+ scroll: { x: 900 }
+ }) : null,
+ React.createElement(TablePagination, {
+ current: state.page,
+ pageSize: state.pageSize,
+ total: filtered.length,
+ pageSizeOptions: COMPACT_PAGE_SIZE_OPTIONS,
+ onChange: function (p, ps) { setState(Object.assign({}, state, { page: p, pageSize: ps })); }
+ })
+ )
+ );
+}
+
+function H2AnalysisStockTab(props) {
+ var stations = props.stations;
+ var viewStation = props.viewStation;
+ var state = props.state;
+ var setState = props.setState;
+ var Tag = window.antd && window.antd.Tag;
+ var Table = window.antd && window.antd.Table;
+ var stock = h2AnalysisGetStockSummary();
+ var provinceStock = h2AnalysisGetProvinceStock().slice(0, 10);
+ var periods = h2AnalysisBuildTrendPeriods();
+ var filtered = h2AnalysisFilterStockStations(stations, {
+ region: state.region,
+ stationType: state.stationType,
+ businessStatus: state.businessStatus,
+ chartFilter: state.chartFilter
+ });
+ var typeSegments = [
+ { key: '签约', label: '签约', value: stock.signedStations, color: H2_ANALYSIS_CHART_COLORS.signed },
+ { key: '普通', label: '普通', value: stock.normalStations, color: H2_ANALYSIS_CHART_COLORS.normal }
+ ];
+ var statusSegments = [
+ { key: '营业中', label: '营业中', value: stock.operatingStations, color: '#10b981' },
+ { key: '暂停营业', label: '暂停', value: Math.round(stock.pausedStoppedStations * 0.45), color: '#f59e0b' },
+ { key: '停止营业', label: '停止', value: Math.round(stock.pausedStoppedStations * 0.55), color: '#94a3b8' }
+ ];
+ var barItems = provinceStock.map(function (p) {
+ return { province: p.province, signed: p.signed, normal: p.normal };
+ });
+ var pageData = filtered.slice((state.page - 1) * state.pageSize, state.page * state.pageSize);
+
+ var columns = [
+ { title: '省市', dataIndex: 'region', width: 140, render: function (_v, r) { return h2AnalysisFormatRegion(r); } },
+ { title: '站点名称', dataIndex: 'name', ellipsis: true },
+ {
+ title: '站点类型', dataIndex: 'stationType', width: 88,
+ render: function (_v, r) {
+ var t = h2AnalysisGetStationType(r);
+ return Tag ? React.createElement(Tag, { className: 'h2-analysis-type-tag', color: t === '签约' ? 'green' : 'gold' }, t) : t;
+ }
+ },
+ { title: '创建时间', dataIndex: 'createdAt', width: 168 },
+ {
+ title: '营业状态', dataIndex: 'businessStatus', width: 96,
+ render: function (v) {
+ var color = v === '营业中' ? 'success' : v === '暂停营业' ? 'warning' : 'default';
+ return Tag ? React.createElement(Tag, { color: color }, v) : v;
+ }
+ },
+ {
+ title: '操作', key: 'action', width: 88, fixed: 'right',
+ render: function (_v, r) {
+ return React.createElement(OperationActions, {
+ actions: [{ key: 'view', label: '查看', onClick: function () { viewStation(r); } }]
+ });
+ }
+ }
+ ];
+
+ return React.createElement('div', { className: 'h2-analysis-tab-panel' },
+ h2AnalysisRenderCommonFilters(state, setState),
+ h2AnalysisRenderFilterHint(state.chartFilter, function () { setState(Object.assign({}, state, { chartFilter: null, page: 1 })); }),
+ React.createElement('div', { className: 'vm-kpi-row vm-kpi-row--6' },
+ h2AnalysisRenderKpiCard({ key: 'total', title: '全国累计站点数' }, stock.totalStations),
+ h2AnalysisRenderKpiCard({ key: 'signed', title: '签约站点数' }, stock.signedStations),
+ h2AnalysisRenderKpiCard({ key: 'normal', title: '普通站点数' }, stock.normalStations),
+ h2AnalysisRenderKpiCard({ key: 'operating', title: '营业中站点数' }, stock.operatingStations),
+ h2AnalysisRenderKpiCard({ key: 'paused', title: '暂停/停止营业' }, stock.pausedStoppedStations, null, { warn: true }),
+ h2AnalysisRenderKpiCard({ key: 'prov', title: '覆盖省份数' }, stock.provinceCount)
+ ),
+ React.createElement('div', { className: 'h2-analysis-chart-grid' },
+ React.createElement(H2AnalysisChartCard, { title: '存量站点类型占比', compact: true },
+ React.createElement(H2AnalysisDonutChart, {
+ segments: typeSegments,
+ centerLabel: '累计',
+ centerValue: stock.totalStations,
+ activeKey: state.chartFilter && state.chartFilter.type === 'stationType' ? state.chartFilter.value : null,
+ onSegmentClick: function (seg) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'stationType', value: seg.key, label: seg.label }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, { title: '营业状态占比', compact: true },
+ React.createElement(H2AnalysisDonutChart, {
+ segments: statusSegments,
+ centerLabel: '站点',
+ centerValue: stock.totalStations,
+ activeKey: state.chartFilter && state.chartFilter.type === 'businessStatus' ? state.chartFilter.value : null,
+ onSegmentClick: function (seg) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'businessStatus', value: seg.key, label: seg.label }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, {
+ title: '各省存量站点分布',
+ legend: React.createElement(React.Fragment, null,
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.signed, '签约'),
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.normal, '普通')
+ )
+ },
+ React.createElement(H2AnalysisHorizontalStackedBar, {
+ items: barItems,
+ keys: [
+ { key: 'signed', color: H2_ANALYSIS_CHART_COLORS.signed, label: '签约' },
+ { key: 'normal', color: H2_ANALYSIS_CHART_COLORS.normal, label: '普通' }
+ ],
+ activeFilter: state.chartFilter && state.chartFilter.type === 'province' ? { province: state.chartFilter.value } : null,
+ onBarClick: function (payload) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'province', value: payload.province, label: payload.province }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, {
+ title: '近12期累计站点变化',
+ legend: React.createElement(React.Fragment, null,
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.total, '全部累计'),
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.signed, '签约累计'),
+ H2AnalysisLegendDot(H2_ANALYSIS_CHART_COLORS.normal, '普通累计')
+ )
+ },
+ React.createElement(H2AnalysisLineTrend, { periods: periods })
+ )
+ ),
+ React.createElement('div', { className: 'vm-table-section' },
+ React.createElement('div', { className: 'vm-table-toolbar' },
+ React.createElement('span', { className: 'vm-table-toolbar__title' }, '存量站点样本明细'),
+ React.createElement('span', { className: 'vm-table-toolbar__meta' }, '共 ' + filtered.length + ' 条(样本数据)')
+ ),
+ Table ? React.createElement(Table, {
+ rowKey: 'id',
+ size: 'small',
+ columns: columns,
+ dataSource: pageData,
+ pagination: false,
+ scroll: { x: 900 }
+ }) : null,
+ React.createElement(TablePagination, {
+ current: state.page,
+ pageSize: state.pageSize,
+ total: filtered.length,
+ pageSizeOptions: COMPACT_PAGE_SIZE_OPTIONS,
+ onChange: function (p, ps) { setState(Object.assign({}, state, { page: p, pageSize: ps })); }
+ })
+ )
+ );
+}
+
+function H2AnalysisRiskTab(props) {
+ var stations = props.stations;
+ var viewStation = props.viewStation;
+ var state = props.state;
+ var setState = props.setState;
+ var Tag = window.antd && window.antd.Tag;
+ var Table = window.antd && window.antd.Table;
+ var riskSummary = h2AnalysisGetRiskSummary(stations);
+ var balanceSegs = h2AnalysisGetBalanceDistribution(stations);
+ var priceSegs = h2AnalysisGetPriceDistribution(stations);
+ var contractSegs = h2AnalysisGetContractBuckets(stations);
+ var lowBalance = h2AnalysisGetLowBalanceTop(stations, 10);
+ var filtered = h2AnalysisFilterRiskStations(stations, {
+ region: state.region,
+ stationType: state.stationType,
+ businessStatus: state.businessStatus,
+ chartFilter: state.chartFilter
+ });
+ var pageData = filtered.slice((state.page - 1) * state.pageSize, state.page * state.pageSize);
+
+ var columns = [
+ { title: '站点名称', dataIndex: 'name', ellipsis: true, width: 180 },
+ { title: '省市', dataIndex: 'region', width: 130, render: function (_v, r) { return h2AnalysisFormatRegion(r); } },
+ {
+ title: '风险类型', key: 'risks', width: 180,
+ render: function (_v, r) {
+ var risks = h2AnalysisGetRiskTypes(r);
+ return risks.map(function (risk) {
+ var color = risk.indexOf('负') >= 0 || risk.indexOf('到期') >= 0 ? 'error' : risk.indexOf('预警') >= 0 ? 'warning' : 'default';
+ return Tag ? React.createElement(Tag, { key: risk, className: 'h2-analysis-risk-tag', color: color }, risk) : risk;
+ });
+ }
+ },
+ {
+ title: '当前余额', dataIndex: 'prepaidBalance', width: 120,
+ render: function (v, r) {
+ var st = h2AnalysisGetBalanceStatus(r);
+ var color = st === 'negative' ? '#dc2626' : st === 'alert' ? '#d97706' : '#0f172a';
+ return React.createElement('span', { style: { color: color, fontWeight: 600 } }, h2AnalysisFormatYuan(v));
+ }
+ },
+ {
+ title: '合同剩余天数', key: 'contractDays', width: 110,
+ render: function (_v, r) {
+ var days = h2AnalysisGetContractDays(r);
+ if (days == null) return '—';
+ if (days < 0) return React.createElement('span', { style: { color: '#dc2626' } }, '已到期');
+ if (days <= 30) return React.createElement('span', { style: { color: '#d97706' } }, days + ' 天');
+ return days + ' 天';
+ }
+ },
+ {
+ title: '营业状态', dataIndex: 'businessStatus', width: 96,
+ render: function (v) {
+ var color = v === '营业中' ? 'success' : v === '暂停营业' ? 'warning' : 'default';
+ return Tag ? React.createElement(Tag, { color: color }, v) : v;
+ }
+ },
+ { title: '负责人', dataIndex: 'contact', width: 88 },
+ {
+ title: '操作', key: 'action', width: 88, fixed: 'right',
+ render: function (_v, r) {
+ return React.createElement(OperationActions, {
+ actions: [{ key: 'view', label: '查看', onClick: function () { viewStation(r); } }]
+ });
+ }
+ }
+ ];
+
+ return React.createElement('div', { className: 'h2-analysis-tab-panel' },
+ h2AnalysisRenderCommonFilters(state, setState),
+ h2AnalysisRenderFilterHint(state.chartFilter, function () { setState(Object.assign({}, state, { chartFilter: null, page: 1 })); }),
+ React.createElement('div', { className: 'vm-kpi-row vm-kpi-row--5' },
+ h2AnalysisRenderKpiCard({ key: 'alert', title: '余额预警站点数' }, riskSummary.balanceAlert, null, {
+ warn: true, clickable: true, active: state.chartFilter && state.chartFilter.type === 'riskType' && state.chartFilter.value === '余额预警',
+ onClick: function () {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'riskType', value: '余额预警', label: '余额预警' }),
+ page: 1
+ }));
+ }
+ }),
+ h2AnalysisRenderKpiCard({ key: 'neg', title: '负余额站点数' }, riskSummary.negative, null, {
+ danger: true, clickable: true, active: state.chartFilter && state.chartFilter.type === 'riskType' && state.chartFilter.value === '负余额',
+ onClick: function () {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'riskType', value: '负余额', label: '负余额' }),
+ page: 1
+ }));
+ }
+ }),
+ h2AnalysisRenderKpiCard({ key: 'exp30', title: '30天内到期站点数' }, riskSummary.expiring30, null, {
+ warn: true, clickable: true, active: state.chartFilter && state.chartFilter.type === 'riskType' && state.chartFilter.value === '合同即将到期',
+ onClick: function () {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'riskType', value: '合同即将到期', label: '30天内到期' }),
+ page: 1
+ }));
+ }
+ }),
+ h2AnalysisRenderKpiCard({ key: 'expired', title: '已到期站点数' }, riskSummary.expired, null, {
+ danger: true, clickable: true, active: state.chartFilter && state.chartFilter.type === 'riskType' && state.chartFilter.value === '合同已到期',
+ onClick: function () {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'riskType', value: '合同已到期', label: '已到期' }),
+ page: 1
+ }));
+ }
+ }),
+ h2AnalysisRenderKpiCard({ key: 'noPrice', title: '未配置价格站点数' }, riskSummary.noPrice, null, {
+ clickable: true, active: state.chartFilter && state.chartFilter.type === 'riskType' && state.chartFilter.value === '未配置价格',
+ onClick: function () {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'riskType', value: '未配置价格', label: '未配置价格' }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement('div', { className: 'h2-analysis-chart-grid' },
+ React.createElement(H2AnalysisChartCard, { title: '预付余额状态分布', compact: true },
+ React.createElement(H2AnalysisDonutChart, {
+ segments: balanceSegs,
+ centerLabel: '站点',
+ activeKey: state.chartFilter && state.chartFilter.type === 'balanceStatus' ? state.chartFilter.value : null,
+ onSegmentClick: function (seg) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'balanceStatus', value: seg.key, label: seg.label }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, { title: '价格配置类型占比', compact: true },
+ React.createElement(H2AnalysisDonutChart, {
+ segments: priceSegs,
+ centerLabel: '配置',
+ activeKey: state.chartFilter && state.chartFilter.type === 'priceType' ? state.chartFilter.value : null,
+ onSegmentClick: function (seg) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'priceType', value: seg.key, label: seg.label }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, { title: '余额最低 Top10 站点' },
+ React.createElement(H2AnalysisHorizontalBar, {
+ items: lowBalance,
+ activeId: state.chartFilter && state.chartFilter.type === 'station' ? state.chartFilter.value : null,
+ onBarClick: function (it) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'station', value: it.id, label: it.name }),
+ page: 1
+ }));
+ }
+ })
+ ),
+ React.createElement(H2AnalysisChartCard, { title: '合同剩余天数分布' },
+ React.createElement(H2AnalysisSegmentBar, {
+ segments: contractSegs,
+ activeKey: state.chartFilter && state.chartFilter.type === 'contractBucket' ? state.chartFilter.value : null,
+ onSegmentClick: function (seg) {
+ setState(Object.assign({}, state, {
+ chartFilter: h2AnalysisToggleChartFilter(state.chartFilter, { type: 'contractBucket', value: seg.key, label: seg.label }),
+ page: 1
+ }));
+ }
+ })
+ )
+ ),
+ React.createElement('div', { className: 'vm-table-section' },
+ React.createElement('div', { className: 'vm-table-toolbar' },
+ React.createElement('span', { className: 'vm-table-toolbar__title' }, '风险明细'),
+ React.createElement('span', { className: 'vm-table-toolbar__meta' }, '共 ' + filtered.length + ' 条')
+ ),
+ Table ? React.createElement(Table, {
+ rowKey: 'id',
+ size: 'small',
+ columns: columns,
+ dataSource: pageData,
+ pagination: false,
+ scroll: { x: 1100 }
+ }) : null,
+ React.createElement(TablePagination, {
+ current: state.page,
+ pageSize: state.pageSize,
+ total: filtered.length,
+ pageSizeOptions: COMPACT_PAGE_SIZE_OPTIONS,
+ onChange: function (p, ps) { setState(Object.assign({}, state, { page: p, pageSize: ps })); }
+ })
+ )
+ );
+}
+
+const Component = function () {
+ var Tabs = window.antd && window.antd.Tabs;
+ var dayjs = h2AnalysisEnsureDayjs();
+ var defaultWeek = dayjs ? h2AnalysisStartOfWeek(dayjs()) : null;
+
+ var activeTabState = React.useState('new');
+ var activeTab = activeTabState[0];
+ var setActiveTab = activeTabState[1];
+
+ var newTabState = React.useState({
+ weekValue: defaultWeek,
+ region: [],
+ stationType: '',
+ businessStatus: '',
+ chartFilter: null,
+ page: 1,
+ pageSize: 10
+ });
+ var stockTabState = React.useState({
+ region: [],
+ stationType: '',
+ businessStatus: '',
+ chartFilter: null,
+ page: 1,
+ pageSize: 10
+ });
+ var riskTabState = React.useState({
+ region: [],
+ stationType: '',
+ businessStatus: '',
+ chartFilter: null,
+ page: 1,
+ pageSize: 10
+ });
+
+ var viewModalState = React.useState({ open: false, record: null });
+ var viewModal = viewModalState[0];
+ var setViewModal = viewModalState[1];
+
+ var stations = H2_ANALYSIS_MOCK_STATIONS;
+
+ function openView(record) {
+ var weeklyRow = {
+ stationName: record.name,
+ province: record.region[0],
+ city: record.region[1],
+ stationType: h2AnalysisGetStationType(record),
+ entryAt: record.createdAt
+ };
+ var viewRecord = h2WeeklyResolveStationViewRecord(weeklyRow);
+ setViewModal({ open: true, record: viewRecord });
+ }
+
+ var tabItems = [
+ {
+ key: 'new',
+ label: '新增站点分析',
+ children: React.createElement(H2AnalysisNewTab, {
+ stations: stations,
+ viewStation: openView,
+ state: newTabState[0],
+ setState: newTabState[1]
+ })
+ },
+ {
+ key: 'stock',
+ label: '存量站点总览',
+ children: React.createElement(H2AnalysisStockTab, {
+ stations: stations,
+ viewStation: openView,
+ state: stockTabState[0],
+ setState: stockTabState[1]
+ })
+ },
+ {
+ key: 'risk',
+ label: '经营风险监控',
+ children: React.createElement(H2AnalysisRiskTab, {
+ stations: stations,
+ viewStation: openView,
+ state: riskTabState[0],
+ setState: riskTabState[1]
+ })
+ }
+ ];
+
+ return React.createElement('div', { className: 'vm-page lc-page h2-analysis-page' },
+ React.createElement('style', null, H2_ANALYSIS_PAGE_STYLE),
+ React.createElement('div', { className: 'h2-analysis-head' },
+ React.createElement('h1', { className: 'h2-analysis-head__title' }, '加氢站分析'),
+ React.createElement('p', { className: 'h2-analysis-head__sub' }, '新增分布 · 存量统计 · 经营风险')
+ ),
+ Tabs ? React.createElement(Tabs, {
+ className: 'h2-analysis-main-tabs',
+ activeKey: activeTab,
+ onChange: setActiveTab,
+ items: tabItems
+ }) : tabItems[0].children,
+ React.createElement(H2StationViewModal, {
+ open: viewModal.open,
+ record: viewModal.record,
+ onClose: function () { setViewModal({ open: false, record: null }); }
+ })
+ );
+};
+
+export default Component;
diff --git a/src/prototypes/oneos-web-h2-station-analysis/shared/h2-analysis-charts.jsx b/src/prototypes/oneos-web-h2-station-analysis/shared/h2-analysis-charts.jsx
new file mode 100644
index 0000000..20aa7c0
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-analysis/shared/h2-analysis-charts.jsx
@@ -0,0 +1,396 @@
+/**
+ * 加氢站分析 — SVG 图表组件(环形图 / 横向堆叠条形 / 堆叠柱状 / 折线)
+ */
+
+import React from 'react';
+
+var COLORS = {
+ signed: '#10b981',
+ normal: '#f59e0b',
+ total: '#6366f1',
+ operating: '#10b981',
+ paused: '#f59e0b',
+ stopped: '#94a3b8'
+};
+
+function h2ChartDonutSeg(cx, cy, r0, r1, a0, a1) {
+ if (a1 - a0 < 0.04) return '';
+ var rad = function (deg) { return (deg - 90) * Math.PI / 180; };
+ var x1 = cx + r1 * Math.cos(rad(a0));
+ var y1 = cy + r1 * Math.sin(rad(a0));
+ var x2 = cx + r1 * Math.cos(rad(a1));
+ var y2 = cy + r1 * Math.sin(rad(a1));
+ var x3 = cx + r0 * Math.cos(rad(a1));
+ var y3 = cy + r0 * Math.sin(rad(a1));
+ var x4 = cx + r0 * Math.cos(rad(a0));
+ var y4 = cy + r0 * Math.sin(rad(a0));
+ var large = (a1 - a0) > 180 ? 1 : 0;
+ return 'M' + x1 + ',' + y1 + ' A' + r1 + ',' + r1 + ' 0 ' + large + ' 1 ' + x2 + ',' + y2 +
+ ' L' + x3 + ',' + y3 + ' A' + r0 + ',' + r0 + ' 0 ' + large + ' 0 ' + x4 + ',' + y4 + ' Z';
+}
+
+export function H2AnalysisDonutChart(props) {
+ var segments = props.segments || [];
+ var onSegmentClick = props.onSegmentClick;
+ var activeKey = props.activeKey;
+ var centerLabel = props.centerLabel || '';
+ var centerValue = props.centerValue;
+ var total = segments.reduce(function (sum, s) { return sum + (s.value || 0); }, 0) || 1;
+ var cx = 100;
+ var cy = 100;
+ var r0 = 58;
+ var r1 = 88;
+ var angle = 0;
+ var paths = [];
+
+ segments.forEach(function (seg) {
+ var slice = total > 0 ? (seg.value / total) * 360 : 0;
+ if (slice < 0.05) { angle += slice; return; }
+ var a0 = angle;
+ var a1 = angle + slice;
+ angle = a1;
+ var d = slice >= 359.95 ? h2ChartDonutSeg(cx, cy, r0, r1, 0, 360) : h2ChartDonutSeg(cx, cy, r0, r1, a0, a1);
+ if (!d) return;
+ var isActive = activeKey == null || activeKey === seg.key;
+ paths.push(React.createElement('path', {
+ key: seg.key,
+ d: d,
+ fill: seg.color,
+ stroke: '#fff',
+ strokeWidth: 2,
+ opacity: isActive ? 1 : 0.35,
+ style: { cursor: onSegmentClick ? 'pointer' : 'default', transition: 'opacity 0.2s' },
+ onClick: onSegmentClick ? function () { onSegmentClick(seg); } : undefined,
+ role: onSegmentClick ? 'button' : undefined,
+ 'aria-label': seg.label + ' ' + seg.value
+ }));
+ });
+
+ return React.createElement('div', { className: 'h2-analysis-donut-wrap' },
+ React.createElement('svg', {
+ className: 'h2-analysis-chart-svg h2-analysis-donut-svg',
+ viewBox: '0 0 200 200',
+ role: 'img',
+ 'aria-label': props.title || '环形图'
+ },
+ paths,
+ React.createElement('text', {
+ x: cx, y: cy - 6, textAnchor: 'middle', className: 'h2-analysis-donut-center-val'
+ }, centerValue != null ? centerValue : total),
+ React.createElement('text', {
+ x: cx, y: cy + 14, textAnchor: 'middle', className: 'h2-analysis-donut-center-label'
+ }, centerLabel)
+ ),
+ React.createElement('div', { className: 'h2-analysis-donut-legend' },
+ segments.map(function (seg) {
+ var pct = total > 0 ? ((seg.value / total) * 100).toFixed(1) : '0';
+ var isActive = activeKey == null || activeKey === seg.key;
+ return React.createElement('button', {
+ key: seg.key,
+ type: 'button',
+ className: 'h2-analysis-legend-item' + (isActive ? ' is-active' : ''),
+ onClick: onSegmentClick ? function () { onSegmentClick(seg); } : undefined
+ },
+ React.createElement('span', { className: 'h2-analysis-legend-dot', style: { background: seg.color } }),
+ React.createElement('span', { className: 'h2-analysis-legend-label' }, seg.label),
+ React.createElement('span', { className: 'h2-analysis-legend-val' }, seg.value + ' (' + pct + '%)')
+ );
+ })
+ )
+ );
+}
+
+export function H2AnalysisHorizontalStackedBar(props) {
+ var items = props.items || [];
+ var keys = props.keys || [{ key: 'signed', color: COLORS.signed, label: '签约' }, { key: 'normal', color: COLORS.normal, label: '普通' }];
+ var onBarClick = props.onBarClick;
+ var activeFilter = props.activeFilter;
+ var maxVal = Math.max.apply(null, items.map(function (it) {
+ return keys.reduce(function (s, k) { return s + (it[k.key] || 0); }, 0);
+ }).concat([1]));
+ var barH = props.barHeight || 22;
+ var gap = props.gap || 10;
+ var labelW = props.labelWidth || 72;
+ var W = props.width || 520;
+ var chartW = W - labelW - 48;
+ var H = items.length * (barH + gap) + 16;
+
+ return React.createElement('svg', {
+ className: 'h2-analysis-chart-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': props.title || '横向堆叠条形图'
+ },
+ items.map(function (it, idx) {
+ var y = 8 + idx * (barH + gap);
+ var total = keys.reduce(function (s, k) { return s + (it[k.key] || 0); }, 0);
+ var x = labelW;
+ var isRowActive = !activeFilter || activeFilter.province === it.province;
+ return React.createElement('g', {
+ key: it.province || it.label || idx,
+ opacity: isRowActive ? 1 : 0.35
+ },
+ React.createElement('text', {
+ x: labelW - 8, y: y + barH / 2 + 4, textAnchor: 'end', className: 'h2-analysis-bar-label'
+ }, it.province || it.label),
+ keys.reduce(function (nodes, k) {
+ var val = it[k.key] || 0;
+ if (!val) return nodes;
+ var w = (val / maxVal) * chartW;
+ var rect = React.createElement('rect', {
+ key: k.key,
+ x: x, y: y, width: w, height: barH,
+ rx: 4, fill: k.color,
+ style: { cursor: onBarClick ? 'pointer' : 'default' },
+ onClick: onBarClick ? function () { onBarClick({ province: it.province, stationType: k.label, key: k.key }); } : undefined
+ });
+ nodes.push(rect);
+ x += w;
+ return nodes;
+ }, []),
+ total > 0 ? React.createElement('text', {
+ x: labelW + (total / maxVal) * chartW + 6,
+ y: y + barH / 2 + 4,
+ className: 'h2-analysis-bar-val'
+ }, total) : null
+ );
+ })
+ );
+}
+
+export function H2AnalysisStackedBarTrend(props) {
+ var periods = props.periods || [];
+ var onBarClick = props.onBarClick;
+ var activePeriod = props.activePeriod;
+ var W = 860;
+ var H = 280;
+ var PL = 44;
+ var PR = 16;
+ var PT = 16;
+ var PB = 42;
+ var chartW = W - PL - PR;
+ var chartH = H - PT - PB;
+ var count = periods.length || 1;
+ var groupW = chartW / count;
+ var barW = Math.min(28, groupW * 0.55);
+ var maxNew = Math.max.apply(null, periods.map(function (p) { return p.newTotal || 0; }).concat([1]));
+
+ return React.createElement('svg', {
+ className: 'h2-analysis-chart-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': props.title || '堆叠柱状趋势图'
+ },
+ [0, Math.ceil(maxNew / 2), maxNew].map(function (tick, idx) {
+ var y = PT + chartH - (tick / maxNew) * chartH;
+ return React.createElement('g', { key: 'y-' + idx },
+ React.createElement('line', { x1: PL, y1: y, x2: W - PR, y2: y, stroke: '#f1f5f9', strokeWidth: 1 }),
+ React.createElement('text', { x: PL - 8, y: y + 4, textAnchor: 'end', className: 'h2-analysis-axis-tick' }, tick)
+ );
+ }),
+ periods.map(function (p, idx) {
+ var cx = PL + groupW * idx + groupW / 2;
+ var signedH = ((p.newCooperative || 0) / maxNew) * chartH;
+ var normalH = ((p.newNormal || 0) / maxNew) * chartH;
+ var baseY = PT + chartH;
+ var isActive = activePeriod == null || activePeriod === idx;
+ return React.createElement('g', {
+ key: 'p-' + idx,
+ opacity: isActive ? 1 : 0.35,
+ style: { cursor: onBarClick ? 'pointer' : 'default' },
+ onClick: onBarClick ? function () { onBarClick(idx, p); } : undefined
+ },
+ React.createElement('rect', {
+ x: cx - barW / 2, y: baseY - signedH - normalH, width: barW, height: signedH,
+ rx: 3, fill: COLORS.signed
+ }),
+ React.createElement('rect', {
+ x: cx - barW / 2, y: baseY - normalH, width: barW, height: normalH,
+ rx: 3, fill: COLORS.normal
+ }),
+ (p.newTotal || 0) > 0 ? React.createElement('text', {
+ x: cx, y: baseY - signedH - normalH - 4, textAnchor: 'middle', className: 'h2-analysis-bar-val'
+ }, p.newTotal) : null,
+ React.createElement('text', {
+ x: cx, y: H - 12, textAnchor: 'middle', className: 'h2-analysis-axis-label'
+ }, p.label)
+ );
+ })
+ );
+}
+
+export function H2AnalysisLineTrend(props) {
+ var periods = props.periods || [];
+ var onPointClick = props.onPointClick;
+ var W = 860;
+ var H = 280;
+ var PL = 52;
+ var PR = 16;
+ var PT = 16;
+ var PB = 42;
+ var chartW = W - PL - PR;
+ var chartH = H - PT - PB;
+ var series = [
+ { key: 'cumulativeTotal', color: COLORS.total, label: '全部累计' },
+ { key: 'cumulativeCooperative', color: COLORS.signed, label: '签约累计' },
+ { key: 'cumulativeNormal', color: COLORS.normal, label: '普通累计' }
+ ];
+ var min = Math.min.apply(null, periods.map(function (p) {
+ return Math.min(p.cumulativeTotal, p.cumulativeCooperative, p.cumulativeNormal);
+ }));
+ var max = Math.max.apply(null, periods.map(function (p) {
+ return Math.max(p.cumulativeTotal, p.cumulativeCooperative, p.cumulativeNormal);
+ }));
+ var range = max - min || 1;
+
+ function linePath(key) {
+ return periods.map(function (p, idx) {
+ var x = PL + (periods.length === 1 ? chartW / 2 : (idx / (periods.length - 1)) * chartW);
+ var y = PT + chartH - ((p[key] - min) / range) * chartH;
+ return (idx === 0 ? 'M' : 'L') + x.toFixed(1) + ' ' + y.toFixed(1);
+ }).join(' ');
+ }
+
+ return React.createElement('svg', {
+ className: 'h2-analysis-chart-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': props.title || '折线趋势图'
+ },
+ [{ value: max, y: PT }, { value: min + range / 2, y: PT + chartH / 2 }, { value: min, y: PT + chartH }].map(function (tick, idx) {
+ return React.createElement('g', { key: 'ly-' + idx },
+ React.createElement('line', { x1: PL, y1: tick.y, x2: W - PR, y2: tick.y, stroke: '#f1f5f9', strokeWidth: 1 }),
+ React.createElement('text', { x: PL - 8, y: tick.y + 4, textAnchor: 'end', className: 'h2-analysis-axis-tick' }, Math.round(tick.value))
+ );
+ }),
+ series.map(function (s) {
+ return React.createElement('path', {
+ key: s.key,
+ d: linePath(s.key),
+ fill: 'none',
+ stroke: s.color,
+ strokeWidth: 2.5,
+ strokeLinejoin: 'round'
+ });
+ }),
+ periods.map(function (p, idx) {
+ if (idx % 3 !== 0 && idx !== periods.length - 1) return null;
+ var x = PL + (periods.length === 1 ? chartW / 2 : (idx / (periods.length - 1)) * chartW);
+ return React.createElement('text', {
+ key: 'xl-' + idx,
+ x: x, y: H - 12, textAnchor: 'middle', className: 'h2-analysis-axis-label'
+ }, p.label);
+ })
+ );
+}
+
+export function H2AnalysisSegmentBar(props) {
+ var segments = props.segments || [];
+ var onSegmentClick = props.onSegmentClick;
+ var activeKey = props.activeKey;
+ var W = 520;
+ var H = 200;
+ var PL = 44;
+ var PR = 16;
+ var PT = 24;
+ var PB = 48;
+ var chartW = W - PL - PR;
+ var chartH = H - PT - PB;
+ var maxVal = Math.max.apply(null, segments.map(function (s) { return s.value; }).concat([1]));
+ var barW = Math.min(56, chartW / segments.length - 16);
+
+ return React.createElement('svg', {
+ className: 'h2-analysis-chart-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': props.title || '分段柱状图'
+ },
+ segments.map(function (seg, idx) {
+ var cx = PL + (idx + 0.5) * (chartW / segments.length);
+ var h = (seg.value / maxVal) * chartH;
+ var y = PT + chartH - h;
+ var isActive = activeKey == null || activeKey === seg.key;
+ return React.createElement('g', {
+ key: seg.key,
+ opacity: isActive ? 1 : 0.35,
+ style: { cursor: onSegmentClick ? 'pointer' : 'default' },
+ onClick: onSegmentClick ? function () { onSegmentClick(seg); } : undefined
+ },
+ React.createElement('rect', {
+ x: cx - barW / 2, y: y, width: barW, height: h,
+ rx: 4, fill: seg.color
+ }),
+ seg.value > 0 ? React.createElement('text', {
+ x: cx, y: y - 6, textAnchor: 'middle', className: 'h2-analysis-bar-val'
+ }, seg.value) : null,
+ React.createElement('text', {
+ x: cx, y: H - 16, textAnchor: 'middle', className: 'h2-analysis-axis-label'
+ }, seg.label)
+ );
+ })
+ );
+}
+
+export function H2AnalysisHorizontalBar(props) {
+ var items = props.items || [];
+ var onBarClick = props.onBarClick;
+ var activeId = props.activeId;
+ var maxAbs = Math.max.apply(null, items.map(function (it) {
+ return Math.abs(it.value);
+ }).concat([1]));
+ var barH = 20;
+ var gap = 8;
+ var labelW = 120;
+ var W = props.width || 520;
+ var chartW = W - labelW - 56;
+ var H = items.length * (barH + gap) + 12;
+
+ return React.createElement('svg', {
+ className: 'h2-analysis-chart-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': props.title || '横向条形图'
+ },
+ items.map(function (it, idx) {
+ var y = 6 + idx * (barH + gap);
+ var w = (Math.abs(it.value) / maxAbs) * chartW;
+ var isActive = activeId == null || activeId === it.id;
+ var fill = it.value < 0 ? '#ef4444' : '#10b981';
+ return React.createElement('g', {
+ key: it.id || idx,
+ opacity: isActive ? 1 : 0.35,
+ style: { cursor: onBarClick ? 'pointer' : 'default' },
+ onClick: onBarClick ? function () { onBarClick(it); } : undefined
+ },
+ React.createElement('text', {
+ x: 0, y: y + barH / 2 + 4, className: 'h2-analysis-bar-label-sm'
+ }, (it.name || '').length > 10 ? (it.name || '').slice(0, 10) + '…' : it.name),
+ React.createElement('rect', {
+ x: labelW, y: y, width: Math.max(w, 2), height: barH, rx: 4, fill: fill
+ }),
+ React.createElement('text', {
+ x: labelW + w + 6, y: y + barH / 2 + 4, className: 'h2-analysis-bar-val'
+ }, it.value)
+ );
+ })
+ );
+}
+
+export function H2AnalysisChartCard(props) {
+ return React.createElement('div', { className: 'h2-analysis-chart-card' },
+ React.createElement('div', { className: 'h2-analysis-chart-head' },
+ React.createElement('span', { className: 'h2-analysis-chart-head__title' }, props.title),
+ props.legend ? React.createElement('div', { className: 'h2-analysis-chart-legend' }, props.legend) : null
+ ),
+ React.createElement('div', { className: 'h2-analysis-chart-body' + (props.compact ? ' is-compact' : '') }, props.children)
+ );
+}
+
+export function H2AnalysisLegendDot(color, label) {
+ return React.createElement('span', { className: 'h2-analysis-legend-inline' },
+ React.createElement('span', { className: 'h2-analysis-legend-dot', style: { background: color } }),
+ label
+ );
+}
+
+export { COLORS as H2_ANALYSIS_CHART_COLORS };
diff --git a/src/prototypes/oneos-web-h2-station-analysis/shared/h2-analysis-mock.js b/src/prototypes/oneos-web-h2-station-analysis/shared/h2-analysis-mock.js
new file mode 100644
index 0000000..6f2cc3b
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-analysis/shared/h2-analysis-mock.js
@@ -0,0 +1,508 @@
+/**
+ * 加氢站分析 — Mock 数据与聚合工具
+ * 字段口径与「站点信息」「站点周报统计」对齐
+ */
+
+export var H2_ANALYSIS_REGION_OPTIONS = [
+ { value: '湖北省', label: '湖北省', children: [{ value: '宜昌市', label: '宜昌市' }, { value: '黄石市', label: '黄石市' }] },
+ { value: '四川省', label: '四川省', children: [{ value: '资阳市', label: '资阳市' }] },
+ { value: '上海市', label: '上海市', children: [{ value: '奉贤区', label: '奉贤区' }, { value: '上海市', label: '上海市' }] },
+ { value: '浙江省', label: '浙江省', children: [{ value: '嘉兴市', label: '嘉兴市' }, { value: '杭州市', label: '杭州市' }, { value: '宁波市', label: '宁波市' }] },
+ { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] },
+ { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }
+];
+
+/** 各省存量站点分布(签约/普通),与全国累计 284 对齐 */
+var H2_ANALYSIS_PROVINCE_STOCK = [
+ { province: '浙江省', signed: 22, normal: 38 },
+ { province: '江苏省', signed: 14, normal: 31 },
+ { province: '广东省', signed: 12, normal: 28 },
+ { province: '湖北省', signed: 11, normal: 24 },
+ { province: '上海市', signed: 9, normal: 18 },
+ { province: '四川省', signed: 8, normal: 19 },
+ { province: '山东省', signed: 6, normal: 14 },
+ { province: '河南省', signed: 4, normal: 12 }
+];
+
+function h2AnalysisEnsureDayjs() {
+ return typeof window !== 'undefined' && window.dayjs ? window.dayjs : null;
+}
+
+export function h2AnalysisStartOfWeek(d) {
+ var dayjs = h2AnalysisEnsureDayjs();
+ if (!dayjs || !d) return null;
+ var cur = dayjs(d);
+ var weekday = cur.day();
+ var diff = weekday === 0 ? -6 : 1 - weekday;
+ return cur.add(diff, 'day').startOf('day');
+}
+
+export function h2AnalysisEndOfWeek(d) {
+ var start = h2AnalysisStartOfWeek(d);
+ return start ? start.add(6, 'day').endOf('day') : null;
+}
+
+export function h2AnalysisIsSameWeek(a, b) {
+ var sa = h2AnalysisStartOfWeek(a);
+ var sb = h2AnalysisStartOfWeek(b);
+ if (!sa || !sb) return false;
+ return sa.format('YYYY-MM-DD') === sb.format('YYYY-MM-DD');
+}
+
+export function h2AnalysisNum(v) {
+ var n = typeof v === 'number' ? v : parseFloat(v);
+ return isNaN(n) ? 0 : n;
+}
+
+export function h2AnalysisGetStationType(record) {
+ if (!record) return '普通';
+ if (record.stationType) return record.stationType;
+ return record.isSigned ? '签约' : '普通';
+}
+
+export function h2AnalysisGetBalanceStatus(record) {
+ var balance = h2AnalysisNum(record && record.prepaidBalance);
+ if (balance < 0) return 'negative';
+ var threshold = record && record.balanceAlertThreshold;
+ if (threshold != null && threshold !== '' && balance < h2AnalysisNum(threshold)) return 'alert';
+ return 'normal';
+}
+
+export function h2AnalysisGetPriceType(record) {
+ if (!record || !record.priceConfigs || !record.priceConfigs.length) return 'none';
+ var hasFixed = false;
+ var hasTiered = false;
+ record.priceConfigs.forEach(function (pc) {
+ if (pc.priceType === 'fixed') hasFixed = true;
+ if (pc.priceType === 'tiered') hasTiered = true;
+ });
+ if (hasTiered && !hasFixed) return 'tiered';
+ if (hasFixed) return 'fixed';
+ return 'none';
+}
+
+export function h2AnalysisGetContractDays(record) {
+ var dayjs = h2AnalysisEnsureDayjs();
+ if (!dayjs || !record || !record.contractEnd) return null;
+ var end = dayjs(record.contractEnd);
+ if (!end.isValid || !end.isValid()) return null;
+ return end.diff(dayjs(), 'day');
+}
+
+export function h2AnalysisGetContractBucket(days) {
+ if (days == null) return 'unknown';
+ if (days < 0) return 'expired';
+ if (days <= 30) return '0-30';
+ if (days <= 60) return '31-60';
+ if (days <= 90) return '61-90';
+ return '90+';
+}
+
+export function h2AnalysisGetRiskTypes(record) {
+ var risks = [];
+ if (h2AnalysisGetBalanceStatus(record) === 'alert') risks.push('余额预警');
+ if (h2AnalysisGetBalanceStatus(record) === 'negative') risks.push('负余额');
+ var days = h2AnalysisGetContractDays(record);
+ if (days != null && days < 0) risks.push('合同已到期');
+ else if (days != null && days <= 30) risks.push('合同即将到期');
+ if (h2AnalysisGetPriceType(record) === 'none') risks.push('未配置价格');
+ return risks;
+}
+
+/** 明细站点 Mock(含本周新增 + 存量 + 风险样本) */
+export var H2_ANALYSIS_MOCK_STATIONS = [
+ {
+ id: 'ws-1', name: '宜昌中石化枝江服务区加氢站', region: ['湖北省', '宜昌市'],
+ isSigned: true, stationType: '签约', createdAt: '2026-07-13 10:22:00',
+ businessStatus: '营业中', prepaidBalance: 62800, balanceAlertThreshold: 50000,
+ contractEnd: '2027-03-31', contact: '王供',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 43.2 }]
+ },
+ {
+ id: 'ws-2', name: '资阳临空经济区加氢站', region: ['四川省', '资阳市'],
+ isSigned: true, stationType: '签约', createdAt: '2026-07-14 15:08:00',
+ businessStatus: '营业中', prepaidBalance: 45200, balanceAlertThreshold: 40000,
+ contractEnd: '2027-05-31', contact: '李供',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 42.8 }]
+ },
+ {
+ id: 'ws-3', name: '黄石开发区加氢站', region: ['湖北省', '黄石市'],
+ isSigned: true, stationType: '签约', createdAt: '2026-07-15 09:41:00',
+ businessStatus: '营业中', prepaidBalance: 38500, balanceAlertThreshold: 30000,
+ contractEnd: '2027-02-28', contact: '周供',
+ priceConfigs: [{ priceType: 'tiered', customerScope: 'all', tierRules: [{ thresholdKg: 0, price: 44 }] }]
+ },
+ {
+ id: 'ws-4', name: '上海奉贤海湾加氢站', region: ['上海市', '奉贤区'],
+ isSigned: true, stationType: '签约', createdAt: '2026-07-16 11:30:00',
+ businessStatus: '营业中', prepaidBalance: 51200, balanceAlertThreshold: 45000,
+ contractEnd: '2027-06-30', contact: '孙供',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 45.5 }]
+ },
+ {
+ id: 1, name: '嘉兴加氢站(一期)', region: ['浙江省', '嘉兴市'],
+ isSigned: true, stationType: '签约', createdAt: '2024-01-15 09:00:00',
+ businessStatus: '营业中', prepaidBalance: 85620, balanceAlertThreshold: 50000,
+ contractEnd: '2026-12-31', contact: '张三',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 42.5 }, { priceType: 'tiered', customerScope: 'customer' }]
+ },
+ {
+ id: 2, name: '杭州临平加氢站', region: ['浙江省', '杭州市'],
+ isSigned: true, stationType: '签约', createdAt: '2025-03-10 14:20:00',
+ businessStatus: '营业中', prepaidBalance: 125000.5, balanceAlertThreshold: 80000,
+ contractEnd: '2027-02-28', contact: '李四',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 43 }]
+ },
+ {
+ id: 3, name: '上海宝山加氢站', region: ['上海市', '上海市'],
+ isSigned: true, stationType: '签约', createdAt: '2023-06-01 10:00:00',
+ businessStatus: '暂停营业', prepaidBalance: -3250, balanceAlertThreshold: 10000,
+ contractEnd: '2025-05-31', contact: '王五',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 44 }]
+ },
+ {
+ id: 4, name: '平湖合作停车场(待建加氢)', region: ['浙江省', '嘉兴市'],
+ isSigned: false, stationType: '普通', createdAt: '2025-11-20 11:20:00',
+ businessStatus: '停止营业', prepaidBalance: 0, balanceAlertThreshold: null,
+ contractEnd: '', contact: '赵六', priceConfigs: []
+ },
+ {
+ id: 5, name: '苏州工业园区备用站', region: ['江苏省', '苏州市'],
+ isSigned: false, stationType: '普通', createdAt: '2025-08-05 08:50:00',
+ businessStatus: '停止营业', prepaidBalance: 15800, balanceAlertThreshold: 20000,
+ contractEnd: '', contact: '钱七',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 41 }]
+ },
+ {
+ id: 6, name: '桐乡合作加氢站', region: ['浙江省', '嘉兴市'],
+ isSigned: true, stationType: '签约', createdAt: '2025-06-01 15:20:00',
+ businessStatus: '营业中', prepaidBalance: 42000, balanceAlertThreshold: 35000,
+ contractEnd: '2027-05-31', contact: '周八',
+ priceConfigs: [{ priceType: 'tiered', customerScope: 'all', tierRules: [{ thresholdKg: 0, price: 41.8 }] }]
+ },
+ {
+ id: 7, name: '南京江宁加氢站', region: ['江苏省', '南京市'],
+ isSigned: true, stationType: '签约', createdAt: '2024-09-12 10:30:00',
+ businessStatus: '营业中', prepaidBalance: 9800, balanceAlertThreshold: 15000,
+ contractEnd: '2026-08-15', contact: '吴九',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 43.5 }]
+ },
+ {
+ id: 8, name: '广州南沙加氢站', region: ['广东省', '广州市'],
+ isSigned: true, stationType: '签约', createdAt: '2024-05-18 16:00:00',
+ businessStatus: '营业中', prepaidBalance: 67200, balanceAlertThreshold: 50000,
+ contractEnd: '2026-07-28', contact: '郑十',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 46 }]
+ },
+ {
+ id: 9, name: '深圳坪山加氢站', region: ['广东省', '深圳市'],
+ isSigned: false, stationType: '普通', createdAt: '2025-01-22 09:15:00',
+ businessStatus: '营业中', prepaidBalance: 22800, balanceAlertThreshold: 25000,
+ contractEnd: '', contact: '冯十一',
+ priceConfigs: []
+ },
+ {
+ id: 10, name: '宁波北仑加氢站', region: ['浙江省', '宁波市'],
+ isSigned: false, stationType: '普通', createdAt: '2025-04-08 13:40:00',
+ businessStatus: '暂停营业', prepaidBalance: 5600, balanceAlertThreshold: 8000,
+ contractEnd: '', contact: '陈十二',
+ priceConfigs: [{ priceType: 'tiered', customerScope: 'all', tierRules: [{ thresholdKg: 0, price: 42 }] }]
+ },
+ {
+ id: 11, name: '济南章丘加氢站', region: ['山东省', '济南市'],
+ isSigned: true, stationType: '签约', createdAt: '2024-11-30 08:00:00',
+ businessStatus: '营业中', prepaidBalance: 31000, balanceAlertThreshold: 28000,
+ contractEnd: '2026-09-30', contact: '褚十三',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 42.2 }]
+ },
+ {
+ id: 12, name: '郑州航空港加氢站', region: ['河南省', '郑州市'],
+ isSigned: false, stationType: '普通', createdAt: '2025-07-01 11:00:00',
+ businessStatus: '营业中', prepaidBalance: -1200, balanceAlertThreshold: 5000,
+ contractEnd: '', contact: '卫十四',
+ priceConfigs: [{ priceType: 'fixed', customerScope: 'all', fixedPrice: 41.5 }]
+ }
+];
+
+/** 近 12 期趋势(与站点周报统计口径一致) */
+export function h2AnalysisBuildTrendPeriods() {
+ var dayjs = h2AnalysisEnsureDayjs();
+ var now = dayjs ? dayjs() : null;
+ var seeds = [
+ { newTotal: 1, newCooperative: 0 },
+ { newTotal: 2, newCooperative: 1 },
+ { newTotal: 0, newCooperative: 0 },
+ { newTotal: 3, newCooperative: 1 },
+ { newTotal: 1, newCooperative: 0 },
+ { newTotal: 2, newCooperative: 2 },
+ { newTotal: 0, newCooperative: 0 },
+ { newTotal: 4, newCooperative: 2 },
+ { newTotal: 1, newCooperative: 1 },
+ { newTotal: 2, newCooperative: 0 },
+ { newTotal: 0, newCooperative: 0 },
+ { newTotal: 4, newCooperative: 4 }
+ ];
+ var cumulativeTotal = 264;
+ var cumulativeCooperative = 75;
+ return seeds.map(function (seed, idx) {
+ var weekStart = now ? h2AnalysisStartOfWeek(now.subtract((11 - idx) * 7, 'day')) : null;
+ var newNormal = seed.newTotal - seed.newCooperative;
+ cumulativeTotal += seed.newTotal;
+ cumulativeCooperative += seed.newCooperative;
+ return {
+ period: idx + 1,
+ label: weekStart ? weekStart.format('MM/DD') : '第' + (idx + 1) + '期',
+ range: weekStart && weekStart.format ? weekStart.format('MM/DD') + '-' + weekStart.add(6, 'day').format('MM/DD') : '',
+ weekStart: weekStart,
+ newTotal: seed.newTotal,
+ newCooperative: seed.newCooperative,
+ newNormal: newNormal,
+ cumulativeTotal: cumulativeTotal,
+ cumulativeCooperative: cumulativeCooperative,
+ cumulativeNormal: cumulativeTotal - cumulativeCooperative
+ };
+ });
+}
+
+export function h2AnalysisGetStockSummary() {
+ var signed = 0;
+ var normal = 0;
+ H2_ANALYSIS_PROVINCE_STOCK.forEach(function (p) {
+ signed += p.signed;
+ normal += p.normal;
+ });
+ var operating = 0;
+ var pausedStopped = 0;
+ H2_ANALYSIS_MOCK_STATIONS.forEach(function (s) {
+ if (s.businessStatus === '营业中') operating += 1;
+ else pausedStopped += 1;
+ });
+ var scale = (signed + normal) / H2_ANALYSIS_MOCK_STATIONS.length;
+ return {
+ totalStations: signed + normal,
+ signedStations: signed,
+ normalStations: normal,
+ operatingStations: Math.round(operating * scale) || 238,
+ pausedStoppedStations: Math.round(pausedStopped * scale) || 46,
+ provinceCount: H2_ANALYSIS_PROVINCE_STOCK.length
+ };
+}
+
+export function h2AnalysisGetProvinceStock() {
+ return H2_ANALYSIS_PROVINCE_STOCK.map(function (p) {
+ return {
+ province: p.province,
+ signed: p.signed,
+ normal: p.normal,
+ total: p.signed + p.normal
+ };
+ }).sort(function (a, b) { return b.total - a.total; });
+}
+
+export function h2AnalysisFilterNewStations(stations, filters) {
+ var dayjs = h2AnalysisEnsureDayjs();
+ var weekStart = filters.weekValue ? h2AnalysisStartOfWeek(filters.weekValue) : null;
+ var weekEnd = weekStart ? h2AnalysisEndOfWeek(filters.weekValue) : null;
+ return stations.filter(function (s) {
+ if (!s.createdAt) return false;
+ if (weekStart && weekEnd && dayjs) {
+ var created = dayjs(s.createdAt.indexOf('T') >= 0 ? s.createdAt : s.createdAt.replace(' ', 'T'));
+ if (!created.isValid || !created.isValid()) return false;
+ if (created.isBefore(weekStart) || created.isAfter(weekEnd)) return false;
+ }
+ if (filters.region && filters.region.length) {
+ if (filters.region[0] && s.region[0] !== filters.region[0]) return false;
+ if (filters.region[1] && s.region[1] !== filters.region[1]) return false;
+ }
+ if (filters.stationType && h2AnalysisGetStationType(s) !== filters.stationType) return false;
+ if (filters.businessStatus && s.businessStatus !== filters.businessStatus) return false;
+ if (filters.chartFilter) {
+ var cf = filters.chartFilter;
+ if (cf.type === 'stationType' && h2AnalysisGetStationType(s) !== cf.value) return false;
+ if (cf.type === 'province' && s.region[0] !== cf.value) return false;
+ if (cf.type === 'period' && cf.periodIndex != null) {
+ var periods = h2AnalysisBuildTrendPeriods();
+ var p = periods[cf.periodIndex];
+ if (p && p.weekStart && dayjs) {
+ var c = dayjs(s.createdAt.replace(' ', 'T'));
+ var ws = p.weekStart;
+ var we = ws.add(6, 'day').endOf('day');
+ if (c.isBefore(ws) || c.isAfter(we)) return false;
+ }
+ }
+ }
+ return true;
+ });
+}
+
+export function h2AnalysisFilterStockStations(stations, filters) {
+ return stations.filter(function (s) {
+ if (filters.region && filters.region.length) {
+ if (filters.region[0] && s.region[0] !== filters.region[0]) return false;
+ if (filters.region[1] && s.region[1] !== filters.region[1]) return false;
+ }
+ if (filters.stationType && h2AnalysisGetStationType(s) !== filters.stationType) return false;
+ if (filters.businessStatus && s.businessStatus !== filters.businessStatus) return false;
+ if (filters.chartFilter) {
+ var cf = filters.chartFilter;
+ if (cf.type === 'stationType' && h2AnalysisGetStationType(s) !== cf.value) return false;
+ if (cf.type === 'businessStatus' && s.businessStatus !== cf.value) return false;
+ if (cf.type === 'province' && s.region[0] !== cf.value) return false;
+ }
+ return true;
+ });
+}
+
+export function h2AnalysisFilterRiskStations(stations, filters) {
+ return stations.filter(function (s) {
+ var risks = h2AnalysisGetRiskTypes(s);
+ if (filters.region && filters.region.length) {
+ if (filters.region[0] && s.region[0] !== filters.region[0]) return false;
+ if (filters.region[1] && s.region[1] !== filters.region[1]) return false;
+ }
+ if (filters.stationType && h2AnalysisGetStationType(s) !== filters.stationType) return false;
+ if (filters.businessStatus && s.businessStatus !== filters.businessStatus) return false;
+ if (filters.chartFilter) {
+ var cf = filters.chartFilter;
+ if (cf.type === 'balanceStatus' && h2AnalysisGetBalanceStatus(s) !== cf.value) return false;
+ if (cf.type === 'priceType' && h2AnalysisGetPriceType(s) !== cf.value) return false;
+ if (cf.type === 'contractBucket') {
+ if (!s.isSigned || !s.contractEnd) return false;
+ if (h2AnalysisGetContractBucket(h2AnalysisGetContractDays(s)) !== cf.value) return false;
+ }
+ if (cf.type === 'riskType' && risks.indexOf(cf.value) < 0) return false;
+ if (cf.type === 'station' && s.id !== cf.value) return false;
+ return true;
+ }
+ return risks.length > 0;
+ });
+}
+
+export function h2AnalysisGetNewSummary(stations, weekValue) {
+ var filtered = h2AnalysisFilterNewStations(stations, { weekValue: weekValue });
+ var signed = 0;
+ var normal = 0;
+ var provinces = {};
+ filtered.forEach(function (s) {
+ if (h2AnalysisGetStationType(s) === '签约') signed += 1;
+ else normal += 1;
+ provinces[s.region[0]] = true;
+ });
+ return {
+ newTotal: filtered.length,
+ newSigned: signed,
+ newNormal: normal,
+ newProvinceCount: Object.keys(provinces).length
+ };
+}
+
+export function h2AnalysisGetNewProvinceTop(stations, weekValue, limit) {
+ var filtered = h2AnalysisFilterNewStations(stations, { weekValue: weekValue });
+ var map = {};
+ filtered.forEach(function (s) {
+ var p = s.region[0];
+ if (!map[p]) map[p] = { province: p, signed: 0, normal: 0, total: 0 };
+ if (h2AnalysisGetStationType(s) === '签约') map[p].signed += 1;
+ else map[p].normal += 1;
+ map[p].total += 1;
+ });
+ return Object.keys(map).map(function (k) { return map[k]; })
+ .sort(function (a, b) { return b.total - a.total; })
+ .slice(0, limit || 10);
+}
+
+export function h2AnalysisGetRiskSummary(stations) {
+ var balanceAlert = 0;
+ var negative = 0;
+ var expiring30 = 0;
+ var expired = 0;
+ var noPrice = 0;
+ stations.forEach(function (s) {
+ var risks = h2AnalysisGetRiskTypes(s);
+ if (risks.indexOf('余额预警') >= 0) balanceAlert += 1;
+ if (risks.indexOf('负余额') >= 0) negative += 1;
+ if (risks.indexOf('合同即将到期') >= 0) expiring30 += 1;
+ if (risks.indexOf('合同已到期') >= 0) expired += 1;
+ if (risks.indexOf('未配置价格') >= 0) noPrice += 1;
+ });
+ var scale = 284 / Math.max(stations.length, 1);
+ return {
+ balanceAlert: Math.max(balanceAlert, Math.round(18 * scale / 12)),
+ negative: Math.max(negative, 3),
+ expiring30: Math.max(expiring30, Math.round(12 * scale / 12)),
+ expired: Math.max(expired, 2),
+ noPrice: Math.max(noPrice, 4)
+ };
+}
+
+export function h2AnalysisGetBalanceDistribution(stations) {
+ var normal = 0;
+ var alert = 0;
+ var negative = 0;
+ stations.forEach(function (s) {
+ var st = h2AnalysisGetBalanceStatus(s);
+ if (st === 'normal') normal += 1;
+ else if (st === 'alert') alert += 1;
+ else negative += 1;
+ });
+ return [
+ { key: 'normal', label: '正常', value: normal, color: '#10b981' },
+ { key: 'alert', label: '预警', value: alert, color: '#f59e0b' },
+ { key: 'negative', label: '负余额', value: negative, color: '#ef4444' }
+ ];
+}
+
+export function h2AnalysisGetPriceDistribution(stations) {
+ var fixed = 0;
+ var tiered = 0;
+ var none = 0;
+ stations.forEach(function (s) {
+ var pt = h2AnalysisGetPriceType(s);
+ if (pt === 'fixed') fixed += 1;
+ else if (pt === 'tiered') tiered += 1;
+ else none += 1;
+ });
+ return [
+ { key: 'fixed', label: '固定价格', value: fixed, color: '#10b981' },
+ { key: 'tiered', label: '阶梯价格', value: tiered, color: '#3b82f6' },
+ { key: 'none', label: '未配置', value: none, color: '#94a3b8' }
+ ];
+}
+
+export function h2AnalysisGetContractBuckets(stations) {
+ var buckets = { '0-30': 0, '31-60': 0, '61-90': 0, '90+': 0 };
+ stations.forEach(function (s) {
+ if (!s.isSigned || !s.contractEnd) return;
+ var b = h2AnalysisGetContractBucket(h2AnalysisGetContractDays(s));
+ if (b !== 'unknown' && b !== 'expired' && buckets[b] != null) buckets[b] += 1;
+ });
+ return [
+ { key: '0-30', label: '0-30天', value: buckets['0-30'], color: '#ef4444' },
+ { key: '31-60', label: '31-60天', value: buckets['31-60'], color: '#f59e0b' },
+ { key: '61-90', label: '61-90天', value: buckets['61-90'], color: '#3b82f6' },
+ { key: '90+', label: '90天以上', value: buckets['90+'], color: '#10b981' }
+ ];
+}
+
+export function h2AnalysisGetLowBalanceTop(stations, limit) {
+ return stations.slice().sort(function (a, b) {
+ return h2AnalysisNum(a.prepaidBalance) - h2AnalysisNum(b.prepaidBalance);
+ }).slice(0, limit || 10).map(function (s) {
+ return {
+ id: s.id,
+ name: s.name,
+ value: h2AnalysisNum(s.prepaidBalance),
+ province: s.region[0]
+ };
+ });
+}
+
+export function h2AnalysisFormatRegion(record) {
+ if (!record || !record.region) return '';
+ return record.region[0] + '-' + record.region[1];
+}
+
+export function h2AnalysisFormatYuan(v) {
+ var n = h2AnalysisNum(v);
+ return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
diff --git a/src/prototypes/oneos-web-h2-station-weekly/.spec/requirements-prd.md b/src/prototypes/oneos-web-h2-station-weekly/.spec/requirements-prd.md
new file mode 100644
index 0000000..f1429b9
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-weekly/.spec/requirements-prd.md
@@ -0,0 +1,253 @@
+# 加氢站管理 · 站点周报统计 — 产品需求文档(PRD)
+
+| 项 | 内容 |
+|---|---|
+| 文档版本 | v1.0 |
+| 模块名称 | 加氢站管理 → 站点周报统计 |
+| 所属系统 | ONE-OS Web 端 |
+| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |
+| 交互原型 | `/prototypes/oneos-web-h2-station-weekly` |
+| 关联原型 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢站合包](/prototypes/oneos-web-h2-station) |
+| 设计规范 | `src/prototypes/vm-shared/DESIGN.md` |
+| 文档状态 | 已对齐原型与批注 |
+
+---
+
+## 1. 背景与目标
+
+### 1.1 业务背景
+
+采购与运营团队每周五需人工整理加氢站新增与累计数据,通过邮件发送周报。该流程耗时、口径易不一致,且难以回溯历史周数据。
+
+### 1.2 本期目标
+
+- 提供**自然周维度**的站点新增与累计统计,替代人工邮件周报主流程
+- 支持按周筛选、KPI 概览、邮件风格摘要、新增明细表、趋势图与 Excel 导出
+- 与「站点信息」主数据口径对齐(创建时间、站点类型、省/市等)
+
+### 1.3 用户角色
+
+| 角色 | 典型诉求 |
+|---|---|
+| 采购/运营 | 每周查看新增签约与普通站点,复制摘要用于沟通 |
+| 管理层 | 查看近 12 期趋势与全国累计 |
+| 数据分析 | 导出 Excel 做二次分析 |
+
+### 1.4 非目标(本期不做)
+
+- 邮件自动发送、PDF 排版导出
+- 后端接口联调(原型使用 Mock)
+- 站点主数据维护(仍在「站点信息」)
+
+---
+
+## 2. 信息架构
+
+```text
+站点周报统计
+├── 筛选区(统计周、快捷周、站点类型、地区)
+├── KPI 卡片(4 张)
+├── 周报摘要条
+├── 新增明细表 + 工具栏(趋势展示、导出 Excel)
+├── 趋势弹窗(双 Tab:每期新增 / 累计数量)
+└── 站点详情抽屉
+```
+
+---
+
+## 3. 筛选区
+
+与站点信息等模块一致的 **vm 筛选卡片** 布局。
+
+| 字段 | 控件 | 说明 |
+|---|---|---|
+| 统计周 | 周选择器 | 自然周:周一 00:00 ~ 周日 23:59(Asia/Shanghai);展示 `YYYY-MM-DD ~ YYYY-MM-DD` |
+| 快捷周 | 按钮组 | 「本周」「上周」一键切换并**立即查询** |
+| 站点类型 | 下拉 | 全部 / 签约站点 / 普通站点 |
+| 地区(省-市) | 级联 | 省、市两级,可只选省;`changeOnSelect` |
+
+**交互**:
+
+- **查询**:应用筛选,刷新 KPI、摘要、列表,回到第 1 页
+- **重置**:清空类型与地区,统计周回到本周并刷新
+- **手选统计周**:与快捷周一致,选择后**自动查询**(无需再点查询)
+
+---
+
+## 4. KPI 统计卡片
+
+四张 **vm-kpi-card**,只读展示(非点击筛选)。
+
+| 卡片 | 口径 | 动态标题 |
+|---|---|---|
+| 该周新增 | 所选自然周内创建的有效站点数 | 本周/上周/该周新增 |
+| 全国累计 | 截至所选周末仍在册的有效站点总数 | 固定 |
+| 签约站点 | 合作加氢站累计 | 固定 |
+| 普通站点 | 非合作加氢站累计 | 固定 |
+
+首卡副文案:`签约 N · 普通 N`。
+
+KPI 标题、表格空态、摘要尾部「截至本周末」等文案须**随所选统计周**切换(本周 / 上周 / 该周末)。
+
+---
+
+## 5. 周报摘要条
+
+邮件风格一句话汇总,位于 KPI 与表格之间。
+
+**周期前缀规则**:
+
+| 所选周 | 前缀示例 |
+|---|---|
+| 本周 | 本周(07/13-07/19) |
+| 上周 | 上周(07/06-07/12) |
+| 其它历史周 | 仅日期区间 `05/26-06/01` |
+
+**有新增模板**:
+
+> {周期前缀}新增签约站点 **N** 个、普通站点 **N** 个;截至{本周末/上周末/该周末},全国加氢站 **N** 个,其中签约 **N** 个、普通 **N** 个。
+
+**无新增模板**:
+
+> {周期前缀}无新增站点;截至{周末标签},全国加氢站 **N** 个……
+
+---
+
+## 6. 新增明细表
+
+### 6.1 工具栏
+
+| 按钮 | 行为 |
+|---|---|
+| 趋势展示 | 打开近 12 期趋势弹窗 |
+| 导出 Excel | 导出当前筛选结果 CSV(UTF-8 BOM),文件名含统计周期 |
+
+**不包含**:导出 PDF(P1)按钮。
+
+### 6.2 列表字段
+
+| 列名 | 说明 |
+|---|---|
+| 序 | 当前页序号 |
+| 省/市 | 合并展示,格式 `湖北省-宜昌市` |
+| 加氢站名称 | 可点击查看详情 |
+| 站点类型 | 签约站点 / 普通站点 Tag |
+| 创建时间 | `YYYY-MM-DD HH:mm:ss` |
+| 操作 | `OperationActions` 组件,主操作「查看」 |
+
+### 6.3 空态
+
+随所选周动态:`本周无新增站点` / `上周无新增站点` / `{日期区间} 无新增站点`。
+
+### 6.4 分页
+
+默认 10 条,可选 5/10/20/50(`TablePagination` + `COMPACT_PAGE_SIZE_OPTIONS`)。
+
+---
+
+## 7. 趋势展示弹窗
+
+标题:**趋势展示**。副标题:近 12 期站点变化趋势。
+
+拆为 **两个 Tab**,避免单页图表过小:
+
+### Tab 1:每期新增站点
+
+柱状图三组:
+
+| 系列 | 颜色 | 说明 |
+|---|---|---|
+| 新增收录站点数 | 蓝色 | 该期全部新增 |
+| 新增合作站点数 | 绿色 | 该期签约新增 |
+| 新增普通站点数 | 橙色 | 收录 − 合作 |
+
+数据表列:期次、周期、新增收录、新增合作、新增普通。
+
+### Tab 2:累计站点数量
+
+折线图三条:
+
+| 系列 | 颜色 | 说明 |
+|---|---|---|
+| 签约站点累计数量 | 绿色 | |
+| 普通站点累计数量 | 橙色 | 全部 − 签约 |
+| 全部站点累计数量 | 紫色 | |
+
+数据表列:期次、周期、签约累计、普通累计、全部累计。
+
+**约束**:仅展示数量,不使用「增长良好」「环比下降」等评价标签。
+
+**可访问性**:图表下方提供数据表,供读屏与键盘用户查阅。
+
+---
+
+## 8. 站点详情抽屉
+
+字段与主表对齐:
+
+| 字段 | 格式 |
+|---|---|
+| 加氢站名称 | 文本 |
+| 省/市 | `省-市` |
+| 站点类型 | Tag |
+| 创建时间 | `YYYY-MM-DD HH:mm:ss` |
+
+---
+
+## 9. 统计口径
+
+| 指标 | 规则 |
+|---|---|
+| 该周新增 | 站点「创建时间」落入所选自然周的有效站点 |
+| 签约/普通新增 | 按站点类型拆分 |
+| 全国累计 | 截至该周日 23:59:59 仍在册的有效站点 |
+| 签约/普通累计 | 按类型统计截至周末的存量 |
+
+数据来源:加氢站管理 → 站点信息。
+
+---
+
+## 10. 设计规范
+
+- 页面壳层:`vm-page` + `lc-page` + `h2-weekly-page`
+- 主题色 `#32a06e`,字体 Inter / 苹方
+- 操作列使用 `OperationActions`
+- 分页使用 `TablePagination`
+- 不重复侧栏模块大标题
+
+---
+
+## 11. 验收标准
+
+### 11.1 筛选与 KPI
+
+- [ ] 统计周、快捷周、类型、地区筛选联动正确
+- [ ] 手选周与快捷周均自动加载数据
+- [ ] KPI / 空态 / 摘要文案随所选周切换
+
+### 11.2 表格与导出
+
+- [ ] 列名:省/市、加氢站名称、创建时间
+- [ ] 操作列使用 OperationActions
+- [ ] 导出 Excel 含摘要与明细
+
+### 11.3 趋势图
+
+- [ ] 双 Tab 展示
+- [ ] 含普通站点新增与累计
+- [ ] 无评价性标签
+- [ ] 附带数据表
+
+### 11.4 文档
+
+- [ ] 右上角可打开本 PRD
+
+---
+
+## 12. 修订记录
+
+| 版本 | 日期 | 说明 |
+|---|---|---|
+| v1.0 | 2026-07-13 | 初版:整合批注与 UI 评审意见,对齐现行原型 |
+
+**文档结束**
diff --git a/src/prototypes/oneos-web-h2-station-weekly/.spec/reviews/ui-review.md b/src/prototypes/oneos-web-h2-station-weekly/.spec/reviews/ui-review.md
new file mode 100644
index 0000000..9565859
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-weekly/.spec/reviews/ui-review.md
@@ -0,0 +1,92 @@
+# UI Review
+
+- 审查目标:`src/prototypes/oneos-web-h2-station-weekly`(页面实现:`src/prototypes/oneos-web-h2-station/pages/04-站点周报统计.jsx`)
+- 使用设计依据:`src/prototypes/vm-shared/DESIGN.md`
+- 生成时间:2026-07-13
+
+## 总体点评
+
+站点周报统计原型整体遵循 OneOS `vm-page` 列表页壳层:无重复页头大标题、筛选区 + KPI 行 + 工具栏表格 + 底部分页结构清晰,主题色与站点信息模块一致(`#32a06e`、Inter/苹方字体、`TablePagination` 公共分页)。业务主链路「选周 → 看 KPI/摘要 → 查明细 → 导出/趋势」已可演示,趋势弹窗拆为双 Tab 后图表可读性明显优于单页堆叠,摘要条对本周/上周/历史周的周期前缀区分也已落地。
+
+主要风险集中在 **与 DESIGN.md 强制规范的操作列实现偏差**,以及 **「所选周」与界面固定「本周」文案之间的语义错位**——用户切换到上周或更早自然周后,KPI 标题、表格空态、摘要尾部「截至本周末」等仍沿用「本周」语境,容易误判统计范围。次要问题包括主表与抽屉/脚注字段命名不一致、周选择器与快捷周查询行为不一致。趋势图为纯 SVG 展示,数据探索对键盘与读屏用户支持有限。
+
+亮点:页面未重复侧栏模块名;工具栏无重复条数;KPI 卡片带口径说明;筛选区含周选择器、快捷周、类型与地区级联;趋势图仅展示数量、无评价性标签。Impeccable `detect.mjs` 静态扫描未检出违规项。
+
+## P0-P3 优先级问题
+
+### P1 - 操作列未使用 OperationActions 公共组件
+
+- 证据:`04-站点周报统计.jsx` 操作列使用手写 ``,未引入 `OperationActions`;`vm-shared/DESIGN.md` 规定 `vm-page` 列表主表操作列一律使用 `../../common/OperationActions`。
+- 影响:与加氢站站点信息、租赁合同等模块操作区视觉与交互不一致;后续若扩展「跳转站点信息」等更多操作,易再次出现各页自建链接组,增加维护成本与可发现性差异。
+- 修复方向:改为 ` openDetail(record) }} />`;列宽建议 148–168px、`fixed: 'right'`,与 DESIGN 参考实现对齐。
+
+### P1 - KPI / 空态 / 摘要尾部文案未随所选统计周切换
+
+- 证据:KPI 首卡标题恒为「本周新增」;表格 `emptyText` 恒为「本周无新增站点」;摘要后半句恒为「截至本周末」——即使用户通过快捷周或周选择器查看上周/历史周(摘要前半段已正确显示「上周(…)」或纯日期区间)。
+- 影响:周报核对场景下,用户可能认为 KPI 与空态仍指当前自然周,导致对「上周无新增」等结论理解偏差,增加与采购经理解读邮件口径时的沟通成本。
+- 修复方向:根据 `appliedWeek` 动态替换为「本周/上周/该周」新增;空态改为「该周无新增站点」或复用 `summaryPeriod` 逻辑;累计句改为「截至该周末」。
+
+### P2 - 主表与抽屉、脚注字段命名不一致
+
+- 证据:表格列已使用「创建时间」「省/市」「加氢站名称」;底部脚注仍写「入库时间」;详情 Drawer 仍展示「站点名称」「所在省份」「所在城市」「入库时间」且未合并省/市。
+- 影响:同一页面内术语不统一,用户在表格与详情间切换时产生「是否同一字段」的疑惑;与 1.2 站点信息主数据口径对齐感较弱。
+- 修复方向:脚注改为「创建时间」;Drawer 字段与表格列名、格式对齐(省/市合并、`加氢站名称`、`创建时间` 带秒级格式)。
+
+### P2 - 周选择器与快捷周查询行为不一致
+
+- 证据:`DatePicker` 的 `onChange` 仅更新 `weekValue`,不触发 `simulateLoad`;「本周」「上周」快捷按钮则会立即加载数据。用户在手选历史周后必须再点「查询」才能看到结果。
+- 影响:同一筛选区内两种选周方式行为不同,增加学习成本;用户易误以为已切换统计周而实际仍展示旧数据。
+- 修复方向:统一行为——手选周后自动查询,或在周选择器旁增加「已选周未应用」提示;与站点信息筛选区「查询/重置」模式保持一致并在文案上明示。
+
+### P3 - 趋势图缺少键盘与读屏友好的数据探索
+
+- 证据:趋势弹窗内柱状图、折线图为纯 SVG,`role="img"` + 简短 `aria-label`;除柱顶/末点少量数值外,无法通过键盘聚焦逐期读取数据,也无表格兜底。
+- 影响:依赖读屏或键盘的用户难以完整获取 12 期趋势明细,不符合数据报表类页面的包容性预期。
+- 修复方向:为每期数据提供可聚焦节点或「查看数据表」切换;折线图补充 `aria-describedby` 关联简要数据摘要;窄屏下考虑减少 X 轴标签密度避免重叠。
+
+## 核心元件
+
+### 筛选区(vm-filter-card)
+
+四字段栅格(统计周、快捷周、站点类型、地区省-市)与站点信息页面对齐,周选择器附带自然周口径说明,符合 B 端报表筛选习惯。**保留** `vm-filter-title`「筛选条件」与 `vm-filter-actions` 右对齐查询/重置。**调整**周选择器与快捷周的行为一致性(见 P2);地区级联已复用省/市结构,与批注一致。
+
+### KPI 卡片行(vm-kpi-row)
+
+四卡(本周新增、全国累计、签约、普通)数字等宽、副文案拆分签约/普通,帮助图标 + tooltip 说明口径,布局在 1200px 以下降为 2 列、640px 降为 1 列,响应式合理。卡片为只读展示(非可点击筛选),与站点信息 KPI 区分明确。**保留**四卡结构与 tooltip。**调整**首卡标题及副文案应随 `appliedWeek` 动态化(见 P1)。
+
+### 周报摘要条(h2-weekly-summary)
+
+邮件风格一句话汇总,有/无新增两套模板,历史周仅显示日期区间的前缀逻辑正确,有数据与空态通过边框/背景区分。**保留**摘要条位置与语气。**调整**尾部「截至本周末」应随所选周改为「该周末」(见 P1)。
+
+### 数据表格与工具栏(vm-table-section)
+
+工具栏右对齐「趋势展示」「导出 Excel」,无重复条数,符合 DESIGN 工具栏规范;表格列序/省/市/加氢站名称/类型/创建时间/操作清晰,分页使用 `TablePagination` + `COMPACT_PAGE_SIZE_OPTIONS`。**问题**:操作列未用 `OperationActions`(见 P1);加氢站名称与「查看」双入口功能重复但可接受。**保留**表格结构与分页;**调整**操作列组件化。
+
+### 趋势展示弹窗(h2-weekly-trend-modal)
+
+双 Tab(每期新增 / 累计数量)解决单页图表过小问题;图例仅标识系列名称与颜色,无评价性标签,符合产品批注。柱状图双系列分组、折线图双线累计表达清楚。**保留** Tab 拆分与纯数量展示。**调整**可访问性与窄屏 X 轴密度(见 P3)。
+
+### 站点详情抽屉(Drawer)
+
+只读字段列表结构简单。**问题**字段命名与主表不一致(见 P2)。**建议**与表格列对齐后,可考虑增加「前往站点信息」跳转入口(非本次必须)。
+
+## 响应式与可访问性
+
+- **响应式**:KPI 行具备 1200px / 640px 断点;趋势弹窗 `width: 920`、`max-height: 78vh` 在笔记本宽度下可用;主表 `scroll.x: 820` 列数适中,桌面常见宽度下横向滚动压力小于宽表台账页。未针对移动端单独优化,符合后台桌面优先场景。
+- **键盘**:筛选按钮、工具栏按钮、分页控件可 Tab 到达;KPI 帮助区 `tabIndex={0}` 但非标准 `button`,聚焦语义偏弱;趋势图不可键盘操作(见 P3)。
+- **语义**:筛选区 `aria-label="筛选条件"`;摘要 `role="status"`;KPI `aria-label` 含数值;分页由 `TablePagination` 提供 `role="navigation"`。
+- **对比度**:主文字 `#0f172a` / `#334155` 与白底对比充足;辅助说明 `#64748b`、`#94a3b8` 用于脚注与轴标签,建议抽样复核 AA。
+- **动效**:页面未引入额外动效;`Spin` loading 时长约 320ms,可接受。
+
+## 证据与评估说明
+
+- **浏览器/截图**:本次未启动本地预览与浏览器截图,结论主要来自源码、`vm-shared/DESIGN.md` 对照与样式走查。
+- **Scanner**:已运行 `node rules/references/impeccable/scripts/detect.mjs --json src/prototypes/oneos-web-h2-station-weekly`,返回 `[]`(无静态检出项)。
+- **报告模板**:`client/src/resources/templates/ui-review-report-template.md` 在仓库中未找到,结构按 `rules/ui-review-guide.md` 与既有 `vehicle-management/.spec/ui-review.md` 范例组织。
+- **独立评估**:`degraded` — 未使用浏览器实机验收与截图;设计一致性评估基于 `vm-shared/DESIGN.md` 与源码,完整度低于「预览 + 截图 + 双评估」流程。
+
+### 后续建议
+
+1. 优先修复 2 项 P1(操作列组件、周语境文案),再在预览中验收「上周 / 历史周」场景。
+2. 统一 Drawer、脚注与主表字段命名后,可请业务方做一次与邮件周报口径的对照验收。
+3. 若趋势图需对外汇报,建议补充「导出趋势图」或数据表视图,并复评 P3 可访问性项。
diff --git a/src/prototypes/oneos-web-h2-station-weekly/index.tsx b/src/prototypes/oneos-web-h2-station-weekly/index.tsx
new file mode 100644
index 0000000..b7e141f
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station-weekly/index.tsx
@@ -0,0 +1,70 @@
+/**
+ * @name 站点周报统计
+ * 加氢站管理 — 站点周报统计独立预览(与站点信息并列)
+ */
+import '../../common/oneosWebLegacy/legacyGlobals';
+import React, { useEffect } from 'react';
+import * as antd from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import isoWeek from 'dayjs/plugin/isoWeek';
+import updateLocale from 'dayjs/plugin/updateLocale';
+import { ConfigProvider } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+import 'antd/dist/reset.css';
+import '../vehicle-management/style.css';
+import '../lease-contract-management/styles/lease-contract.css';
+import '../oneos-web-h2-station/styles/h2-station-vm-filter.css';
+import { clearHostPrototypeRouteInfo } from '../../common/useHashPage';
+import WeeklyReportPage from '../oneos-web-h2-station/pages/04-站点周报统计.jsx';
+
+dayjs.extend(isoWeek);
+dayjs.extend(updateLocale);
+dayjs.locale('zh-cn');
+dayjs.updateLocale('zh-cn', { weekStart: 1 });
+
+/** 与租赁合同管理 / vehicle-management / 站点信息主题一致 */
+const vmTheme = {
+ token: {
+ colorPrimary: '#32a06e',
+ colorLink: '#32a06e',
+ colorLinkHover: '#3fb87c',
+ borderRadius: 8,
+ fontFamily:
+ 'Inter, -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif',
+ fontSize: 14,
+ colorText: '#18181b',
+ colorTextSecondary: '#52525b',
+ colorBorder: '#e5e7eb',
+ colorBgContainer: '#ffffff',
+ },
+ components: {
+ Table: {
+ headerBg: '#f6f7f7',
+ headerColor: '#71717a',
+ rowHoverBg: '#f6f7f7',
+ borderColor: '#e5e7eb',
+ cellPaddingBlock: 8,
+ cellPaddingInline: 12,
+ },
+ Card: {
+ borderRadiusLG: 12,
+ },
+ },
+};
+
+window.React = React;
+window.antd = antd;
+(window as Window & { dayjs: typeof dayjs }).dayjs = dayjs;
+
+export default function OneosWebH2StationWeekly() {
+ useEffect(() => {
+ clearHostPrototypeRouteInfo();
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/prototypes/oneos-web-h2-station/pages/04-站点周报统计.jsx b/src/prototypes/oneos-web-h2-station/pages/04-站点周报统计.jsx
new file mode 100644
index 0000000..43753d9
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station/pages/04-站点周报统计.jsx
@@ -0,0 +1,1303 @@
+// 【重要】必须使用 const Component 作为组件变量名
+// 加氢站管理 - 站点周报统计(替代每周五人工邮件周报)
+
+import { TablePagination, COMPACT_PAGE_SIZE_OPTIONS } from '../../../common/TablePagination';
+import { OperationActions } from '../../../common/OperationActions';
+import { H2_WEEKLY_REQUIREMENT_DOC } from './站点周报统计-需求内容.js';
+import {
+ H2StationViewModal,
+ H2_STATION_VIEW_MODAL_STYLES,
+ h2WeeklyResolveStationViewRecord
+} from '../shared/h2-station-view-shared.jsx';
+
+/** @see web端/styles/oneos-ant-table-global-fix.js */
+var ONEOS_ANT_TABLE_GLOBAL_FIX = [
+ '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }',
+ '.ant-table-container .ant-table-body { margin-top: 0 !important; }',
+ '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }',
+ '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }'
+];
+
+var H2_WEEKLY_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([
+ '.vm-page.lc-page.h2-weekly-page { font-family: var(--vm-font); font-size: 0.875rem; line-height: 1.5; letter-spacing: -0.01em; color: var(--ln-body); -webkit-font-smoothing: antialiased; height: 100vh; overflow: auto; box-sizing: border-box; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-body { min-width: 0; }',
+ '.vm-page.lc-page.h2-weekly-page .vm-filter-card { margin-bottom: 16px; }',
+ '.vm-page.lc-page.h2-weekly-page .vm-kpi-row { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 16px; }',
+ '@media (max-width: 1200px) { .vm-page.lc-page.h2-weekly-page .vm-kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }',
+ '@media (max-width: 640px) { .vm-page.lc-page.h2-weekly-page .vm-kpi-row { grid-template-columns: 1fr; } }',
+ '.vm-page.lc-page.h2-weekly-page .vm-kpi-card { cursor: default; }',
+ '.vm-page.lc-page.h2-weekly-page .vm-kpi-card.h2-weekly-kpi--clickable { cursor: pointer; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-kpi-sub { display: block; margin-top: 4px; font-size: 0.75rem; font-weight: 500; color: var(--ln-muted); font-variant-numeric: tabular-nums; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-summary { margin-bottom: 16px; padding: 12px 16px; border-radius: 10px; border: 1px solid #bbf7d0; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); color: #334155; font-size: 0.8125rem; line-height: 1.55; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-summary--empty { border-color: #e2e8f0; background: #f8fafc; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-summary strong { color: #0f172a; font-weight: 600; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-footnote { margin-top: 12px; padding: 0 2px; font-size: 0.75rem; color: var(--ln-muted); line-height: 1.5; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-week-hint { margin-top: 6px; font-size: 0.75rem; color: var(--ln-muted); font-variant-numeric: tabular-nums; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-quick { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-quick .vm-btn { min-height: var(--vm-control-height, 32px); }',
+ '.vm-page.lc-page.h2-weekly-page .h2-weekly-type-tag.ant-tag { margin: 0; border-radius: 6px; font-size: 0.75rem; font-weight: 500; line-height: 20px; }',
+ '.vm-page.lc-page.h2-weekly-page .vm-table-section { min-width: 0; max-width: 100%; flex: 1; display: flex; flex-direction: column; min-height: 0; }',
+ '.vm-page.lc-page.h2-weekly-page .vm-filter-field .ant-select, .vm-page.lc-page.h2-weekly-page .vm-filter-field .ant-picker, .vm-page.lc-page.h2-weekly-page .vm-filter-field .ant-cascader { width: 100%; }',
+ '.h2-weekly-trend-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; }',
+ '.h2-weekly-trend-modal .ant-modal-header { padding: 18px 24px 12px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }',
+ '.h2-weekly-trend-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }',
+ '.h2-weekly-trend-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; max-height: 78vh; overflow: auto; }',
+ '.h2-weekly-trend-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }',
+ '.h2-weekly-trend-panel { display: flex; flex-direction: column; gap: 16px; }',
+ '.h2-weekly-trend-subtitle { margin: 0; font-size: 13px; font-weight: 600; color: #334155; }',
+ '.h2-weekly-trend-chart-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }',
+ '.h2-weekly-trend-chart-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }',
+ '.h2-weekly-trend-chart-head__title { font-size: 13px; font-weight: 700; color: #334155; }',
+ '.h2-weekly-trend-legend { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }',
+ '.h2-weekly-trend-legend__item { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; }',
+ '.h2-weekly-trend-legend__dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }',
+ '.h2-weekly-trend-legend__dot--line { border-radius: 50%; }',
+ '.h2-weekly-trend-chart-body { padding: 12px 14px 10px; min-height: 300px; }',
+ '.h2-weekly-trend-tabs .ant-tabs-nav { margin-bottom: 12px !important; }',
+ '.h2-weekly-trend-tabs .ant-tabs-tab { font-size: 13px !important; font-weight: 600 !important; padding: 8px 4px !important; }',
+ '.h2-weekly-trend-tabs .ant-tabs-content-holder { padding-top: 4px; }',
+ '.h2-weekly-trend-svg { display: block; width: 100%; height: auto; }',
+ '.h2-weekly-trend-axis-label { font-size: 10px; fill: #94a3b8; font-family: var(--vm-font); }',
+ '.h2-weekly-trend-axis-tick { font-size: 10px; fill: #94a3b8; font-family: var(--vm-font-mono); }',
+ '.h2-weekly-trend-bar-label { font-size: 9px; fill: #64748b; font-family: var(--vm-font-mono); }',
+ '.h2-weekly-trend-data-table { width: 100%; margin-top: 12px; border-collapse: collapse; font-size: 12px; }',
+ '.h2-weekly-trend-data-table th, .h2-weekly-trend-data-table td { padding: 6px 8px; border: 1px solid #e2e8f0; text-align: left; }',
+ '.h2-weekly-trend-data-table th { background: #f8fafc; color: #64748b; font-weight: 600; }',
+ '.h2-weekly-trend-data-table td { color: #334155; font-variant-numeric: tabular-nums; }',
+ '.h2-weekly-trend-data-table caption { caption-side: top; text-align: left; padding-bottom: 8px; font-size: 12px; font-weight: 600; color: #475569; }',
+ '.h2-weekly-page-top { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 12px; }',
+ '.h2-weekly-page-top__link { border: none; background: none; padding: 0; cursor: pointer; font-size: 14px; font-weight: 500; color: #32a06e; }',
+ '.h2-weekly-page-top__link:hover { color: #3fb87c; text-decoration: underline; }',
+ '.h2-weekly-req-doc-panel { padding: 4px 4px 16px; }'
+].concat(H2_STATION_VIEW_MODAL_STYLES));
+
+var H2_WEEKLY_REGION_CASCADER_OPTIONS = [
+ { value: '湖北省', label: '湖北省', children: [{ value: '宜昌市', label: '宜昌市' }, { value: '黄石市', label: '黄石市' }] },
+ { value: '四川省', label: '四川省', children: [{ value: '资阳市', label: '资阳市' }] },
+ { value: '上海市', label: '上海市', children: [{ value: '奉贤区', label: '奉贤区' }] },
+ { value: '浙江省', label: '浙江省', children: [{ value: '嘉兴市', label: '嘉兴市' }, { value: '杭州市', label: '杭州市' }, { value: '宁波市', label: '宁波市' }] },
+ { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] },
+ { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] }
+];
+
+var H2_WEEKLY_KPI_META = [
+ { key: 'newTotal', title: '本周新增', desc: '所选自然周内入库的有效站点数量' },
+ { key: 'totalStations', title: '全国累计', desc: '截至所选周末仍在册的有效站点总数' },
+ { key: 'cooperative', title: '签约站点', desc: '合作加氢站累计(签约站点)' },
+ { key: 'normal', title: '普通站点', desc: '非合作加氢站累计(普通站点)' }
+];
+
+function h2WeeklyHelpIcon() {
+ return React.createElement('svg', {
+ xmlns: 'http://www.w3.org/2000/svg',
+ width: 14,
+ height: 14,
+ viewBox: '0 0 24 24',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 2,
+ strokeLinecap: 'round',
+ strokeLinejoin: 'round',
+ 'aria-hidden': true
+ },
+ React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
+ React.createElement('path', { d: 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3' }),
+ React.createElement('path', { d: 'M12 17h.01' })
+ );
+}
+
+function h2WeeklyEnsureDayjs() {
+ return window.dayjs || null;
+}
+
+function h2WeeklyStartOfWeek(d) {
+ var dayjs = h2WeeklyEnsureDayjs();
+ if (!dayjs || !d) return null;
+ var cur = dayjs(d);
+ var weekday = cur.day(); // 0=Sun … 6=Sat
+ var diff = weekday === 0 ? -6 : 1 - weekday;
+ return cur.add(diff, 'day').startOf('day');
+}
+
+function h2WeeklyEndOfWeek(d) {
+ var start = h2WeeklyStartOfWeek(d);
+ return start ? start.add(6, 'day').endOf('day') : null;
+}
+
+function h2WeeklyFormatRange(weekStart) {
+ var dayjs = h2WeeklyEnsureDayjs();
+ if (!dayjs || !weekStart) return '';
+ var start = h2WeeklyStartOfWeek(weekStart);
+ var end = h2WeeklyEndOfWeek(weekStart);
+ if (!start || !end) return '';
+ return start.format('YYYY-MM-DD') + ' ~ ' + end.format('YYYY-MM-DD');
+}
+
+function h2WeeklyFormatRangeShort(weekStart) {
+ var dayjs = h2WeeklyEnsureDayjs();
+ if (!dayjs || !weekStart) return '';
+ var start = h2WeeklyStartOfWeek(weekStart);
+ var end = h2WeeklyEndOfWeek(weekStart);
+ if (!start || !end) return '';
+ return start.format('MM/DD') + '-' + end.format('MM/DD');
+}
+
+/** 摘要条周期文案:本周 / 上周 / 仅日期区间 */
+function h2WeeklyResolveSummaryPeriod(weekStart) {
+ var dayjs = h2WeeklyEnsureDayjs();
+ var range = h2WeeklyFormatRangeShort(weekStart);
+ var now = dayjs ? dayjs() : null;
+ if (!now || !weekStart) return { type: 'other', range: range };
+ if (h2WeeklyIsSameWeek(weekStart, now)) return { type: 'this', range: range };
+ if (h2WeeklyIsSameWeek(weekStart, now.subtract(7, 'day'))) return { type: 'last', range: range };
+ return { type: 'other', range: range };
+}
+
+function h2WeeklyRenderSummaryPeriodLead(period) {
+ if (!period || !period.range) return null;
+ if (period.type === 'this') {
+ return React.createElement(React.Fragment, null,
+ '本周(', React.createElement('strong', null, period.range), ')'
+ );
+ }
+ if (period.type === 'last') {
+ return React.createElement(React.Fragment, null,
+ '上周(', React.createElement('strong', null, period.range), ')'
+ );
+ }
+ return React.createElement('strong', null, period.range);
+}
+
+/** 随所选统计周切换的文案(KPI / 空态 / 摘要尾部) */
+function h2WeeklyGetPeriodCopy(period) {
+ if (!period) {
+ return { newLabel: '该周新增', emptyText: '该周无新增站点', weekendLabel: '该周末' };
+ }
+ if (period.type === 'this') {
+ return { newLabel: '本周新增', emptyText: '本周无新增站点', weekendLabel: '本周末' };
+ }
+ if (period.type === 'last') {
+ return { newLabel: '上周新增', emptyText: '上周无新增站点', weekendLabel: '上周末' };
+ }
+ return {
+ newLabel: '该周新增',
+ emptyText: (period.range || '该周') + ' 无新增站点',
+ weekendLabel: '该周末'
+ };
+}
+
+function h2WeeklyFormatCreatedAt(value) {
+ if (!value) return '';
+ var dayjs = h2WeeklyEnsureDayjs();
+ if (dayjs) {
+ var d = dayjs(value.indexOf('T') >= 0 ? value : value.replace(' ', 'T'));
+ if (d.isValid && d.isValid()) return d.format('YYYY-MM-DD HH:mm:ss');
+ }
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(value)) return value + ':00';
+ return value;
+}
+
+function h2WeeklyFormatRegion(record) {
+ if (!record) return '';
+ return record.province + '-' + record.city;
+}
+
+function h2WeeklyMatchRegion(record, region) {
+ if (!region || !region.length) return true;
+ var provinceMatch = !region[0] || record.province === region[0];
+ var cityMatch = !region[1] || record.city === region[1];
+ return provinceMatch && cityMatch;
+}
+
+function h2WeeklyIsSameWeek(a, b) {
+ var sa = h2WeeklyStartOfWeek(a);
+ var sb = h2WeeklyStartOfWeek(b);
+ if (!sa || !sb) return false;
+ return sa.format('YYYY-MM-DD') === sb.format('YYYY-MM-DD');
+}
+
+/** Mock A:本周有新增 4 条签约站;累计 284 / 86 / 198 */
+var H2_WEEKLY_MOCK_WITH_NEW = {
+ summary: {
+ newTotal: 4,
+ newCooperative: 4,
+ newNormal: 0,
+ totalStations: 284,
+ cooperativeStations: 86,
+ normalStations: 198
+ },
+ list: [
+ {
+ id: 'ws-1',
+ province: '湖北省',
+ city: '宜昌市',
+ stationName: '宜昌中石化枝江服务区加氢站',
+ stationType: '签约',
+ entryAt: '2026-07-13 10:22:00'
+ },
+ {
+ id: 'ws-2',
+ province: '四川省',
+ city: '资阳市',
+ stationName: '资阳临空经济区加氢站',
+ stationType: '签约',
+ entryAt: '2026-07-14 15:08:00'
+ },
+ {
+ id: 'ws-3',
+ province: '湖北省',
+ city: '黄石市',
+ stationName: '黄石开发区加氢站',
+ stationType: '签约',
+ entryAt: '2026-07-15 09:41:00'
+ },
+ {
+ id: 'ws-4',
+ province: '上海市',
+ city: '奉贤区',
+ stationName: '上海奉贤海湾加氢站',
+ stationType: '签约',
+ entryAt: '2026-07-16 11:30:00'
+ }
+ ]
+};
+
+/** Mock B:上周无新增 */
+var H2_WEEKLY_MOCK_EMPTY = {
+ summary: {
+ newTotal: 0,
+ newCooperative: 0,
+ newNormal: 0,
+ totalStations: 284,
+ cooperativeStations: 86,
+ normalStations: 198
+ },
+ list: []
+};
+
+function h2WeeklyPickMock(weekValue) {
+ var dayjs = h2WeeklyEnsureDayjs();
+ var now = dayjs ? dayjs() : null;
+ if (now && h2WeeklyIsSameWeek(weekValue, now)) return H2_WEEKLY_MOCK_WITH_NEW;
+ var lastWeek = now ? now.subtract(7, 'day') : null;
+ if (lastWeek && h2WeeklyIsSameWeek(weekValue, lastWeek)) return H2_WEEKLY_MOCK_EMPTY;
+ // 其它历史周:默认无新增(便于演示空态)
+ return H2_WEEKLY_MOCK_EMPTY;
+}
+
+/** 近 12 期(自然周)站点变化趋势 mock */
+function h2WeeklyBuildTrendPeriods() {
+ var dayjs = h2WeeklyEnsureDayjs();
+ var now = dayjs ? dayjs() : null;
+ var seeds = [
+ { newTotal: 1, newCooperative: 0 },
+ { newTotal: 2, newCooperative: 1 },
+ { newTotal: 0, newCooperative: 0 },
+ { newTotal: 3, newCooperative: 1 },
+ { newTotal: 1, newCooperative: 0 },
+ { newTotal: 2, newCooperative: 2 },
+ { newTotal: 0, newCooperative: 0 },
+ { newTotal: 4, newCooperative: 2 },
+ { newTotal: 1, newCooperative: 1 },
+ { newTotal: 2, newCooperative: 0 },
+ { newTotal: 0, newCooperative: 0 },
+ { newTotal: 4, newCooperative: 4 }
+ ];
+ var cumulativeTotal = 264;
+ var cumulativeCooperative = 75;
+ return seeds.map(function (seed, idx) {
+ var weekStart = now ? h2WeeklyStartOfWeek(now.subtract((11 - idx) * 7, 'day')) : null;
+ var newNormal = seed.newTotal - seed.newCooperative;
+ cumulativeTotal += seed.newTotal;
+ cumulativeCooperative += seed.newCooperative;
+ return {
+ period: idx + 1,
+ label: weekStart ? weekStart.format('MM/DD') : '第' + (idx + 1) + '期',
+ range: weekStart ? h2WeeklyFormatRangeShort(weekStart) : '',
+ newTotal: seed.newTotal,
+ newCooperative: seed.newCooperative,
+ newNormal: newNormal,
+ cumulativeTotal: cumulativeTotal,
+ cumulativeCooperative: cumulativeCooperative,
+ cumulativeNormal: cumulativeTotal - cumulativeCooperative
+ };
+ });
+}
+
+function h2WeeklyTrendLegendItem(color, label, isLine) {
+ return React.createElement('span', { className: 'h2-weekly-trend-legend__item', key: label },
+ React.createElement('span', {
+ className: 'h2-weekly-trend-legend__dot' + (isLine ? ' h2-weekly-trend-legend__dot--line' : ''),
+ style: { background: color }
+ }),
+ label
+ );
+}
+
+function h2WeeklyRenderBarTrendChart(periods) {
+ var W = 860;
+ var H = 300;
+ var PL = 44;
+ var PR = 16;
+ var PT = 16;
+ var PB = 42;
+ var chartW = W - PL - PR;
+ var chartH = H - PT - PB;
+ var count = periods.length;
+ var groupW = chartW / count;
+ var barW = Math.min(10, groupW * 0.16);
+ var gap = 3;
+ var maxNew = Math.max.apply(null, periods.map(function (p) {
+ return Math.max(p.newTotal, p.newCooperative, p.newNormal);
+ }).concat([1]));
+
+ return React.createElement('svg', {
+ className: 'h2-weekly-trend-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': '每期新增站点柱状图'
+ },
+ [0, Math.ceil(maxNew / 2), maxNew].map(function (tick, idx) {
+ var y = PT + chartH - (tick / maxNew) * chartH;
+ return React.createElement('g', { key: 'y-' + idx },
+ React.createElement('line', { x1: PL, y1: y, x2: W - PR, y2: y, stroke: '#f1f5f9', strokeWidth: 1 }),
+ React.createElement('text', {
+ x: PL - 8, y: y + 4, textAnchor: 'end', className: 'h2-weekly-trend-axis-tick'
+ }, tick)
+ );
+ }),
+ periods.map(function (p, idx) {
+ var cx = PL + groupW * idx + groupW / 2;
+ var totalH = (p.newTotal / maxNew) * chartH;
+ var coopH = (p.newCooperative / maxNew) * chartH;
+ var normalH = (p.newNormal / maxNew) * chartH;
+ var totalX = cx - barW - gap - barW;
+ var coopX = cx - barW / 2;
+ var normalX = cx + gap;
+ var baseY = PT + chartH;
+ return React.createElement('g', { key: 'bar-' + p.period },
+ React.createElement('rect', {
+ x: totalX, y: baseY - totalH, width: barW, height: totalH,
+ rx: 3, fill: '#3b82f6'
+ }),
+ p.newTotal > 0 ? React.createElement('text', {
+ x: totalX + barW / 2, y: baseY - totalH - 4,
+ textAnchor: 'middle', className: 'h2-weekly-trend-bar-label'
+ }, p.newTotal) : null,
+ React.createElement('rect', {
+ x: coopX, y: baseY - coopH, width: barW, height: coopH,
+ rx: 3, fill: '#10b981'
+ }),
+ p.newCooperative > 0 ? React.createElement('text', {
+ x: coopX + barW / 2, y: baseY - coopH - 4,
+ textAnchor: 'middle', className: 'h2-weekly-trend-bar-label'
+ }, p.newCooperative) : null,
+ React.createElement('rect', {
+ x: normalX, y: baseY - normalH, width: barW, height: normalH,
+ rx: 3, fill: '#f59e0b'
+ }),
+ p.newNormal > 0 ? React.createElement('text', {
+ x: normalX + barW / 2, y: baseY - normalH - 4,
+ textAnchor: 'middle', className: 'h2-weekly-trend-bar-label'
+ }, p.newNormal) : null,
+ React.createElement('text', {
+ x: cx, y: H - 12, textAnchor: 'middle', className: 'h2-weekly-trend-axis-label'
+ }, p.label)
+ );
+ })
+ );
+}
+
+function h2WeeklyRenderLineTrendChart(periods) {
+ var W = 860;
+ var H = 300;
+ var PL = 52;
+ var PR = 16;
+ var PT = 16;
+ var PB = 42;
+ var chartW = W - PL - PR;
+ var chartH = H - PT - PB;
+ var min = Math.min.apply(null, periods.map(function (p) {
+ return Math.min(p.cumulativeCooperative, p.cumulativeTotal, p.cumulativeNormal);
+ }));
+ var max = Math.max.apply(null, periods.map(function (p) {
+ return Math.max(p.cumulativeCooperative, p.cumulativeTotal, p.cumulativeNormal);
+ }));
+ var range = max - min || 1;
+ var coordsTotal = periods.map(function (p, idx) {
+ var x = PL + (periods.length === 1 ? chartW / 2 : (idx / (periods.length - 1)) * chartW);
+ var y = PT + chartH - ((p.cumulativeTotal - min) / range) * chartH;
+ return { x: x, y: y, value: p.cumulativeTotal, label: p.label };
+ });
+ var coordsCoop = periods.map(function (p, idx) {
+ var x = PL + (periods.length === 1 ? chartW / 2 : (idx / (periods.length - 1)) * chartW);
+ var y = PT + chartH - ((p.cumulativeCooperative - min) / range) * chartH;
+ return { x: x, y: y, value: p.cumulativeCooperative, label: p.label };
+ });
+ var coordsNormal = periods.map(function (p, idx) {
+ var x = PL + (periods.length === 1 ? chartW / 2 : (idx / (periods.length - 1)) * chartW);
+ var y = PT + chartH - ((p.cumulativeNormal - min) / range) * chartH;
+ return { x: x, y: y, value: p.cumulativeNormal, label: p.label };
+ });
+ var linePath = function (coords) {
+ return coords.map(function (c, i) {
+ return (i === 0 ? 'M' : 'L') + c.x.toFixed(1) + ' ' + c.y.toFixed(1);
+ }).join(' ');
+ };
+ var yTicks = [
+ { value: max, y: PT },
+ { value: min + range / 2, y: PT + chartH / 2 },
+ { value: min, y: PT + chartH }
+ ];
+
+ return React.createElement('svg', {
+ className: 'h2-weekly-trend-svg',
+ viewBox: '0 0 ' + W + ' ' + H,
+ role: 'img',
+ 'aria-label': '站点累计数量折线图'
+ },
+ yTicks.map(function (tick, idx) {
+ return React.createElement('g', { key: 'ly-' + idx },
+ React.createElement('line', { x1: PL, y1: tick.y, x2: W - PR, y2: tick.y, stroke: '#f1f5f9', strokeWidth: 1 }),
+ React.createElement('text', {
+ x: PL - 8, y: tick.y + 4, textAnchor: 'end', className: 'h2-weekly-trend-axis-tick'
+ }, Math.round(tick.value))
+ );
+ }),
+ React.createElement('path', {
+ d: linePath(coordsTotal), fill: 'none', stroke: '#6366f1', strokeWidth: 2.5, strokeLinejoin: 'round'
+ }),
+ React.createElement('path', {
+ d: linePath(coordsCoop), fill: 'none', stroke: '#10b981', strokeWidth: 2.5, strokeLinejoin: 'round'
+ }),
+ React.createElement('path', {
+ d: linePath(coordsNormal), fill: 'none', stroke: '#f59e0b', strokeWidth: 2.5, strokeLinejoin: 'round'
+ }),
+ coordsTotal.map(function (c, idx) {
+ return React.createElement('g', { key: 'pt-' + idx },
+ React.createElement('circle', { cx: c.x, cy: c.y, r: 3.5, fill: '#6366f1', stroke: '#fff', strokeWidth: 1.5 }),
+ idx === coordsTotal.length - 1 ? React.createElement('text', {
+ x: c.x, y: c.y - 8, textAnchor: 'middle', className: 'h2-weekly-trend-bar-label'
+ }, c.value) : null
+ );
+ }),
+ coordsCoop.map(function (c, idx) {
+ return React.createElement('g', { key: 'pc-' + idx },
+ React.createElement('circle', { cx: c.x, cy: c.y, r: 3.5, fill: '#10b981', stroke: '#fff', strokeWidth: 1.5 }),
+ idx === coordsCoop.length - 1 ? React.createElement('text', {
+ x: c.x, y: c.y + 14, textAnchor: 'middle', className: 'h2-weekly-trend-bar-label'
+ }, c.value) : null
+ );
+ }),
+ coordsNormal.map(function (c, idx) {
+ return React.createElement('g', { key: 'pn-' + idx },
+ React.createElement('circle', { cx: c.x, cy: c.y, r: 3.5, fill: '#f59e0b', stroke: '#fff', strokeWidth: 1.5 }),
+ idx === coordsNormal.length - 1 ? React.createElement('text', {
+ x: c.x, y: c.y + 26, textAnchor: 'middle', className: 'h2-weekly-trend-bar-label'
+ }, c.value) : null
+ );
+ }),
+ coordsTotal.filter(function (_c, idx) {
+ return idx === 0 || idx === coordsTotal.length - 1 || idx % 3 === 0;
+ }).map(function (c, idx) {
+ return React.createElement('text', {
+ key: 'xl-' + idx,
+ x: c.x, y: H - 12, textAnchor: 'middle', className: 'h2-weekly-trend-axis-label'
+ }, c.label);
+ })
+ );
+}
+
+function h2WeeklyRenderTrendNewTable(periods) {
+ return React.createElement('table', { className: 'h2-weekly-trend-data-table' },
+ React.createElement('caption', null, '每期新增数据表(可供读屏与键盘用户查阅)'),
+ React.createElement('thead', null,
+ React.createElement('tr', null,
+ React.createElement('th', { scope: 'col' }, '期次'),
+ React.createElement('th', { scope: 'col' }, '周期'),
+ React.createElement('th', { scope: 'col' }, '新增收录'),
+ React.createElement('th', { scope: 'col' }, '新增合作'),
+ React.createElement('th', { scope: 'col' }, '新增普通')
+ )
+ ),
+ React.createElement('tbody', null,
+ periods.map(function (p) {
+ return React.createElement('tr', { key: 'new-row-' + p.period },
+ React.createElement('td', null, '第' + p.period + '期'),
+ React.createElement('td', null, p.range || p.label),
+ React.createElement('td', null, p.newTotal),
+ React.createElement('td', null, p.newCooperative),
+ React.createElement('td', null, p.newNormal)
+ );
+ })
+ )
+ );
+}
+
+function h2WeeklyRenderTrendCumulativeTable(periods) {
+ return React.createElement('table', { className: 'h2-weekly-trend-data-table' },
+ React.createElement('caption', null, '累计站点数据表(可供读屏与键盘用户查阅)'),
+ React.createElement('thead', null,
+ React.createElement('tr', null,
+ React.createElement('th', { scope: 'col' }, '期次'),
+ React.createElement('th', { scope: 'col' }, '周期'),
+ React.createElement('th', { scope: 'col' }, '签约累计'),
+ React.createElement('th', { scope: 'col' }, '普通累计'),
+ React.createElement('th', { scope: 'col' }, '全部累计')
+ )
+ ),
+ React.createElement('tbody', null,
+ periods.map(function (p) {
+ return React.createElement('tr', { key: 'cum-row-' + p.period },
+ React.createElement('td', null, '第' + p.period + '期'),
+ React.createElement('td', null, p.range || p.label),
+ React.createElement('td', null, p.cumulativeCooperative),
+ React.createElement('td', null, p.cumulativeNormal),
+ React.createElement('td', null, p.cumulativeTotal)
+ );
+ })
+ )
+ );
+}
+
+function h2WeeklyRenderTrendPanel() {
+ var Tabs = window.antd && window.antd.Tabs;
+ var periods = h2WeeklyBuildTrendPeriods();
+ var newChartCard = React.createElement('div', { className: 'h2-weekly-trend-chart-card' },
+ React.createElement('div', { className: 'h2-weekly-trend-chart-head' },
+ React.createElement('span', { className: 'h2-weekly-trend-chart-head__title' }, '每期新增站点'),
+ React.createElement('div', { className: 'h2-weekly-trend-legend' },
+ h2WeeklyTrendLegendItem('#3b82f6', '新增收录站点数', false),
+ h2WeeklyTrendLegendItem('#10b981', '新增合作站点数', false),
+ h2WeeklyTrendLegendItem('#f59e0b', '新增普通站点数', false)
+ )
+ ),
+ React.createElement('div', { className: 'h2-weekly-trend-chart-body' },
+ h2WeeklyRenderBarTrendChart(periods),
+ h2WeeklyRenderTrendNewTable(periods)
+ )
+ );
+ var cumulativeChartCard = React.createElement('div', { className: 'h2-weekly-trend-chart-card' },
+ React.createElement('div', { className: 'h2-weekly-trend-chart-head' },
+ React.createElement('span', { className: 'h2-weekly-trend-chart-head__title' }, '累计站点数量'),
+ React.createElement('div', { className: 'h2-weekly-trend-legend' },
+ h2WeeklyTrendLegendItem('#10b981', '签约站点累计数量', true),
+ h2WeeklyTrendLegendItem('#f59e0b', '普通站点累计数量', true),
+ h2WeeklyTrendLegendItem('#6366f1', '全部站点累计数量', true)
+ )
+ ),
+ React.createElement('div', { className: 'h2-weekly-trend-chart-body' },
+ h2WeeklyRenderLineTrendChart(periods),
+ h2WeeklyRenderTrendCumulativeTable(periods)
+ )
+ );
+
+ if (!Tabs) {
+ return React.createElement('div', { className: 'h2-weekly-trend-panel' },
+ React.createElement('p', { className: 'h2-weekly-trend-subtitle' }, '近12期站点变化趋势'),
+ newChartCard,
+ cumulativeChartCard
+ );
+ }
+
+ return React.createElement('div', { className: 'h2-weekly-trend-panel' },
+ React.createElement('p', { className: 'h2-weekly-trend-subtitle' }, '近12期站点变化趋势'),
+ React.createElement(Tabs, {
+ className: 'h2-weekly-trend-tabs',
+ defaultActiveKey: 'new',
+ items: [
+ { key: 'new', label: '每期新增站点', children: newChartCard },
+ { key: 'cumulative', label: '累计站点数量', children: cumulativeChartCard }
+ ]
+ })
+ );
+}
+
+function h2WeeklyParsePrdInlineText(text) {
+ var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g);
+ var nodes = [];
+ for (var i = 0; i < parts.length; i++) {
+ var p = parts[i];
+ if (!p) continue;
+ if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) {
+ nodes.push(React.createElement('strong', { key: i }, p.slice(2, -2)));
+ } else {
+ nodes.push(p);
+ }
+ }
+ return nodes.length === 1 ? nodes[0] : nodes;
+}
+
+function h2WeeklyIsPrdTableRow(line) {
+ return /^\|.+\|$/.test(String(line || '').trim());
+}
+
+function h2WeeklyIsPrdTableSep(line) {
+ return /^\|[\s\-:|]+\|$/.test(String(line || '').trim());
+}
+
+function h2WeeklyRenderPrdTableRow(line, rowKey, isHeader) {
+ var cells = String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (c) {
+ return c.trim();
+ });
+ return React.createElement('tr', { key: rowKey },
+ cells.map(function (cell, ci) {
+ var Tag = isHeader ? 'th' : 'td';
+ return React.createElement(Tag, {
+ key: ci,
+ style: {
+ border: '1px solid #e5e7eb',
+ padding: '8px 10px',
+ textAlign: 'left',
+ verticalAlign: 'top',
+ fontWeight: isHeader ? 600 : 400,
+ background: isHeader ? '#f8fafc' : '#fff',
+ fontSize: 13,
+ lineHeight: 1.5
+ }
+ }, h2WeeklyParsePrdInlineText(cell));
+ })
+ );
+}
+
+function h2WeeklyRenderPrdMarkdown(markdown) {
+ var lines = String(markdown || '').split(/\r?\n/);
+ var nodes = [];
+ var i = 0;
+ var inCode = false;
+ var codeBuf = [];
+
+ while (i < lines.length) {
+ var line = lines[i];
+ var trimmed = String(line || '').trim();
+
+ if (trimmed.indexOf('```') === 0) {
+ if (inCode) {
+ nodes.push(React.createElement('pre', {
+ key: 'code-' + i,
+ style: {
+ margin: '12px 0', padding: '12px 14px', background: '#f6f8fa',
+ border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 12,
+ lineHeight: 1.6, overflow: 'auto', color: '#334155', whiteSpace: 'pre-wrap'
+ }
+ }, codeBuf.join('\n')));
+ codeBuf = [];
+ inCode = false;
+ } else {
+ inCode = true;
+ }
+ i += 1;
+ continue;
+ }
+ if (inCode) { codeBuf.push(line); i += 1; continue; }
+ if (trimmed === '---') {
+ nodes.push(React.createElement('hr', { key: 'hr-' + i, style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } }));
+ i += 1;
+ continue;
+ }
+ if (h2WeeklyIsPrdTableRow(trimmed)) {
+ var tableLines = [];
+ while (i < lines.length && h2WeeklyIsPrdTableRow(String(lines[i]).trim())) {
+ tableLines.push(String(lines[i]).trim());
+ i += 1;
+ }
+ var bodyRows = [];
+ for (var ti = 0; ti < tableLines.length; ti++) {
+ if (h2WeeklyIsPrdTableSep(tableLines[ti])) continue;
+ bodyRows.push(h2WeeklyRenderPrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0));
+ }
+ if (bodyRows.length) {
+ nodes.push(React.createElement('div', { key: 'tbl-' + i, style: { overflowX: 'auto', margin: '12px 0 16px' } },
+ React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
+ React.createElement('tbody', null, bodyRows)
+ )
+ ));
+ }
+ continue;
+ }
+ if (!trimmed) { i += 1; continue; }
+ if (trimmed.indexOf('# ') === 0) {
+ nodes.push(React.createElement('h1', { key: 'h1-' + i, style: { fontSize: 20, fontWeight: 700, color: '#0f172a', margin: '0 0 16px', lineHeight: 1.35 } }, h2WeeklyParsePrdInlineText(trimmed.slice(2).trim())));
+ i += 1; continue;
+ }
+ if (trimmed.indexOf('## ') === 0) {
+ nodes.push(React.createElement('h2', { key: 'h2-' + i, style: { fontSize: 16, fontWeight: 700, color: '#1e293b', margin: '24px 0 12px', paddingBottom: 6, borderBottom: '2px solid #e0f2fe', lineHeight: 1.4 } }, h2WeeklyParsePrdInlineText(trimmed.slice(3).trim())));
+ i += 1; continue;
+ }
+ if (trimmed.indexOf('### ') === 0) {
+ nodes.push(React.createElement('h3', { key: 'h3-' + i, style: { fontSize: 14, fontWeight: 600, color: '#334155', margin: '16px 0 8px', lineHeight: 1.45 } }, h2WeeklyParsePrdInlineText(trimmed.slice(4).trim())));
+ i += 1; continue;
+ }
+ if (trimmed === '**文档结束**') {
+ nodes.push(React.createElement('div', { key: 'end-' + i, style: { marginTop: 24, paddingTop: 16, borderTop: '1px dashed #e2e8f0', color: '#94a3b8', fontSize: 13, textAlign: 'center' } }, '— 文档结束 —'));
+ i += 1; continue;
+ }
+ if (trimmed.indexOf('- ') === 0) {
+ nodes.push(React.createElement('div', { key: 'ul-' + i, style: { display: 'flex', gap: 8, fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '4px 0 4px 2px' } },
+ React.createElement('span', { style: { color: '#32a06e', flexShrink: 0 } }, '•'),
+ React.createElement('span', { style: { flex: 1 } }, h2WeeklyParsePrdInlineText(trimmed.slice(2).trim()))
+ ));
+ i += 1; continue;
+ }
+ if (trimmed.indexOf('> ') === 0) {
+ nodes.push(React.createElement('blockquote', { key: 'bq-' + i, style: { margin: '8px 0', padding: '8px 12px', borderLeft: '3px solid #bbf7d0', background: '#f8fafc', color: '#475569', fontSize: 13, lineHeight: 1.75 } }, h2WeeklyParsePrdInlineText(trimmed.slice(2).trim())));
+ i += 1; continue;
+ }
+ nodes.push(React.createElement('p', { key: 'p-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0' } }, h2WeeklyParsePrdInlineText(trimmed)));
+ i += 1;
+ }
+ return nodes;
+}
+
+function h2WeeklyRenderRequirementDocPanel() {
+ var md = H2_WEEKLY_REQUIREMENT_DOC || '';
+ if (!md) {
+ return React.createElement('div', { style: { padding: 24, color: '#64748b', textAlign: 'center' } }, '需求文档未加载');
+ }
+ return React.createElement('div', { className: 'h2-weekly-req-doc-panel' }, h2WeeklyRenderPrdMarkdown(md));
+}
+
+function h2WeeklyDownloadCsv(weekStart, summary, list) {
+ var range = h2WeeklyFormatRange(weekStart).replace(/\s/g, '');
+ var rows = [];
+ rows.push(['统计周期', range]);
+ rows.push(['本周新增', String(summary.newTotal)]);
+ rows.push(['本周新增签约', String(summary.newCooperative)]);
+ rows.push(['本周新增普通', String(summary.newNormal)]);
+ rows.push(['全国累计', String(summary.totalStations)]);
+ rows.push(['签约站点累计', String(summary.cooperativeStations)]);
+ rows.push(['普通站点累计', String(summary.normalStations)]);
+ rows.push([]);
+ rows.push(['序', '省/市', '加氢站名称', '站点类型', '创建时间']);
+ (list || []).forEach(function (row, idx) {
+ rows.push([
+ String(idx + 1),
+ h2WeeklyFormatRegion(row),
+ row.stationName,
+ row.stationType,
+ h2WeeklyFormatCreatedAt(row.entryAt)
+ ]);
+ });
+ var csv = rows.map(function (cols) {
+ return cols.map(function (cell) {
+ var s = cell == null ? '' : String(cell);
+ if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
+ return s;
+ }).join(',');
+ }).join('\n');
+ var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a');
+ a.href = url;
+ a.download = '加氢站周报_' + range.replace(/~/g, '_') + '.csv';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+const Component = function () {
+ var useState = React.useState;
+ var useMemo = React.useMemo;
+ var useCallback = React.useCallback;
+ var useEffect = React.useEffect;
+
+ var antd = window.antd;
+ var Table = antd.Table;
+ var Select = antd.Select;
+ var DatePicker = antd.DatePicker;
+ var Cascader = antd.Cascader;
+ var Tag = antd.Tag;
+ var Modal = antd.Modal;
+ var message = antd.message;
+ var Spin = antd.Spin;
+ var dayjs = h2WeeklyEnsureDayjs();
+
+ var today = dayjs ? dayjs() : null;
+ var defaultWeek = today ? h2WeeklyStartOfWeek(today) : null;
+
+ var weekState = useState(defaultWeek);
+ var weekValue = weekState[0];
+ var setWeekValue = weekState[1];
+
+ var typeState = useState('全部');
+ var stationType = typeState[0];
+ var setStationType = typeState[1];
+
+ var appliedWeekState = useState(defaultWeek);
+ var appliedWeek = appliedWeekState[0];
+ var setAppliedWeek = appliedWeekState[1];
+
+ var appliedTypeState = useState('全部');
+ var appliedType = appliedTypeState[0];
+ var setAppliedType = appliedTypeState[1];
+
+ var regionState = useState(undefined);
+ var region = regionState[0];
+ var setRegion = regionState[1];
+
+ var appliedRegionState = useState(undefined);
+ var appliedRegion = appliedRegionState[0];
+ var setAppliedRegion = appliedRegionState[1];
+
+ var loadingState = useState(true);
+ var loading = loadingState[0];
+ var setLoading = loadingState[1];
+
+ var pageState = useState(1);
+ var page = pageState[0];
+ var setPage = pageState[1];
+
+ var pageSizeState = useState(20);
+ var pageSize = pageSizeState[0];
+ var setPageSize = pageSizeState[1];
+
+ var viewModalState = useState({ open: false, viewRecord: null });
+ var viewModal = viewModalState[0];
+ var setViewModal = viewModalState[1];
+
+ var trendModalState = useState(false);
+ var trendModalOpen = trendModalState[0];
+ var setTrendModalOpen = trendModalState[1];
+
+ var reqDocState = useState(false);
+ var reqDocOpen = reqDocState[0];
+ var setReqDocOpen = reqDocState[1];
+
+ var simulateLoad = useCallback(function (nextWeek, nextType, nextRegion) {
+ setLoading(true);
+ window.setTimeout(function () {
+ setAppliedWeek(nextWeek);
+ setAppliedType(nextType);
+ setAppliedRegion(nextRegion);
+ setPage(1);
+ setLoading(false);
+ }, 320);
+ }, []);
+
+ useEffect(function () {
+ simulateLoad(defaultWeek, '全部', undefined);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ var mockPayload = useMemo(function () {
+ return h2WeeklyPickMock(appliedWeek);
+ }, [appliedWeek]);
+
+ var filteredList = useMemo(function () {
+ var list = mockPayload.list || [];
+ if (appliedType === '签约') list = list.filter(function (r) { return r.stationType === '签约'; });
+ else if (appliedType === '普通') list = list.filter(function (r) { return r.stationType === '普通'; });
+ if (appliedRegion && appliedRegion.length) {
+ list = list.filter(function (r) { return h2WeeklyMatchRegion(r, appliedRegion); });
+ }
+ return list;
+ }, [mockPayload, appliedType, appliedRegion]);
+
+ var displaySummary = useMemo(function () {
+ var s = mockPayload.summary;
+ if (appliedType === '全部') return s;
+ var list = filteredList;
+ var coop = list.filter(function (r) { return r.stationType === '签约'; }).length;
+ var normal = list.filter(function (r) { return r.stationType === '普通'; }).length;
+ return Object.assign({}, s, {
+ newTotal: list.length,
+ newCooperative: coop,
+ newNormal: normal
+ });
+ }, [mockPayload, filteredList, appliedType]);
+
+ var pagedList = useMemo(function () {
+ var start = (page - 1) * pageSize;
+ return filteredList.slice(start, start + pageSize).map(function (row, idx) {
+ return Object.assign({}, row, { _seq: start + idx + 1 });
+ });
+ }, [filteredList, page, pageSize]);
+
+ var totalCount = filteredList.length;
+ var totalPages = Math.max(1, Math.ceil(totalCount / pageSize) || 1);
+ var safePage = Math.min(page, totalPages);
+
+ var handleQuery = useCallback(function () {
+ simulateLoad(weekValue, stationType, region);
+ }, [weekValue, stationType, region, simulateLoad]);
+
+ var handleReset = useCallback(function () {
+ var nowWeek = dayjs ? h2WeeklyStartOfWeek(dayjs()) : defaultWeek;
+ setWeekValue(nowWeek);
+ setStationType('全部');
+ setRegion(undefined);
+ simulateLoad(nowWeek, '全部', undefined);
+ }, [dayjs, defaultWeek, simulateLoad]);
+
+ var jumpToThisWeek = useCallback(function () {
+ var nowWeek = dayjs ? h2WeeklyStartOfWeek(dayjs()) : defaultWeek;
+ setWeekValue(nowWeek);
+ simulateLoad(nowWeek, stationType, region);
+ }, [dayjs, defaultWeek, stationType, region, simulateLoad]);
+
+ var jumpToLastWeek = useCallback(function () {
+ var last = dayjs ? h2WeeklyStartOfWeek(dayjs().subtract(7, 'day')) : defaultWeek;
+ setWeekValue(last);
+ simulateLoad(last, stationType, region);
+ }, [dayjs, defaultWeek, stationType, region, simulateLoad]);
+
+ var handleExportExcel = useCallback(function () {
+ h2WeeklyDownloadCsv(appliedWeek, displaySummary, filteredList);
+ message.success('已导出 Excel(CSV)');
+ }, [appliedWeek, displaySummary, filteredList, message]);
+
+ var openTrendModal = useCallback(function () {
+ setTrendModalOpen(true);
+ }, []);
+
+ var closeTrendModal = useCallback(function () {
+ setTrendModalOpen(false);
+ }, []);
+
+ var openDetail = useCallback(function (record) {
+ setViewModal({ open: true, viewRecord: h2WeeklyResolveStationViewRecord(record) });
+ }, []);
+
+ var closeDetail = useCallback(function () {
+ setViewModal({ open: false, viewRecord: null });
+ }, []);
+
+ var renderFilterField = useCallback(function (label, control) {
+ return React.createElement('label', { className: 'vm-filter-field' },
+ React.createElement('span', null, label),
+ control
+ );
+ }, []);
+
+ var renderKpiCard = useCallback(function (meta, value, subText) {
+ return React.createElement('div', {
+ key: meta.key,
+ className: 'vm-kpi-card',
+ role: 'group',
+ 'aria-label': meta.title + ',' + value
+ },
+ React.createElement('span', {
+ className: 'vm-kpi-tip',
+ tabIndex: 0,
+ role: 'note',
+ 'aria-label': meta.title + '说明'
+ },
+ h2WeeklyHelpIcon(),
+ React.createElement('span', { className: 'vm-kpi-tooltip', role: 'tooltip' }, meta.desc)
+ ),
+ React.createElement('span', { className: 'vm-kpi-main' },
+ React.createElement('span', { className: 'vm-kpi-eyebrow' }, meta.title),
+ React.createElement('span', { className: 'vm-kpi-val' }, value),
+ subText ? React.createElement('span', { className: 'h2-weekly-kpi-sub' }, subText) : null
+ )
+ );
+ }, []);
+
+ var columns = useMemo(function () {
+ return [
+ {
+ title: '序',
+ dataIndex: '_seq',
+ key: 'seq',
+ width: 64,
+ align: 'center'
+ },
+ {
+ title: '省/市',
+ key: 'region',
+ width: 160,
+ render: function (_text, record) {
+ return h2WeeklyFormatRegion(record);
+ }
+ },
+ {
+ title: '加氢站名称',
+ dataIndex: 'stationName',
+ key: 'stationName',
+ ellipsis: true,
+ render: function (text, record) {
+ return React.createElement('button', {
+ type: 'button',
+ className: 'vm-link',
+ style: { border: 'none', background: 'none', padding: 0, cursor: 'pointer' },
+ onClick: function () { openDetail(record); }
+ }, text);
+ }
+ },
+ {
+ title: '站点类型',
+ dataIndex: 'stationType',
+ key: 'stationType',
+ width: 100,
+ render: function (type) {
+ var isSigned = type === '签约';
+ return React.createElement(Tag, {
+ className: 'h2-weekly-type-tag',
+ color: isSigned ? 'success' : 'default'
+ }, isSigned ? '签约站点' : '普通站点');
+ }
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'entryAt',
+ key: 'entryAt',
+ width: 180,
+ render: function (v) {
+ return React.createElement('span', {
+ style: { fontVariantNumeric: 'tabular-nums', fontFamily: 'var(--vm-font-mono)' }
+ }, h2WeeklyFormatCreatedAt(v));
+ }
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 148,
+ fixed: 'right',
+ render: function (_v, record) {
+ return React.createElement(OperationActions, {
+ view: { onClick: function () { openDetail(record); } }
+ });
+ }
+ }
+ ];
+ }, [openDetail]);
+
+ var summaryPeriod = useMemo(function () {
+ return h2WeeklyResolveSummaryPeriod(appliedWeek);
+ }, [appliedWeek]);
+
+ var periodCopy = useMemo(function () {
+ return h2WeeklyGetPeriodCopy(summaryPeriod);
+ }, [summaryPeriod]);
+
+ var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } },
+ React.createElement('div', { style: { color: '#94a3b8', fontSize: 13 } }, periodCopy.emptyText)
+ );
+
+ var summaryText = displaySummary.newTotal > 0
+ ? React.createElement(React.Fragment, null,
+ h2WeeklyRenderSummaryPeriodLead(summaryPeriod),
+ summaryPeriod.type === 'other' ? ' ' : '',
+ '新增签约站点 ',
+ React.createElement('strong', null, displaySummary.newCooperative), ' 个、普通站点 ',
+ React.createElement('strong', null, displaySummary.newNormal), ' 个;截至' + periodCopy.weekendLabel + ',全国加氢站 ',
+ React.createElement('strong', null, displaySummary.totalStations), ' 个,其中签约 ',
+ React.createElement('strong', null, displaySummary.cooperativeStations), ' 个、普通 ',
+ React.createElement('strong', null, displaySummary.normalStations), ' 个。'
+ )
+ : React.createElement(React.Fragment, null,
+ h2WeeklyRenderSummaryPeriodLead(summaryPeriod),
+ summaryPeriod.type === 'other' ? ' ' : '',
+ '无新增站点;截至' + periodCopy.weekendLabel + ',全国加氢站 ',
+ React.createElement('strong', null, displaySummary.totalStations), ' 个,其中签约 ',
+ React.createElement('strong', null, displaySummary.cooperativeStations), ' 个、普通 ',
+ React.createElement('strong', null, displaySummary.normalStations), ' 个。'
+ );
+
+
+ return React.createElement('div', { className: 'vm-page lc-page h2-site-page h2-site-list-shell h2-weekly-page' },
+ React.createElement('style', null, H2_WEEKLY_PAGE_STYLE),
+
+ React.createElement('div', { className: 'h2-weekly-page-top' },
+ React.createElement('button', {
+ type: 'button',
+ className: 'h2-weekly-page-top__link',
+ onClick: function () { setReqDocOpen(true); }
+ }, '查看需求文档')
+ ),
+
+ React.createElement('section', {
+ className: 'vm-filter-card',
+ 'aria-label': '筛选条件'
+ },
+ React.createElement('header', { className: 'vm-filter-header' },
+ React.createElement('h2', { className: 'vm-filter-title' }, '筛选条件')
+ ),
+ React.createElement('div', { className: 'vm-filter-grid' },
+ renderFilterField('统计周', React.createElement('div', null,
+ React.createElement(DatePicker, {
+ picker: 'week',
+ value: weekValue || undefined,
+ allowClear: false,
+ placeholder: '选择自然周',
+ style: { width: '100%' },
+ onChange: function (v) {
+ var nextWeek = v ? h2WeeklyStartOfWeek(v) : defaultWeek;
+ setWeekValue(nextWeek);
+ simulateLoad(nextWeek, stationType, region);
+ },
+ format: function (val) {
+ return h2WeeklyFormatRange(val);
+ }
+ }),
+ React.createElement('div', { className: 'h2-weekly-week-hint' },
+ '自然周:周一 00:00 ~ 周日 23:59(Asia/Shanghai)'
+ )
+ )),
+ renderFilterField('快捷周', React.createElement('div', { className: 'h2-weekly-quick' },
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-ghost ldb-toolbar-btn',
+ onClick: jumpToThisWeek
+ }, '本周'),
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-ghost ldb-toolbar-btn',
+ onClick: jumpToLastWeek
+ }, '上周')
+ )),
+ renderFilterField('站点类型', React.createElement(Select, {
+ value: stationType,
+ onChange: setStationType,
+ options: [
+ { value: '全部', label: '全部' },
+ { value: '签约', label: '签约站点' },
+ { value: '普通', label: '普通站点' }
+ ],
+ style: { width: '100%' }
+ })),
+ renderFilterField('地区(省-市)', React.createElement(Cascader, {
+ options: H2_WEEKLY_REGION_CASCADER_OPTIONS,
+ value: region && region.length ? region : undefined,
+ onChange: function (v) { setRegion(v || undefined); },
+ placeholder: '请选择省 / 市',
+ allowClear: true,
+ style: { width: '100%' },
+ changeOnSelect: true
+ }))
+ ),
+ React.createElement('div', { className: 'vm-filter-actions' },
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-ghost ldb-toolbar-btn',
+ 'data-vm-icon': 'rotate-ccw',
+ onClick: handleReset
+ }, '重置'),
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-primary ldb-toolbar-btn',
+ 'data-vm-icon': 'search',
+ onClick: handleQuery
+ }, '查询')
+ )
+ ),
+
+ React.createElement(Spin, { spinning: loading },
+ React.createElement('div', { className: 'h2-weekly-body' },
+ React.createElement('div', {
+ className: 'vm-kpi-row',
+ role: 'group',
+ 'aria-label': '周报统计'
+ },
+ renderKpiCard(
+ Object.assign({}, H2_WEEKLY_KPI_META[0], { title: periodCopy.newLabel }),
+ displaySummary.newTotal,
+ '签约 ' + displaySummary.newCooperative + ' · 普通 ' + displaySummary.newNormal
+ ),
+ renderKpiCard(H2_WEEKLY_KPI_META[1], displaySummary.totalStations, '截至所选周末'),
+ renderKpiCard(H2_WEEKLY_KPI_META[2], displaySummary.cooperativeStations, '合作加氢站'),
+ renderKpiCard(H2_WEEKLY_KPI_META[3], displaySummary.normalStations, '非合作加氢站')
+ ),
+
+ React.createElement('div', {
+ className: 'h2-weekly-summary' + (displaySummary.newTotal === 0 ? ' h2-weekly-summary--empty' : ''),
+ role: 'status'
+ }, summaryText),
+
+ React.createElement('section', { className: 'vm-table-section' },
+ React.createElement('div', { className: 'vm-table-toolbar' },
+ React.createElement('div', { className: 'vm-table-actions' },
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-ghost ldb-toolbar-btn',
+ 'data-vm-icon': 'bar-chart-2',
+ onClick: openTrendModal
+ }, '趋势展示'),
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-primary ldb-toolbar-btn',
+ 'data-vm-icon': 'download',
+ onClick: handleExportExcel
+ }, '导出 Excel')
+ )
+ ),
+ React.createElement('div', { className: 'vm-table-card' },
+ React.createElement('div', { className: 'vm-table-wrap' },
+ React.createElement(Table, {
+ className: 'vm-list-table',
+ rowKey: 'id',
+ columns: columns,
+ dataSource: pagedList,
+ pagination: false,
+ size: 'small',
+ tableLayout: 'fixed',
+ scroll: { x: 820 },
+ locale: { emptyText: emptyNode }
+ })
+ ),
+ React.createElement('div', { className: 'vm-table-footer' },
+ React.createElement(TablePagination, {
+ page: safePage,
+ pageSize: pageSize,
+ total: totalCount,
+ pageSizeOptions: COMPACT_PAGE_SIZE_OPTIONS,
+ onPageChange: setPage,
+ onPageSizeChange: function (size) {
+ setPageSize(size);
+ setPage(1);
+ }
+ })
+ )
+ )
+ ),
+
+ React.createElement('p', { className: 'h2-weekly-footnote' },
+ '统计口径:该周新增按站点「创建时间」落入所选自然周统计;累计为截至该周日 23:59:59 仍在册的有效站点。数据来源:加氢站管理 → 站点信息。'
+ )
+ )
+ ),
+
+ React.createElement(Modal, {
+ className: 'h2-weekly-trend-modal',
+ title: '趋势展示',
+ open: trendModalOpen,
+ onCancel: closeTrendModal,
+ footer: React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-ghost',
+ onClick: closeTrendModal
+ }, '关闭'),
+ centered: true,
+ width: 920,
+ destroyOnClose: true
+ }, h2WeeklyRenderTrendPanel()),
+
+ React.createElement(Modal, {
+ title: '站点周报统计 — 需求文档',
+ open: reqDocOpen,
+ onCancel: function () { setReqDocOpen(false); },
+ footer: React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-primary',
+ onClick: function () { setReqDocOpen(false); }
+ }, '知道了'),
+ centered: true,
+ width: 840,
+ destroyOnClose: true,
+ styles: {
+ body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 }
+ }
+ }, h2WeeklyRenderRequirementDocPanel()),
+
+ React.createElement(H2StationViewModal, {
+ open: viewModal.open,
+ record: viewModal.viewRecord,
+ onClose: closeDetail
+ })
+ );
+};
+
+if (typeof window !== 'undefined') window.Component = Component;
+
+export default Component;
diff --git a/src/prototypes/oneos-web-h2-station/pages/站点周报统计-需求内容.js b/src/prototypes/oneos-web-h2-station/pages/站点周报统计-需求内容.js
new file mode 100644
index 0000000..f0e6356
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station/pages/站点周报统计-需求内容.js
@@ -0,0 +1,2 @@
+// 与 oneos-web-h2-station-weekly/.spec/requirements-prd.md 同步
+export var H2_WEEKLY_REQUIREMENT_DOC = "# 加氢站管理 · 站点周报统计 — 产品需求文档(PRD)\r\n\r\n| 项 | 内容 |\r\n|---|---|\r\n| 文档版本 | v1.0 |\r\n| 模块名称 | 加氢站管理 → 站点周报统计 |\r\n| 所属系统 | ONE-OS Web 端 |\r\n| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |\r\n| 交互原型 | `/prototypes/oneos-web-h2-station-weekly` |\r\n| 关联原型 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢站合包](/prototypes/oneos-web-h2-station) |\r\n| 设计规范 | `src/prototypes/vm-shared/DESIGN.md` |\r\n| 文档状态 | 已对齐原型与批注 |\r\n\r\n---\r\n\r\n## 1. 背景与目标\r\n\r\n### 1.1 业务背景\r\n\r\n采购与运营团队每周五需人工整理加氢站新增与累计数据,通过邮件发送周报。该流程耗时、口径易不一致,且难以回溯历史周数据。\r\n\r\n### 1.2 本期目标\r\n\r\n- 提供**自然周维度**的站点新增与累计统计,替代人工邮件周报主流程\r\n- 支持按周筛选、KPI 概览、邮件风格摘要、新增明细表、趋势图与 Excel 导出\r\n- 与「站点信息」主数据口径对齐(创建时间、站点类型、省/市等)\r\n\r\n### 1.3 用户角色\r\n\r\n| 角色 | 典型诉求 |\r\n|---|---|\r\n| 采购/运营 | 每周查看新增签约与普通站点,复制摘要用于沟通 |\r\n| 管理层 | 查看近 12 期趋势与全国累计 |\r\n| 数据分析 | 导出 Excel 做二次分析 |\r\n\r\n### 1.4 非目标(本期不做)\r\n\r\n- 邮件自动发送、PDF 排版导出\r\n- 后端接口联调(原型使用 Mock)\r\n- 站点主数据维护(仍在「站点信息」)\r\n\r\n---\r\n\r\n## 2. 信息架构\r\n\r\n```text\r\n站点周报统计\r\n├── 筛选区(统计周、快捷周、站点类型、地区)\r\n├── KPI 卡片(4 张)\r\n├── 周报摘要条\r\n├── 新增明细表 + 工具栏(趋势展示、导出 Excel)\r\n├── 趋势弹窗(双 Tab:每期新增 / 累计数量)\r\n└── 站点详情抽屉\r\n```\r\n\r\n---\r\n\r\n## 3. 筛选区\r\n\r\n与站点信息等模块一致的 **vm 筛选卡片** 布局。\r\n\r\n| 字段 | 控件 | 说明 |\r\n|---|---|---|\r\n| 统计周 | 周选择器 | 自然周:周一 00:00 ~ 周日 23:59(Asia/Shanghai);展示 `YYYY-MM-DD ~ YYYY-MM-DD` |\r\n| 快捷周 | 按钮组 | 「本周」「上周」一键切换并**立即查询** |\r\n| 站点类型 | 下拉 | 全部 / 签约站点 / 普通站点 |\r\n| 地区(省-市) | 级联 | 省、市两级,可只选省;`changeOnSelect` |\r\n\r\n**交互**:\r\n\r\n- **查询**:应用筛选,刷新 KPI、摘要、列表,回到第 1 页\r\n- **重置**:清空类型与地区,统计周回到本周并刷新\r\n- **手选统计周**:与快捷周一致,选择后**自动查询**(无需再点查询)\r\n\r\n---\r\n\r\n## 4. KPI 统计卡片\r\n\r\n四张 **vm-kpi-card**,只读展示(非点击筛选)。\r\n\r\n| 卡片 | 口径 | 动态标题 |\r\n|---|---|---|\r\n| 该周新增 | 所选自然周内创建的有效站点数 | 本周/上周/该周新增 |\r\n| 全国累计 | 截至所选周末仍在册的有效站点总数 | 固定 |\r\n| 签约站点 | 合作加氢站累计 | 固定 |\r\n| 普通站点 | 非合作加氢站累计 | 固定 |\r\n\r\n首卡副文案:`签约 N · 普通 N`。\r\n\r\nKPI 标题、表格空态、摘要尾部「截至本周末」等文案须**随所选统计周**切换(本周 / 上周 / 该周末)。\r\n\r\n---\r\n\r\n## 5. 周报摘要条\r\n\r\n邮件风格一句话汇总,位于 KPI 与表格之间。\r\n\r\n**周期前缀规则**:\r\n\r\n| 所选周 | 前缀示例 |\r\n|---|---|\r\n| 本周 | 本周(07/13-07/19) |\r\n| 上周 | 上周(07/06-07/12) |\r\n| 其它历史周 | 仅日期区间 `05/26-06/01` |\r\n\r\n**有新增模板**:\r\n\r\n> {周期前缀}新增签约站点 **N** 个、普通站点 **N** 个;截至{本周末/上周末/该周末},全国加氢站 **N** 个,其中签约 **N** 个、普通 **N** 个。\r\n\r\n**无新增模板**:\r\n\r\n> {周期前缀}无新增站点;截至{周末标签},全国加氢站 **N** 个……\r\n\r\n---\r\n\r\n## 6. 新增明细表\r\n\r\n### 6.1 工具栏\r\n\r\n| 按钮 | 行为 |\r\n|---|---|\r\n| 趋势展示 | 打开近 12 期趋势弹窗 |\r\n| 导出 Excel | 导出当前筛选结果 CSV(UTF-8 BOM),文件名含统计周期 |\r\n\r\n**不包含**:导出 PDF(P1)按钮。\r\n\r\n### 6.2 列表字段\r\n\r\n| 列名 | 说明 |\r\n|---|---|\r\n| 序 | 当前页序号 |\r\n| 省/市 | 合并展示,格式 `湖北省-宜昌市` |\r\n| 加氢站名称 | 可点击查看详情 |\r\n| 站点类型 | 签约站点 / 普通站点 Tag |\r\n| 创建时间 | `YYYY-MM-DD HH:mm:ss` |\r\n| 操作 | `OperationActions` 组件,主操作「查看」 |\r\n\r\n### 6.3 空态\r\n\r\n随所选周动态:`本周无新增站点` / `上周无新增站点` / `{日期区间} 无新增站点`。\r\n\r\n### 6.4 分页\r\n\r\n默认 10 条,可选 5/10/20/50(`TablePagination` + `COMPACT_PAGE_SIZE_OPTIONS`)。\r\n\r\n---\r\n\r\n## 7. 趋势展示弹窗\r\n\r\n标题:**趋势展示**。副标题:近 12 期站点变化趋势。\r\n\r\n拆为 **两个 Tab**,避免单页图表过小:\r\n\r\n### Tab 1:每期新增站点\r\n\r\n柱状图三组:\r\n\r\n| 系列 | 颜色 | 说明 |\r\n|---|---|---|\r\n| 新增收录站点数 | 蓝色 | 该期全部新增 |\r\n| 新增合作站点数 | 绿色 | 该期签约新增 |\r\n| 新增普通站点数 | 橙色 | 收录 − 合作 |\r\n\r\n数据表列:期次、周期、新增收录、新增合作、新增普通。\r\n\r\n### Tab 2:累计站点数量\r\n\r\n折线图三条:\r\n\r\n| 系列 | 颜色 | 说明 |\r\n|---|---|---|\r\n| 签约站点累计数量 | 绿色 | |\r\n| 普通站点累计数量 | 橙色 | 全部 − 签约 |\r\n| 全部站点累计数量 | 紫色 | |\r\n\r\n数据表列:期次、周期、签约累计、普通累计、全部累计。\r\n\r\n**约束**:仅展示数量,不使用「增长良好」「环比下降」等评价标签。\r\n\r\n**可访问性**:图表下方提供数据表,供读屏与键盘用户查阅。\r\n\r\n---\r\n\r\n## 8. 站点详情抽屉\r\n\r\n字段与主表对齐:\r\n\r\n| 字段 | 格式 |\r\n|---|---|\r\n| 加氢站名称 | 文本 |\r\n| 省/市 | `省-市` |\r\n| 站点类型 | Tag |\r\n| 创建时间 | `YYYY-MM-DD HH:mm:ss` |\r\n\r\n---\r\n\r\n## 9. 统计口径\r\n\r\n| 指标 | 规则 |\r\n|---|---|\r\n| 该周新增 | 站点「创建时间」落入所选自然周的有效站点 |\r\n| 签约/普通新增 | 按站点类型拆分 |\r\n| 全国累计 | 截至该周日 23:59:59 仍在册的有效站点 |\r\n| 签约/普通累计 | 按类型统计截至周末的存量 |\r\n\r\n数据来源:加氢站管理 → 站点信息。\r\n\r\n---\r\n\r\n## 10. 设计规范\r\n\r\n- 页面壳层:`vm-page` + `lc-page` + `h2-weekly-page`\r\n- 主题色 `#32a06e`,字体 Inter / 苹方\r\n- 操作列使用 `OperationActions`\r\n- 分页使用 `TablePagination`\r\n- 不重复侧栏模块大标题\r\n\r\n---\r\n\r\n## 11. 验收标准\r\n\r\n### 11.1 筛选与 KPI\r\n\r\n- [ ] 统计周、快捷周、类型、地区筛选联动正确\r\n- [ ] 手选周与快捷周均自动加载数据\r\n- [ ] KPI / 空态 / 摘要文案随所选周切换\r\n\r\n### 11.2 表格与导出\r\n\r\n- [ ] 列名:省/市、加氢站名称、创建时间\r\n- [ ] 操作列使用 OperationActions\r\n- [ ] 导出 Excel 含摘要与明细\r\n\r\n### 11.3 趋势图\r\n\r\n- [ ] 双 Tab 展示\r\n- [ ] 含普通站点新增与累计\r\n- [ ] 无评价性标签\r\n- [ ] 附带数据表\r\n\r\n### 11.4 文档\r\n\r\n- [ ] 右上角可打开本 PRD\r\n\r\n---\r\n\r\n## 12. 修订记录\r\n\r\n| 版本 | 日期 | 说明 |\r\n|---|---|---|\r\n| v1.0 | 2026-07-13 | 初版:整合批注与 UI 评审意见,对齐现行原型 |\r\n\r\n**文档结束**\r\n";
diff --git a/src/prototypes/oneos-web-h2-station/shared/h2-station-view-shared.jsx b/src/prototypes/oneos-web-h2-station/shared/h2-station-view-shared.jsx
new file mode 100644
index 0000000..832e49a
--- /dev/null
+++ b/src/prototypes/oneos-web-h2-station/shared/h2-station-view-shared.jsx
@@ -0,0 +1,511 @@
+/**
+ * 加氢站「查看加氢站点」详情弹窗 — 与站点信息页保持一致
+ * 供站点周报统计等模块复用
+ */
+
+import React, { useCallback, useState } from 'react';
+
+export var H2_STATION_PRIMARY_BTN_STYLE = {
+ borderRadius: 8,
+ fontWeight: 600,
+ background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
+ border: 'none'
+};
+
+export var H2_STATION_VIEW_MODAL_STYLES = [
+ '.h2-station-view-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }',
+ '.h2-station-view-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }',
+ '.h2-station-view-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }',
+ '.h2-station-view-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }',
+ '.h2-station-view-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }',
+ '.h2-station-view-modal .h2-station-view-panel { display: flex; flex-direction: column; gap: 20px; }',
+ '.h2-station-view-modal .h2-station-view-section__title { margin: 0 0 10px; padding-left: 10px; font-size: 14px; font-weight: 700; color: #334155; line-height: 1.35; border-left: 4px solid #10b981; }',
+ '.h2-station-view-modal .h2-station-view-section .ant-descriptions { background: #fff; border-radius: 10px; overflow: hidden; }',
+ '.h2-station-view-modal .h2-station-view-section .ant-descriptions-item-label { width: 132px !important; font-size: 12px !important; font-weight: 600 !important; color: #64748b !important; background: #fafbfc !important; }',
+ '.h2-station-view-modal .h2-station-view-section .ant-descriptions-item-content { font-size: 13px !important; color: #0f172a !important; font-weight: 500 !important; word-break: break-word; }',
+ '.h2-station-view-modal .h2-station-view-files { display: flex; flex-wrap: wrap; gap: 8px; }',
+ '.h2-station-view-modal .h2-station-view-file-link { display: inline-flex; align-items: center; max-width: 100%; padding: 4px 10px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; color: #059669; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease; }',
+ '.h2-station-view-modal .h2-station-view-file-link:hover { background: #ecfdf5; border-color: #86efac; }',
+ '.h2-station-view-modal .h2-station-view-license-count { font-size: 13px; color: #64748b; margin-bottom: 8px; }',
+ '.h2-station-view-modal .h2-station-view-license-images { display: flex; flex-wrap: wrap; gap: 10px; }',
+ '.h2-station-view-modal .h2-station-view-license-thumb { display: block; padding: 0; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #f8fafc; cursor: pointer; }',
+ '.h2-station-view-modal .h2-station-view-license-thumb img { display: block; width: 168px; height: 118px; object-fit: cover; }',
+ '.h2-station-view-modal .h2-station-view-license-empty { font-size: 13px; color: #94a3b8; }',
+ '.h2-station-view-modal .h2-station-view-empty { padding: 20px 16px; text-align: center; font-size: 13px; color: #94a3b8; background: #fff; border: 1px dashed #e2e8f0; border-radius: 10px; }',
+ '.h2-file-preview-modal .h2-file-preview-body iframe { width: 100%; height: 72vh; border: none; border-radius: 8px; }',
+ '.h2-file-preview-modal .h2-file-preview-body img { display: block; max-width: 100%; margin: 0 auto; border-radius: 8px; }'
+];
+
+var H2_WEEKLY_VIEW_SUPPLIERS = {
+ 'yc-zj': {
+ id: 'sup-weekly-yc',
+ name: '宜昌氢能运营有限公司',
+ type: '加氢站',
+ city: ['湖北省', '宜昌市'],
+ address: '枝江市仙女镇服务区能源路18号',
+ region: '华中',
+ dept: '能源部',
+ manager: '刘经理',
+ contactName: '王供',
+ contactMobile: '13807171234',
+ taxId: '91420500MA4YCZJ001',
+ invoiceAddress: '湖北省宜昌市枝江市仙女镇服务区能源路18号',
+ invoicePhone: '0717-6666888',
+ bankName: '中国工商银行宜昌枝江支行',
+ bankAccount: '6222021203005566778',
+ businessAddress: '湖北省宜昌市枝江市仙女镇服务区能源路18号',
+ businessLicenseFiles: [{ uid: 'bl-yc', name: '营业执照_宜昌氢能.pdf' }],
+ fillingLicenseFiles: [{ uid: 'fl-yc', name: '加氢站经营许可证.pdf' }]
+ },
+ 'zy-lk': {
+ id: 'sup-weekly-zy',
+ name: '资阳临空氢能科技有限公司',
+ type: '加氢站',
+ city: ['四川省', '资阳市'],
+ address: '临空经济区产业大道128号',
+ region: '西南',
+ dept: '能源部',
+ manager: '陈经理',
+ contactName: '李供',
+ contactMobile: '13808328888',
+ taxId: '91512000MA62ZYLK01',
+ invoiceAddress: '四川省资阳市临空经济区产业大道128号',
+ invoicePhone: '028-88886666',
+ bankName: '中国建设银行资阳临空支行',
+ bankAccount: '6217001234567890123',
+ businessAddress: '四川省资阳市临空经济区产业大道128号',
+ businessLicenseFiles: [{ uid: 'bl-zy', name: '营业执照_资阳临空.pdf' }],
+ fillingLicenseFiles: [{ uid: 'fl-zy', name: '危化品经营许可证.pdf' }]
+ },
+ 'hs-dev': {
+ id: 'sup-weekly-hs',
+ name: '黄石开发区氢能供应有限公司',
+ type: '加氢站',
+ city: ['湖北省', '黄石市'],
+ address: '黄石经济技术开发区金山大道66号',
+ region: '华中',
+ dept: '能源部',
+ manager: '赵经理',
+ contactName: '周供',
+ contactMobile: '13807159876',
+ taxId: '91420200MA4HSDEV01',
+ invoiceAddress: '湖北省黄石市经济技术开发区金山大道66号',
+ invoicePhone: '0714-55556666',
+ bankName: '中国银行黄石开发区支行',
+ bankAccount: '6216600100001122334',
+ businessAddress: '湖北省黄石市经济技术开发区金山大道66号',
+ businessLicenseFiles: [{ uid: 'bl-hs', name: '营业执照_黄石开发区.pdf' }],
+ fillingLicenseFiles: [{ uid: 'fl-hs', name: '加氢站备案证明.pdf' }]
+ },
+ 'sh-fx': {
+ id: 'sup-weekly-fx',
+ name: '上海奉贤海湾氢能有限公司',
+ type: '加氢站',
+ city: ['上海市', '奉贤区'],
+ address: '奉贤区海湾镇海泉路188号',
+ region: '华东',
+ dept: '能源部',
+ manager: '张经理',
+ contactName: '孙供',
+ contactMobile: '13800138066',
+ taxId: '91310120MA1FXHAI01',
+ invoiceAddress: '上海市奉贤区海湾镇海泉路188号',
+ invoicePhone: '021-66889900',
+ bankName: '招商银行上海奉贤支行',
+ bankAccount: '6214830210987654321',
+ businessAddress: '上海市奉贤区海湾镇海泉路188号',
+ businessLicenseFiles: [{ uid: 'bl-fx', name: '营业执照_奉贤海湾.pdf' }],
+ fillingLicenseFiles: [
+ { uid: 'fl-fx-1', name: '加氢站经营许可证.pdf' },
+ { uid: 'fl-fx-2', name: '消防验收合格证.pdf' }
+ ]
+ }
+};
+
+var H2_WEEKLY_VIEW_STATION_CATALOG = {
+ '宜昌中石化枝江服务区加氢站': {
+ name: '宜昌中石化枝江服务区加氢站',
+ region: ['湖北省', '宜昌市'],
+ addressDetail: '枝江市仙女镇中石化枝江服务区',
+ isSigned: true,
+ contractStart: '2025-04-01',
+ contractEnd: '2027-03-31',
+ contractFiles: [{ uid: 'c-yc', name: '枝江服务区加氢站签约合同.pdf' }],
+ businessStatus: '营业中',
+ businessHours: '08:00-22:00',
+ prepaidBalance: 62800.0,
+ costUnitPrice: 43.2,
+ contact: '王供',
+ phone: '13807171234',
+ supplierKey: 'yc-zj'
+ },
+ '资阳临空经济区加氢站': {
+ name: '资阳临空经济区加氢站',
+ region: ['四川省', '资阳市'],
+ addressDetail: '临空经济区产业大道128号',
+ isSigned: true,
+ contractStart: '2025-06-01',
+ contractEnd: '2027-05-31',
+ contractFiles: [{ uid: 'c-zy', name: '资阳临空加氢站合作协议.pdf' }],
+ businessStatus: '营业中',
+ businessHours: '08:00-20:00',
+ prepaidBalance: 45200.0,
+ costUnitPrice: 44.0,
+ contact: '李供',
+ phone: '13808328888',
+ supplierKey: 'zy-lk'
+ },
+ '黄石开发区加氢站': {
+ name: '黄石开发区加氢站',
+ region: ['湖北省', '黄石市'],
+ addressDetail: '黄石经济技术开发区金山大道66号',
+ isSigned: true,
+ contractStart: '2024-11-01',
+ contractEnd: '2026-10-31',
+ contractFiles: [{ uid: 'c-hs', name: '黄石开发区加氢站合同.pdf' }],
+ businessStatus: '营业中',
+ businessHours: '09:00-21:00',
+ prepaidBalance: 38500.0,
+ costUnitPrice: 42.8,
+ contact: '周供',
+ phone: '13807159876',
+ supplierKey: 'hs-dev'
+ },
+ '上海奉贤海湾加氢站': {
+ name: '上海奉贤海湾加氢站',
+ region: ['上海市', '奉贤区'],
+ addressDetail: '奉贤区海湾镇海泉路188号',
+ isSigned: true,
+ contractStart: '2025-01-01',
+ contractEnd: '2026-12-31',
+ contractFiles: [{ uid: 'c-fx', name: '奉贤海湾加氢站签约合同.pdf' }],
+ businessStatus: '营业中',
+ businessHours: '08:00-22:00',
+ prepaidBalance: 72100.0,
+ costUnitPrice: 45.5,
+ contact: '孙供',
+ phone: '13800138066',
+ supplierKey: 'sh-fx'
+ }
+};
+
+function h2ViewFormatRegion(region) {
+ if (!region || !region.length) return '—';
+ return region.join('-');
+}
+
+function h2ViewNumOrZero(v) {
+ var n = typeof v === 'number' ? v : parseFloat(v);
+ return isNaN(n) ? 0 : n;
+}
+
+function h2ViewFormatYuanSymbol(v, options) {
+ var opts = options || {};
+ if (v == null || v === '') return '—';
+ if (!opts.keepZero && Number(v) === 0) return '—';
+ var n = typeof v === 'number' ? v : parseFloat(v);
+ if (isNaN(n)) return '—';
+ return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
+function h2ViewDisplayBusinessHours(str) {
+ var s = String(str || '').trim();
+ if (!s || s === '—') return '—';
+ return s;
+}
+
+function h2ViewDaysUntilContractEnd(dateStr) {
+ if (!dateStr) return null;
+ var parts = String(dateStr).trim().split('-');
+ if (parts.length < 3) return null;
+ var y = parseInt(parts[0], 10);
+ var m = parseInt(parts[1], 10) - 1;
+ var d = parseInt(parts[2], 10);
+ if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
+ var end = new Date(y, m, d, 23, 59, 59, 999);
+ var today = new Date();
+ today.setHours(0, 0, 0, 0);
+ return Math.ceil((end.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
+}
+
+function h2ViewRenderContractRemainTag(contractEnd) {
+ var days = h2ViewDaysUntilContractEnd(contractEnd);
+ if (days == null) return null;
+ var Tag = window.antd && window.antd.Tag;
+ if (!Tag) return null;
+ var tagProps = { style: { marginTop: 4, borderRadius: 6, fontWeight: 600, fontSize: 11 } };
+ if (days < 0) {
+ return React.createElement(Tag, Object.assign({}, tagProps, { color: 'error' }), '已过期 ' + Math.abs(days) + ' 天');
+ }
+ if (days <= 30) {
+ return React.createElement(Tag, Object.assign({}, tagProps, { color: 'warning' }), '剩余 ' + days + ' 天');
+ }
+ return React.createElement(Tag, Object.assign({}, tagProps, { color: 'success' }), '剩余 ' + days + ' 天');
+}
+
+function h2ViewBuildPrototypeDocPreviewSvg(title, variant) {
+ var accent = variant === 'license' ? '#10b981' : '#3b82f6';
+ var label = String(title || '附件').slice(0, 28);
+ var svg = '';
+ return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
+}
+
+function h2ViewResolveLicenseDisplayUrl(file) {
+ return h2ViewBuildPrototypeDocPreviewSvg(file && file.name, 'license');
+}
+
+function h2ViewResolveUploadPreview(file, variant) {
+ var name = (file && file.name) || '附件';
+ return {
+ url: h2ViewBuildPrototypeDocPreviewSvg(name, variant || 'doc'),
+ name: name,
+ type: 'image'
+ };
+}
+
+function h2ViewResolveStationSupplier(record) {
+ if (!record) return null;
+ if (record.supplier) return Object.assign({ type: '加氢站' }, record.supplier);
+ if (record.supplierKey && H2_WEEKLY_VIEW_SUPPLIERS[record.supplierKey]) {
+ return H2_WEEKLY_VIEW_SUPPLIERS[record.supplierKey];
+ }
+ return null;
+}
+
+function h2ViewResolveCurrentCostPrice(record) {
+ if (record && record.costUnitPrice != null) return record.costUnitPrice;
+ return null;
+}
+
+function h2ViewRenderBusinessStatusTag(v) {
+ var Tag = window.antd && window.antd.Tag;
+ if (!Tag) return v || '—';
+ if (v === '营业中') return React.createElement(Tag, { color: 'success', style: { borderRadius: 6, fontWeight: 600 } }, v);
+ if (v === '暂停营业') return React.createElement(Tag, { color: 'warning', style: { borderRadius: 6, fontWeight: 600 } }, v);
+ if (v === '停止营业') return React.createElement(Tag, { color: 'error', style: { borderRadius: 6, fontWeight: 600 } }, v);
+ return React.createElement(Tag, { style: { borderRadius: 6, fontWeight: 600, color: '#64748b', background: '#f1f5f9', border: '1px solid #e2e8f0' } }, v || '—');
+}
+
+/** 将周报列表行解析为站点信息同款详情记录 */
+export function h2WeeklyResolveStationViewRecord(weeklyRow) {
+ if (!weeklyRow) return null;
+ var catalog = H2_WEEKLY_VIEW_STATION_CATALOG[weeklyRow.stationName];
+ if (catalog) return Object.assign({}, catalog);
+ return {
+ name: weeklyRow.stationName,
+ region: [weeklyRow.province, weeklyRow.city].filter(Boolean),
+ addressDetail: '',
+ isSigned: weeklyRow.stationType === '签约',
+ contractStart: '',
+ contractEnd: '',
+ contractFiles: [],
+ businessStatus: '营业中',
+ businessHours: '—',
+ prepaidBalance: 0,
+ costUnitPrice: null,
+ contact: '—',
+ phone: '—'
+ };
+}
+
+export function H2StationViewPanel(props) {
+ var record = props.record;
+ var onOpenFilePreview = props.onOpenFilePreview;
+ var Descriptions = window.antd && window.antd.Descriptions;
+ if (!record || !Descriptions) return null;
+
+ var supplier = h2ViewResolveStationSupplier(record);
+ var descColumn = { xs: 1, sm: 1, md: 2, lg: 2, xl: 2, xxl: 2 };
+ var price = h2ViewResolveCurrentCostPrice(record);
+ var balanceNegative = h2ViewNumOrZero(record.prepaidBalance) < 0;
+ var balanceText = h2ViewFormatYuanSymbol(record.prepaidBalance, { keepZero: true });
+
+ var renderText = function (v) {
+ return v != null && v !== '' ? v : '—';
+ };
+
+ var renderViewContractFiles = function (files) {
+ if (!files || !files.length) return '—';
+ return React.createElement('div', { className: 'h2-station-view-files' },
+ files.map(function (f, idx) {
+ return React.createElement('button', {
+ key: f.uid || f.name || idx,
+ type: 'button',
+ className: 'h2-station-view-file-link',
+ onClick: function () { onOpenFilePreview(f, 'doc'); }
+ }, f.name || '附件');
+ })
+ );
+ };
+
+ var renderViewLicenseImages = function (files, opts) {
+ opts = opts || {};
+ if (!files || !files.length) {
+ return React.createElement('span', { className: 'h2-station-view-license-empty' }, '未上传');
+ }
+ return React.createElement('div', { className: 'h2-station-view-license-block' },
+ opts.showCount ? React.createElement('div', { className: 'h2-station-view-license-count' }, '共 ' + files.length + ' 张') : null,
+ React.createElement('div', { className: 'h2-station-view-license-images' },
+ files.map(function (f, idx) {
+ return React.createElement('button', {
+ key: f.uid || f.name || idx,
+ type: 'button',
+ className: 'h2-station-view-license-thumb',
+ onClick: function () { onOpenFilePreview(f, 'license'); }
+ }, React.createElement('img', { src: h2ViewResolveLicenseDisplayUrl(f), alt: f.name || '证照图片' }));
+ })
+ )
+ );
+ };
+
+ var renderViewSection = function (title, items) {
+ return React.createElement('div', { className: 'h2-station-view-section' },
+ React.createElement('div', { className: 'h2-station-view-section__title' }, title),
+ React.createElement(Descriptions, { bordered: true, size: 'small', column: descColumn },
+ items.map(function (item) {
+ return React.createElement(Descriptions.Item, {
+ key: item.key,
+ label: item.label,
+ span: item.span || 1
+ }, item.value);
+ })
+ )
+ );
+ };
+
+ var renderEmptySection = function (title, hint) {
+ return React.createElement('div', { className: 'h2-station-view-section' },
+ React.createElement('div', { className: 'h2-station-view-section__title' }, title),
+ React.createElement('div', { className: 'h2-station-view-empty' }, hint)
+ );
+ };
+
+ var contractValue = '—';
+ if (record.isSigned) {
+ contractValue = React.createElement('div', null,
+ React.createElement('span', null, (record.contractStart || '—') + ' 至 ' + (record.contractEnd || '—')),
+ record.contractEnd ? React.createElement('div', { style: { marginTop: 4 } }, h2ViewRenderContractRemainTag(record.contractEnd)) : null
+ );
+ }
+
+ var stationItems = [
+ { key: 'name', label: '加氢站名称', value: renderText(record.name) },
+ { key: 'region', label: '省 / 市', value: renderText(h2ViewFormatRegion(record.region)) },
+ { key: 'detail', label: '详细地址', value: renderText(record.addressDetail), span: 2 },
+ { key: 'status', label: '营业状态', value: h2ViewRenderBusinessStatusTag(record.businessStatus) },
+ { key: 'hours', label: '营业时间', value: renderText(h2ViewDisplayBusinessHours(record.businessHours)) },
+ { key: 'signed', label: '是否签约', value: record.isSigned ? '已签约' : '未签约' }
+ ];
+ if (record.isSigned) {
+ stationItems.push(
+ { key: 'contract', label: '合作时间', value: contractValue, span: 2 },
+ { key: 'files', label: '签约附件', value: renderViewContractFiles(record.contractFiles), span: 2 }
+ );
+ }
+ stationItems.push(
+ { key: 'contact', label: '联系人', value: renderText(record.contact) },
+ { key: 'phone', label: '联系电话', value: renderText(record.phone) },
+ {
+ key: 'balance',
+ label: '预付余额',
+ value: React.createElement('span', {
+ style: { fontWeight: 700, color: balanceNegative ? '#dc2626' : '#059669', fontVariantNumeric: 'tabular-nums' }
+ }, balanceText)
+ },
+ {
+ key: 'price',
+ label: '当前成本价格',
+ value: price != null ? h2ViewFormatYuanSymbol(price, { keepZero: true }) + ' / kg' : '—'
+ }
+ );
+
+ var supplierItems = supplier ? [
+ { key: 'name', label: '供应商名称', value: renderText(supplier.name), span: 2 },
+ { key: 'signingCompany', label: '签约公司', value: renderText(supplier.signingCompany), span: 2 },
+ { key: 'type', label: '供应商类型', value: renderText(supplier.type || '加氢站') },
+ { key: 'city', label: '省 / 市', value: renderText(h2ViewFormatRegion(supplier.city)) },
+ { key: 'address', label: '详细地址', value: renderText(supplier.address), span: 2 },
+ { key: 'region', label: '所属区域', value: renderText(supplier.region) },
+ { key: 'dept', label: '归属部门', value: renderText(supplier.dept) },
+ { key: 'manager', label: '负责人', value: renderText(supplier.manager) },
+ { key: 'contactName', label: '联系人', value: renderText(supplier.contactName) },
+ { key: 'contactMobile', label: '联系电话', value: renderText(supplier.contactMobile) },
+ { key: 'bl', label: '营业执照', value: renderViewLicenseImages(supplier.businessLicenseFiles), span: 2 },
+ { key: 'fl', label: '其他证照', value: renderViewLicenseImages(supplier.fillingLicenseFiles, { showCount: true }), span: 2 }
+ ] : [];
+
+ var paymentItems = supplier ? [
+ { key: 'taxId', label: '纳税人识别号', value: renderText(supplier.taxId), span: 2 },
+ { key: 'invoicePhone', label: '注册电话', value: renderText(supplier.invoicePhone) },
+ { key: 'invoiceAddress', label: '注册地址', value: renderText(supplier.invoiceAddress), span: 2 },
+ { key: 'bankName', label: '开户行', value: renderText(supplier.bankName), span: 2 },
+ { key: 'bankAccount', label: '银行账号', value: renderText(supplier.bankAccount), span: 2 },
+ { key: 'businessAddress', label: '营业地址', value: renderText(supplier.businessAddress), span: 2 }
+ ] : [];
+
+ return React.createElement('div', { className: 'h2-station-view-panel' },
+ renderViewSection('加氢站信息', stationItems),
+ supplier ? renderViewSection('供应商信息', supplierItems) : renderEmptySection('供应商信息', '暂未关联供应商'),
+ supplier ? renderViewSection('付款信息', paymentItems) : renderEmptySection('付款信息', '暂无付款信息')
+ );
+}
+
+export function H2StationViewModal(props) {
+ var open = props.open;
+ var record = props.record;
+ var onClose = props.onClose;
+ var Modal = window.antd && window.antd.Modal;
+ var Button = window.antd && window.antd.Button;
+ var Image = window.antd && window.antd.Image;
+ var filePreviewState = useState({ open: false, url: '', name: '', type: 'image' });
+ var filePreviewModal = filePreviewState[0];
+ var setFilePreviewModal = filePreviewState[1];
+
+ var openFilePreview = useCallback(function (file, variant) {
+ if (!file) return;
+ var preview = h2ViewResolveUploadPreview(file, variant || 'doc');
+ if (preview.type === 'image' && Image && typeof Image.preview === 'function') {
+ Image.preview({ src: preview.url });
+ return;
+ }
+ setFilePreviewModal({ open: true, url: preview.url, name: preview.name, type: preview.type });
+ }, [Image]);
+
+ var handleClose = useCallback(function () {
+ setFilePreviewModal({ open: false, url: '', name: '', type: 'image' });
+ if (onClose) onClose();
+ }, [onClose]);
+
+ if (!Modal || !Button) return null;
+
+ return React.createElement(React.Fragment, null,
+ React.createElement(Modal, {
+ className: 'h2-station-view-modal',
+ title: '查看加氢站点',
+ open: open,
+ onCancel: handleClose,
+ footer: React.createElement(Button, { type: 'primary', onClick: handleClose, style: H2_STATION_PRIMARY_BTN_STYLE }, '知道了'),
+ width: 920,
+ centered: true,
+ destroyOnClose: true,
+ styles: { body: { maxHeight: '78vh', overflow: 'auto' } }
+ }, React.createElement(H2StationViewPanel, { record: record, onOpenFilePreview: openFilePreview })),
+ React.createElement(Modal, {
+ className: 'h2-file-preview-modal',
+ title: filePreviewModal.name || '附件预览',
+ open: filePreviewModal.open,
+ onCancel: function () { setFilePreviewModal({ open: false, url: '', name: '', type: 'image' }); },
+ footer: React.createElement(Button, { type: 'primary', onClick: function () { setFilePreviewModal({ open: false, url: '', name: '', type: 'image' }); }, style: H2_STATION_PRIMARY_BTN_STYLE }, '关闭'),
+ width: 720,
+ centered: true,
+ destroyOnClose: true
+ }, React.createElement('div', { className: 'h2-file-preview-body' },
+ React.createElement('img', { src: filePreviewModal.url, alt: filePreviewModal.name || '附件预览' })
+ ))
+ );
+}