refactor(stage5): 合并重复实现,移除失效的演示能力

Excel 导出(原先 4 份各自拼装 workbook)
- 新增 src/shared/xlsx.ts 作为唯一实现:文件名统一 .xlsx、sheet 名截断到 31 字符,
  对外提供 buildAoaSheet / buildJsonSheet / writeWorkbook / exportAoaSheet / exportJsonSheet。
  只收敛"组装与写出"这一层,各调用方仍自行决定列宽、冻结与数字格式,导出样式不变。
- 迁移 assets(内联 json_to_sheet + writeFile)、mileage/xlsx-export、
  hydrogen 的两份 helper(prototype-download.ts 与 download-xls.js,均已删除)。

高德地图(原先 2 份近乎逐行复制)
- 新增 src/shared/amap.ts:SDK 版本、插件列表、安全码注入、底图参数、
  控件位置与热力图色带只在此处定义;两个画布只保留各自的半径/透明度。
- 图例渐变条原先把同一组色值又写了一遍,改为复用 shared 的常量,图例与地图不会漂移。

数值格式化
- 两个下钻视图各自复制了同样的 formatNumber/format(共 42 处调用),
  统一到 hydrogen/model/display-format.ts 的 formatFixed;默认路径与既有行为逐字一致。
- 原 display-format.ts 里的 finiteNumber/formatNumber/formatScaled 无任何生产调用方,
  只被自己的测试引用;改为 formatFixed + blankForMissing 显式选项,
  既保留了"真零 vs 不可用"的区分能力,也不再留无人使用的导出。测试同步重写。

失效的演示能力
- Blur / DemoModeProvider 恒为 enabled=false,等于永久 no-op:
  移除 13 个文件里 50 处 <Blur> 包裹(渲染结果不变)、删除 components/Blur.tsx
  与 Shell 中的 Provider,调用方直接渲染原表达式。

未做(刻意)
- src/lib/cn.ts 不引入 tailwind-merge:全仓只有 2 处调用,不值得新增传递依赖。已在文档说明。

