Compare commits

...

2 Commits

Author SHA1 Message Date
hsu06
84a25c42df merge: integrate remote main with local H2 prototypes and xll driver training nav.
Keep remote xll-miniapp registry while preserving local hydrogen station weekly/analysis pages; sync driver-training into ONEOS xll navigation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 13:41:20 +08:00
hsu06
7ca064f9f7 feat: add hydrogen station weekly report and analysis prototypes.
Register new H2 station pages in prototype nav and sidebar sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 12:03:44 +08:00
16 changed files with 4752 additions and 656 deletions

View File

@@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-07-18T02:30:01.518Z", "updatedAt": "2026-07-20T05:34:45.927Z",
"prototypes": [ "prototypes": [
{ {
"id": "folder-1783875936186-27raza", "id": "folder-1783875936186-27raza",
@@ -184,6 +184,18 @@
"kind": "item", "kind": "item",
"title": "加氢记录", "title": "加氢记录",
"itemKey": "prototypes/oneos-web-h2-station" "itemKey": "prototypes/oneos-web-h2-station"
},
{
"id": "item:prototypes:oneos-web-h2-station-weekly",
"kind": "item",
"title": "站点周报统计",
"itemKey": "prototypes/oneos-web-h2-station-weekly"
},
{
"id": "item-prototypes-oneos-web-h2-station-analysis",
"kind": "item",
"title": "oneos web h2 station analysis",
"itemKey": "prototypes/oneos-web-h2-station-analysis"
} }
] ]
}, },
@@ -1093,4 +1105,4 @@
"templates": [], "templates": [],
"components": [], "components": [],
"canvas": [] "canvas": []
} }

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env node
/**
* 合并 merge 冲突中的 prototype-registry.json 与 sidebar-tree.json保留两侧内容
* 用法node scripts/merge-registry-sidebar-conflicts.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
function gitShow(stage, filePath) {
return execSync(`git show :${stage}:${filePath}`, {
cwd: projectRoot,
encoding: 'utf8',
});
}
function readJsonFromGit(stage, relativePath) {
return JSON.parse(gitShow(stage, relativePath));
}
function writeJson(relativePath, value) {
const abs = path.join(projectRoot, relativePath);
fs.writeFileSync(abs, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function collectItemKeys(items, acc = new Set()) {
for (const item of items || []) {
if (item?.itemKey) acc.add(item.itemKey);
if (Array.isArray(item?.children)) collectItemKeys(item.children, acc);
}
return acc;
}
function findItemByKey(items, itemKey) {
for (const item of items || []) {
if (item?.itemKey === itemKey) return item;
if (Array.isArray(item?.children)) {
const found = findItemByKey(item.children, itemKey);
if (found) return found;
}
}
return null;
}
function findFolder(items, title) {
for (const item of items || []) {
if (item?.kind === 'folder' && item?.title === title) return item;
if (Array.isArray(item?.children)) {
const found = findFolder(item.children, title);
if (found) return found;
}
}
return null;
}
function ensureChildFolder(parent, folderTitle, folderId) {
parent.children = parent.children || [];
let folder = parent.children.find((c) => c.kind === 'folder' && c.title === folderTitle);
if (!folder) {
folder = {
id: folderId,
kind: 'folder',
title: folderTitle,
children: [],
};
parent.children.push(folder);
}
folder.children = folder.children || [];
return folder;
}
function ensureItem(folder, item) {
if (!item?.itemKey) return;
const exists = folder.children.some((c) => c.itemKey === item.itemKey);
if (!exists) folder.children.push(JSON.parse(JSON.stringify(item)));
}
function mergeRegistry() {
const relativePath = 'src/prototypes/oneos-prototype-nav/prototype-registry.json';
const ours = readJsonFromGit(2, relativePath);
const theirs = readJsonFromGit(3, relativePath);
const merged = {
version: 1,
updatedAt: new Date().toISOString(),
prototypes: { ...theirs.prototypes },
recentUpdates: [],
};
for (const key of ['oneos-web-h2-station-weekly', 'oneos-web-h2-station-analysis']) {
if (ours.prototypes[key]) merged.prototypes[key] = ours.prototypes[key];
}
const seen = new Set();
const combinedRecent = [...(ours.recentUpdates || []), ...(theirs.recentUpdates || [])];
for (const entry of combinedRecent) {
const sig = `${entry.prototypeId}|${entry.version}|${entry.date}|${entry.time}`;
if (seen.has(sig)) continue;
seen.add(sig);
merged.recentUpdates.push(entry);
}
merged.recentUpdates = merged.recentUpdates.slice(0, 40);
writeJson(relativePath, merged);
return {
prototypeCount: Object.keys(merged.prototypes).length,
recentCount: merged.recentUpdates.length,
hasXll: Boolean(merged.prototypes['xll-miniapp']),
h2AnalysisVersion: merged.prototypes['oneos-web-h2-station-analysis']?.version,
};
}
function mergeSidebar() {
const relativePath = '.axhub/make/sidebar-tree.json';
const ours = readJsonFromGit(2, relativePath);
const theirs = readJsonFromGit(3, relativePath);
const merged = JSON.parse(JSON.stringify(theirs));
merged.updatedAt = new Date().toISOString();
const oursKeys = collectItemKeys(ours.prototypes);
const mergedKeys = collectItemKeys(merged.prototypes);
const localOnlyKeys = [
'prototypes/oneos-web-h2-station-weekly',
'prototypes/oneos-web-h2-station-analysis',
].filter((key) => oursKeys.has(key) && !mergedKeys.has(key));
const oneOsFolder = findFolder(merged.prototypes, 'OneOS');
const h2Folder = oneOsFolder ? findFolder(oneOsFolder.children, '加氢站管理') : findFolder(merged.prototypes, '加氢站管理');
if (h2Folder) {
for (const itemKey of localOnlyKeys) {
const source =
findItemByKey(ours.prototypes, itemKey) ||
({
'prototypes/oneos-web-h2-station-weekly': {
id: 'item:prototypes:oneos-web-h2-station-weekly',
kind: 'item',
title: '站点周报统计',
itemKey: 'prototypes/oneos-web-h2-station-weekly',
},
'prototypes/oneos-web-h2-station-analysis': {
id: 'item-prototypes-oneos-web-h2-station-analysis',
kind: 'item',
title: '加氢站分析',
itemKey: 'prototypes/oneos-web-h2-station-analysis',
},
}[itemKey]);
ensureItem(h2Folder, source);
}
}
writeJson(relativePath, merged);
return {
addedKeys: localOnlyKeys,
itemCount: collectItemKeys(merged.prototypes).size,
};
}
const registry = mergeRegistry();
const sidebar = mergeSidebar();
console.log('[merge] prototype-registry.json', registry);
console.log('[merge] sidebar-tree.json', sidebar);

View File

@@ -24,6 +24,8 @@ const ROUTE_TO_PAGE = {
'third-return': 'third-return', 'third-return': 'third-return',
replace: 'replace', replace: 'replace',
'audit-return': 'audit-return', 'audit-return': 'audit-return',
training: 'driver-training',
'driver-training': 'driver-training',
}; };
function parseArgs(argv) { function parseArgs(argv) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-07-18T02:52:26.271Z", "updatedAt": "2026-07-20T05:34:46.063Z",
"title": "小羚羚", "title": "小羚羚",
"sectionId": "folder-prototypes-xll-miniapp", "sectionId": "folder-prototypes-xll-miniapp",
"description": "氢能车辆运营移动端原型;菜单与小羚羚「小程序」项目目录同步。", "description": "氢能车辆运营移动端原型;菜单与小羚羚「小程序」项目目录同步。",
"prototypeId": "xll-miniapp", "prototypeId": "xll-miniapp",
"runtimeOrigin": "http://localhost:51721", "runtimeOrigin": "http://localhost:51723",
"hrefPrefix": "http://localhost:51721/prototypes/xll-miniapp", "hrefPrefix": "http://localhost:51723/prototypes/xll-miniapp",
"groups": [ "groups": [
{ {
"id": "directory-main", "id": "directory-main",
@@ -80,6 +80,12 @@
"pageId": "replace", "pageId": "replace",
"route": "replace" "route": "replace"
}, },
{
"id": "route-driver-training",
"title": "司机安全培训",
"pageId": "driver-training",
"route": "driver-training"
},
{ {
"id": "route-audit-return", "id": "route-audit-return",
"title": "还车应结款(说明)", "title": "还车应结款(说明)",

View File

@@ -1,5 +1,5 @@
{ {
"projectRoot": "/Users/sylvawong/make-project-20260629", "projectRoot": "C:/Users/hsu06/Projects/xll-mini-program",
"prototypeId": "xll-miniapp", "prototypeId": "xll-miniapp",
"annotationSource": "src/prototypes/xll-miniapp/annotation-source.json", "annotationSource": "src/prototypes/xll-miniapp/annotation-source.json",
"devServerInfo": ".axhub/make/.dev-server-info.json", "devServerInfo": ".axhub/make/.dev-server-info.json",

View File

@@ -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 (
<ConfigProvider locale={zhCN} theme={vmTheme}>
<H2AnalysisPage />
</ConfigProvider>
);
}

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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 });
}

View File

@@ -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:59Asia/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 | 导出当前筛选结果 CSVUTF-8 BOM文件名含统计周期 |
**不包含**:导出 PDFP1按钮。
### 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 评审意见,对齐现行原型 |
**文档结束**

View File

@@ -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` 操作列使用手写 `<button class="vm-link">查看</button>`,未引入 `OperationActions``vm-shared/DESIGN.md` 规定 `vm-page` 列表主表操作列一律使用 `../../common/OperationActions`
- 影响:与加氢站站点信息、租赁合同等模块操作区视觉与交互不一致;后续若扩展「跳转站点信息」等更多操作,易再次出现各页自建链接组,增加维护成本与可发现性差异。
- 修复方向:改为 `<OperationActions view={{ onClick: () => openDetail(record) }} />`;列宽建议 148168px、`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 可访问性项。

View File

@@ -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 (
<ConfigProvider locale={zhCN} theme={vmTheme}>
<WeeklyReportPage />
</ConfigProvider>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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 = '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="880" viewBox="0 0 640 880">'
+ '<rect width="640" height="880" fill="#f8fafc"/>'
+ '<rect x="48" y="48" width="544" height="784" rx="12" fill="#fff" stroke="#e2e8f0"/>'
+ '<rect x="48" y="48" width="544" height="72" rx="12" fill="' + accent + '"/>'
+ '<text x="320" y="460" text-anchor="middle" fill="#64748b" font-size="18">' + label + '</text>'
+ '</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 || '附件预览' })
))
);
}