架构守护新增 2 条:只有 shared/amap.ts 可加载高德 SDK;
modules/** 不得再出现 book_new / book_append_sheet / writeFile(。

lint / test(137) / build 全绿,可达性仍为 0 未引用文件。
This commit is contained in:
dsh-agent
2026-09-11 10:28:32 +08:00
parent 5d14a28b0f
commit 507bc90ed2
33 changed files with 334 additions and 318 deletions
+77
View File
@@ -0,0 +1,77 @@
/**
* 高德地图 JSAPI 2.0 的唯一接入点。
*
* 两个热力图模块此前各自重复了:SDK 版本与插件列表、安全码注入、
* 底图参数、控件位置以及整条渐变色带。这些必须保持一致,否则"同类地图
* 长得不一样",因此收敛到这里。各页面只保留自己的热力图参数(半径/透明度)。
*/
/** 最小结构类型:只声明我们实际用到的构造器。 */
export type AmapInstance = {
Map: new (container: HTMLElement, options: Record<string, unknown>) => any;
HeatMap: new (map: any, options: Record<string, unknown>) => any;
ToolBar: new (options?: Record<string, unknown>) => any;
Scale: new (options?: Record<string, unknown>) => any;
Bounds: new (southWest: [number, number], northEast: [number, number]) => any;
};
export interface AmapClientConfig {
key: string;
securityCode: string;
}
declare global {
interface Window {
_AMapSecurityConfig?: { securityJsCode: string };
}
}
/** 热力图色带:冷→热,两处地图共用。 */
export const HEATMAP_GRADIENT: Record<number, string> = {
0.1: '#2563eb',
0.3: '#0891b2',
0.5: '#16a34a',
0.68: '#eab308',
0.84: '#f97316',
1: '#dc2626',
};
/** 与地图色带对应的 UI 图例渐变,避免图例与地图各写一份色值。 */
export const HEATMAP_LEGEND_GRADIENT =
'linear-gradient(90deg, #2563eb 0%, #0891b2 25%, #16a34a 45%, #eab308 65%, #f97316 82%, #dc2626 100%)';
/** 加载 JSAPI:版本、插件列表与安全码只在此处定义。 */
export async function loadAmap(config: AmapClientConfig): Promise<AmapInstance> {
window._AMapSecurityConfig = { securityJsCode: config.securityCode };
const loaderModule = await import('@amap/amap-jsapi-loader');
const loaded = await loaderModule.default.load({
key: config.key,
version: '2.0',
plugins: ['AMap.HeatMap', 'AMap.ToolBar', 'AMap.Scale'],
});
return loaded as unknown as AmapInstance;
}
/** 创建只读热力图底图:底图样式、控件位置与色带统一。 */
export function createHeatmapMap(
AMap: AmapInstance,
container: HTMLElement,
style: { radius: number; opacity: [number, number] },
): { map: any; heatmap: any } {
const map = new AMap.Map(container, {
viewMode: '2D',
zoom: 5,
center: [105.4, 34.4],
mapStyle: 'amap://styles/whitesmoke',
resizeEnable: true,
showLabel: true,
});
map.addControl(new AMap.ToolBar({ position: { right: '20px', bottom: '76px' } }));
map.addControl(new AMap.Scale({ position: { right: '18px', bottom: '24px' } }));
const heatmap = new AMap.HeatMap(map, {
radius: style.radius,
opacity: style.opacity,
gradient: { ...HEATMAP_GRADIENT },
});
return { map, heatmap };
}
+59
View File
@@ -0,0 +1,59 @@
import * as XLSX from 'xlsx';
/**
* Excel 导出的唯一实现。
*
* 此前仓库里有 4 份各自拼装 workbook 的代码(assets 内联、mileage 内联、
* hydrogen 的两份 aoa/json helper),产物后缀与 sheet 名截断规则也不一致。
* 这里只收敛"组装与写出"这一层原语;各调用方仍自行决定列宽、冻结与数字格式,
* 以免改变已验收的导出样式。
*/
export type AoaRow = Array<string | number | boolean | null | undefined>;
/** 统一产物后缀:内部一律写出 .xlsx。 */
export function ensureXlsxFilename(name: string | undefined): string {
const raw = String(name || 'export').trim() || 'export';
return `${raw.replace(/\.(csv|xls|xlsx)$/i, '')}.xlsx`;
}
/** 由二维数组构建 sheet。 */
export function buildAoaSheet(rows: AoaRow[]): XLSX.WorkSheet {
return XLSX.utils.aoa_to_sheet(rows);
}
/** 由对象数组构建 sheet(首行为字段名)。 */
export function buildJsonSheet(rows: Record<string, unknown>[]): XLSX.WorkSheet {
return XLSX.utils.json_to_sheet(rows);
}
/** Excel 的 sheet 名上限是 31 字符,空名回退为 Sheet1。 */
function safeSheetName(name: string): string {
return (name || 'Sheet1').slice(0, 31) || 'Sheet1';
}
/** 写出一本工作簿。调用方可先给 sheet 设置 `!cols` / `!freeze` / 单元格格式。 */
export function writeWorkbook(
sheets: Array<{ name: string; sheet: XLSX.WorkSheet }>,
fileName: string,
): void {
const workbook = XLSX.utils.book_new();
for (const { name, sheet } of sheets) {
XLSX.utils.book_append_sheet(workbook, sheet, safeSheetName(name));
}
XLSX.writeFile(workbook, ensureXlsxFilename(fileName));
}
/** 便捷:单个二维数组 sheet 直接下载。 */
export function exportAoaSheet(rows: AoaRow[], fileName: string, sheetName = 'Sheet1'): void {
writeWorkbook([{ name: sheetName, sheet: buildAoaSheet(rows) }], fileName);
}
/** 便捷:单个对象数组 sheet 直接下载。 */
export function exportJsonSheet(
rows: Record<string, unknown>[],
fileName: string,
sheetName = 'Sheet1',
): void {
writeWorkbook([{ name: sheetName, sheet: buildJsonSheet(rows) }], fileName);
}