普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月29日首页

HarmonyOS应用开发:多重筛选

作者 鹿人戛
2025年12月29日 22:13

前言

本示例主要介绍多重筛选场景,利用数组方法过滤满足条件的数据,利用LazyForEach实现列表信息的渲染以及刷新。

效果图预览

使用说明

  1. 等待列表数据全部加载完成后,点击筛选类型,展开筛选数据。
  2. 选中想要筛选的数据,点击确认,列表刷新。
  3. 再次点开筛选类型,保留上次筛选的内容,点击重置筛选内容复原,列表数据恢复为未筛选前的数据。

实现思路

本例涉及的关键特性和实现方案如下:

  1. 使用Grid实现筛选条件布局。

    Grid() { ForEach(this.item.options, (options: string, idx: number) => { GridItem() { Text(options) .textAlign(TextAlign.Center) .fontSize(16) .height(40) .width('100%') } ... }) } .columnsTemplate('1fr 1fr 1fr') .rowsGap(16) .columnsGap(16) .margin({ left: 16, right: 6, top: 8, bottom: 8 }) .layoutDirection(GridDirection.Row) .constraintSize({ minHeight: '15%', maxHeight: '15%'// grid会撑满maxHeight,先限定死高度 })

  2. 使用数组方法对筛选数据进行过滤,得到筛选数据。

    GridItem() { Text(options) .textAlign(TextAlign.Center) .fontSize(16) .height(40) .width('100%') } .onClick(() => { if (this.item.selectItem.includes(idx)) { let index = this.item.selectItem.indexOf(idx); let listIdx = this.changData.indexOf(options); // 删除已存在的筛选数据的index值 this.item.selectItem.splice(index, 1); // 过滤出来没有重复数据的筛选值 this.changData = this.changData.filter(i => i !== options); this.selectArr = this.item.selectItem; // 删除已选择的数据的行数index数组 this.arrayListData.splice(listIdx, 1); } else { // 添加筛选数据的index值 this.item.selectItem.push(idx); // 添加选中的数据 this.changData.push(options); this.selectArr = this.item.selectItem; // 添加选择的数据的行数index数组 this.arrayListData.push(this.listIndex); } })

  3. 得到筛选的数据后根据点击的筛选数据行数,使用has进行if判断看是否满足多重筛选的条件。

    Button('确认') .height(40) .width(150) .backgroundColor(Color.White) .fontColor('#333') .onClick(() => { this.isShow = false; let arrayListData = new Set(this.arrayListData) if (arrayListData.has(0) && !arrayListData.has(1)) { // 仅选择停放时间 this.siteList.timeMultiFilter(this.changData); } else if (!arrayListData.has(0) && arrayListData.has(1)) { // 仅选择套餐类型 this.siteList.typeMultiFilter(this.changData); } else if (!arrayListData.has(0) && !arrayListData.has(1) && arrayListData.has(2)) { // 仅选择充电 this.siteList.getInitalList(); } else if (this.changData.length === 0) { // 未对数据进行选择 this.siteList.getInitalList(); } else { // 多重筛选 this.siteList.multiFilter(this.changData); } if (this.siteList.totalCount() === 0) { this.siteList.getInitalList(); promptAction.showToast({ message: "未找到相关数据" }); } })

  4. 使用filter过滤出来符合条件的数据,筛选出来的数组构建一个新的Set,使用Set中的has判断列表中相关数据是否存在。

    public multiFilter(changData: Array) { let siteListString: string | undefined = AppStorage.get('siteList') if (siteListString) { let siteListObject: SiteListDataSource | undefined = JSON.parse(siteListString) if (siteListObject === undefined) { return } this.initialSiteList = siteListObject.dataList this.dataList = [] this.dataList = this.initialSiteList // 筛选数据 let changDataSet = new Set(changData) let dataList: SiteItem[] = this.dataList.filter(item => { item.siteBale = item.siteBale.filter(item => { if ((item.time && item.type) && (changDataSet.has(item.time)) && (changDataSet.has(item.type))) { return item } return }) return item.siteBale }) dataList = dataList.filter(item => item.siteBale.length !== 0); this.dataList = []; this.dataList = dataList; this.notifyDataReload(); } }

  5. 使用深拷贝保留原数据。

    /**

    • 返回原数组 */ public getInitalList() { let siteListString: string | undefined = AppStorage.get('siteList'); if (siteListString) { let siteListObject: SiteListDataSource | undefined = JSON.parse(siteListString); if (siteListObject === undefined) { return; } this.initialSiteList = siteListObject.dataList; this.dataList = []; this.dataList = this.initialSiteList; this.notifyDataReload(); } }

如果您想系统深入地学习 HarmonyOS 开发或想考取HarmonyOS认证证书,欢迎加入华为开发者学堂:

请点击→: HarmonyOS官方认证培训

绿联云 NAS 安装 AudioDock 详细教程

2025年12月29日 21:54

前言

AudioDock(声仓)发布之后,好多感兴趣的小伙伴给了我反馈,感谢支持!

github.com/mmdctjj/Aud…

今天先来介绍下绿联云 NAS 的安装指南。我的NAS型号是:DH2600,新系统。

往期精彩推荐

正文

准备工作

首先确保自己的 NAS 可以下载 Docker 镜像。无法下载可以在后台私信我。

然后在 共享文件夹/docker 目录下新增一个文件目录:audiodock。

我新建过了,所以新建了 audiodock2 项目。

新建目录

打开这个文件目录,新建三个文件夹:music、audio、covers

music 是映射音乐的目录、audio 是映射声书的目录,covers 存放解析后封面的目录。

从 GitHub 下载的 nginx.conf 文件拖动到当前目录下。下载地址:github.com/mmdctjj/Aud…

然后打开 Docker 应用的项目栏目,新建一个项目:audiodock

新建项目

这时候系统会自动识别新建的 audiodock 目录。

将下面的内容复制到 compose 配置中。

version: "3.8"

services:
  # 1. API 后端服务 (Node.js)
  api:
    platform: linux/amd64
    image: mmdctjj/audiodock-api
    container_name: audiodock-api

    # 容器内部端口 (3000) 默认对内部网络开放,无需 ports 字段映射到宿主机
    # 如果要直接测试 API,可以加上 ports: - "3000:3000"
    ports:
      - "8858:3000"

    environment:
      - AUDIO_BOOK_DIR=/audio
      - MUSIC_BASE_DIR=/music
      - CACHE_DIR=/covers
      - DATABASE_URL=file:/data/dev.db

    # 挂载数据文件和缓存,使用 Docker 命名卷更安全
    volumes:
      - /volume1/迅雷下载/有声书:/audio
      - /volume1/迅雷下载/音乐:/music
      - ./covers:/covers
      - api-db:/data

    restart: unless-stopped
    networks:
      - audiodock-network

  # 2. Web 前端服务 (Nginx) - 用于托管静态文件和反向代理
  web:
    platform: linux/amd64
    image: mmdctjj/audiodock-web
    container_name: audiodock-web
    ports:
      - "9959:9958" # <--- 将 Web 服务的 80 端口映射到宿主机的 8080 端口
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api # 确保 API 容器先启动
    networks:
      - audiodock-network

volumes:
  api-cache: # 命名卷用于缓存
  api-db: # 命名卷用于 SQLite 或其他数据文件

networks:
  audiodock-network:

重点替换替换下映射路径:

 # 挂载数据文件和缓存,使用 Docker 命名卷更安全
    volumes:
      - /volume1/迅雷下载/有声书:/audio
      - /volume1/迅雷下载/音乐:/music
      - ./covers:/covers
      - api-db:/data

映射路径的查看是选中文件夹右键属性,可以看到具体的地址,复制即可。

查看文件地址

最后保证服务端口映射没有重复,点击重新部署即可启动服务。

启动部署

部署成功

接下来稍等一会,等数据入库完成,后端服务占用资源减少

入库完成

打开页面地址,会看到页面是这样的

页面

输入后端服务器地址,鼠标点击页面空白区域,或者按 tab 键,会触发后端服务状态检查,绿代表链接成功,红色代表链接错误。

后端服务链接成功

输入用户名、密码登陆,或者点注册之后输入确认密码登陆并注册!

注册并登陆

页面会刷新首页,看到是这样的首页说明完全成功了(马赛克是防止版权问题平台不过审)!

登陆成功

以上就是部署服务端、web端的教程!桌面端的部署请看上篇文章。移动端预计本周末发版,敬请期待!

最后

本篇文章主要介绍了绿联云 nas 如何安装 AudioDock !

为了方便大家交流,我建了一个沟通群,欢迎大家入群交流。

如果无法下载镜像或者 nginx.conf 等文件,可以在后台回复 audiodock,我看到会发最新版的下载链接。

欢迎 Star:github.com/mmdctjj/Aud…

往期精彩推荐

GIS 数据转换:使用 GDAL 将 GeoJSON 转换为 Shp 数据

作者 GIS之路
2025年12月29日 21:21

前言

GeoJSON 作为一种通用的地理数据格式,可以很方便地用于共享交换。在 GIS 开发中,经常需要进行数据的转换处理,其中常见的便是将 GeoJSON 转换为 Shp 数据进行展示。

在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDALGeoJSON转换为Shp数据。

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 数据准备

GeoJSON是一种用于编码各种地理数据结构的格式,采用JSON方式表示。在WebGIS开发中,被广泛应用于数据传输和共享交换。

有关GeoJSON数据的详细介绍,请参考往期文章:GeoJSON 数据简介

如下是本文选取的部分国家边界范围的GeoJSON数据结构:

{"type":"FeatureCollection","features":[{"type":"Feature","id":"AFG","properties":{"name":"Afghanistan"},"geometry":{"type":"Polygon","coordinates":[[[61.210817,35.650072],[62.230651,35.270664],[62.984662,35.404041],[63.193538,35.857166],[63.982896,36.007957],[64.546479,36.312073],[64.746105,37.111818],[65.588948,37.305217],[65.745631,37.661164],[66.217385,37.39379],[66.518607,37.362784],[67.075782,37.356144],[67.83,37.144994],[68.135562,37.023115],[68.859446,37.344336],[69.196273,37.151144],[69.518785,37.608997],[70.116578,37.588223],[70.270574,37.735165],[70.376304,38.138396],[70.806821,38.486282],[71.348131,38.258905],[71.239404,37.953265],[71.541918,37.905774],[71.448693,37.065645],[71.844638,36.738171],[72.193041,36.948288],[72.63689,37.047558],[73.260056,37.495257],[73.948696,37.421566],[74.980002,37.41999],[75.158028,37.133031],[74.575893,37.020841],[74.067552,36.836176],[72.920025,36.720007],[71.846292,36.509942],[71.262348,36.074388],[71.498768,35.650563],[71.613076,35.153203],[71.115019,34.733126],[71.156773,34.348911],[70.881803,33.988856],[69.930543,34.02012],[70.323594,33.358533],[69.687147,33.105499],[69.262522,32.501944],[69.317764,31.901412],[68.926677,31.620189],[68.556932,31.71331],[67.792689,31.58293],[67.683394,31.303154],[66.938891,31.304911],[66.381458,30.738899],[66.346473,29.887943],[65.046862,29.472181],[64.350419,29.560031],[64.148002,29.340819],[63.550261,29.468331],[62.549857,29.318572],[60.874248,29.829239],[61.781222,30.73585],[61.699314,31.379506],[60.941945,31.548075],[60.863655,32.18292],[60.536078,32.981269],[60.9637,33.528832],[60.52843,33.676446],[60.803193,34.404102],[61.210817,35.650072]]]}},
{"type":"Feature","id":"AGO","properties":{"name":"Angola"},"geometry":{"type":"MultiPolygon","coordinates":[[[[16.326528,-5.87747],[16.57318,-6.622645],[16.860191,-7.222298],[17.089996,-7.545689],[17.47297,-8.068551],[18.134222,-7.987678],[18.464176,-7.847014],[19.016752,-7.988246],[19.166613,-7.738184],[19.417502,-7.155429],[20.037723,-7.116361],[20.091622,-6.94309],[20.601823,-6.939318],[20.514748,-7.299606],[21.728111,-7.290872],[21.746456,-7.920085],[21.949131,-8.305901],[21.801801,-8.908707],[21.875182,-9.523708],[22.208753,-9.894796],[22.155268,-11.084801],[22.402798,-10.993075],[22.837345,-11.017622],[23.456791,-10.867863],[23.912215,-10.926826],[24.017894,-11.237298],[23.904154,-11.722282],[24.079905,-12.191297],[23.930922,-12.565848],[24.016137,-12.911046],[21.933886,-12.898437],[21.887843,-16.08031],[22.562478,-16.898451],[23.215048,-17.523116],[21.377176,-17.930636],[18.956187,-17.789095],[18.263309,-17.309951],[14.209707,-17.353101],[14.058501,-17.423381],[13.462362,-16.971212],[12.814081,-16.941343],[12.215461,-17.111668],[11.734199,-17.301889],[11.640096,-16.673142],[11.778537,-15.793816],[12.123581,-14.878316],[12.175619,-14.449144],[12.500095,-13.5477],[12.738479,-13.137906],[13.312914,-12.48363],[13.633721,-12.038645],[13.738728,-11.297863],[13.686379,-10.731076],[13.387328,-10.373578],[13.120988,-9.766897],[12.87537,-9.166934],[12.929061,-8.959091],[13.236433,-8.562629],[12.93304,-7.596539],[12.728298,-6.927122],[12.227347,-6.294448],[12.322432,-6.100092],[12.735171,-5.965682],[13.024869,-5.984389],[13.375597,-5.864241],[16.326528,-5.87747]]],[[[12.436688,-5.684304],[12.182337,-5.789931],[11.914963,-5.037987],[12.318608,-4.60623],[12.62076,-4.438023],[12.995517,-4.781103],[12.631612,-4.991271],[12.468004,-5.248362],[12.436688,-5.684304]]]]}},
{"type":"Feature","id":"ALB","properties":{"name":"Albania"},"geometry":{"type":"Polygon","coordinates":[[[20.590247,41.855404],[20.463175,41.515089],[20.605182,41.086226],[21.02004,40.842727],[20.99999,40.580004],[20.674997,40.435],[20.615,40.110007],[20.150016,39.624998],[19.98,39.694993],[19.960002,39.915006],[19.406082,40.250773],[19.319059,40.72723],[19.40355,41.409566],[19.540027,41.719986],[19.371769,41.877548],[19.304486,42.195745],[19.738051,42.688247],[19.801613,42.500093],[20.0707,42.58863],[20.283755,42.32026],[20.52295,42.21787],[20.590247,41.855404]]]}},
{"type":"Feature","id":"ARE","properties":{"name":"United Arab Emirates"},"geometry":{"type":"Polygon","coordinates":[[[51.579519,24.245497],[51.757441,24.294073],[51.794389,24.019826],[52.577081,24.177439],[53.404007,24.151317],[54.008001,24.121758],[54.693024,24.797892],[55.439025,25.439145],[56.070821,26.055464],[56.261042,25.714606],[56.396847,24.924732],[55.886233,24.920831],[55.804119,24.269604],[55.981214,24.130543],[55.528632,23.933604],[55.525841,23.524869],[55.234489,23.110993],[55.208341,22.70833],[55.006803,22.496948],[52.000733,23.001154],[51.617708,24.014219],[51.579519,24.245497]]]}},
{"type":"Feature","id":"ARG","properties":{"name":"Argentina"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-65.5,-55.2],[-66.45,-55.25],[-66.95992,-54.89681],[-67.56244,-54.87001],[-68.63335,-54.8695],[-68.63401,-52.63637],[-68.25,-53.1],[-67.75,-53.85],[-66.45,-54.45],[-65.05,-54.7],[-65.5,-55.2]]],[[[-64.964892,-22.075862],[-64.377021,-22.798091],[-63.986838,-21.993644],[-62.846468,-22.034985],[-62.685057,-22.249029],[-60.846565,-23.880713],[-60.028966,-24.032796],[-58.807128,-24.771459],[-57.777217,-25.16234],[-57.63366,-25.603657],[-58.618174,-27.123719],[-57.60976,-27.395899],[-56.486702,-27.548499],[-55.695846,-27.387837],[-54.788795,-26.621786],[-54.625291,-25.739255],[-54.13005,-25.547639],[-53.628349,-26.124865],[-53.648735,-26.923473],[-54.490725,-27.474757],[-55.162286,-27.881915],[-56.2909,-28.852761],[-57.625133,-30.216295],[-57.874937,-31.016556],[-58.14244,-32.044504],[-58.132648,-33.040567],[-58.349611,-33.263189],[-58.427074,-33.909454],[-58.495442,-34.43149],[-57.22583,-35.288027],[-57.362359,-35.97739],[-56.737487,-36.413126],[-56.788285,-36.901572],[-57.749157,-38.183871],[-59.231857,-38.72022],[-61.237445,-38.928425],[-62.335957,-38.827707],[-62.125763,-39.424105],[-62.330531,-40.172586],[-62.145994,-40.676897],[-62.745803,-41.028761],[-63.770495,-41.166789],[-64.73209,-40.802677],[-65.118035,-41.064315],[-64.978561,-42.058001],[-64.303408,-42.359016],[-63.755948,-42.043687],[-63.458059,-42.563138],[-64.378804,-42.873558],[-65.181804,-43.495381],[-65.328823,-44.501366],[-65.565269,-45.036786],[-66.509966,-45.039628],[-67.293794,-45.551896],[-67.580546,-46.301773],[-66.597066,-47.033925],[-65.641027,-47.236135],[-65.985088,-48.133289],[-67.166179,-48.697337],[-67.816088,-49.869669],[-68.728745,-50.264218],[-69.138539,-50.73251],[-68.815561,-51.771104],[-68.149995,-52.349983],[-68.571545,-52.299444],[-69.498362,-52.142761],[-71.914804,-52.009022],[-72.329404,-51.425956],[-72.309974,-50.67701],[-72.975747,-50.74145],[-73.328051,-50.378785],[-73.415436,-49.318436],[-72.648247,-48.878618],[-72.331161,-48.244238],[-72.447355,-47.738533],[-71.917258,-46.884838],[-71.552009,-45.560733],[-71.659316,-44.973689],[-71.222779,-44.784243],[-71.329801,-44.407522],[-71.793623,-44.207172],[-71.464056,-43.787611],[-71.915424,-43.408565],[-72.148898,-42.254888],[-71.746804,-42.051386],[-71.915734,-40.832339],[-71.680761,-39.808164],[-71.413517,-38.916022],[-70.814664,-38.552995],[-71.118625,-37.576827],[-71.121881,-36.658124],[-70.364769,-36.005089],[-70.388049,-35.169688],[-69.817309,-34.193571],[-69.814777,-33.273886],[-70.074399,-33.09121],[-70.535069,-31.36501],[-69.919008,-30.336339],[-70.01355,-29.367923],[-69.65613,-28.459141],[-69.001235,-27.521214],[-68.295542,-26.89934],[-68.5948,-26.506909],[-68.386001,-26.185016],[-68.417653,-24.518555],[-67.328443,-24.025303],[-66.985234,-22.986349],[-67.106674,-22.735925],[-66.273339,-21.83231],[-64.964892,-22.075862]]]]}},
{"type":"Feature","id":"ARM","properties":{"name":"Armenia"},"geometry":{"type":"Polygon","coordinates":[[[43.582746,41.092143],[44.97248,41.248129],[45.179496,40.985354],[45.560351,40.81229],[45.359175,40.561504],[45.891907,40.218476],[45.610012,39.899994],[46.034534,39.628021],[46.483499,39.464155],[46.50572,38.770605],[46.143623,38.741201],[45.735379,39.319719],[45.739978,39.473999],[45.298145,39.471751],[45.001987,39.740004],[44.79399,39.713003],[44.400009,40.005],[43.656436,40.253564],[43.752658,40.740201],[43.582746,41.092143]]]}},
{"type":"Feature","id":"ATA","properties":{"name":"Antarctica"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-59.572095,-80.040179],[-59.865849,-80.549657],[-60.159656,-81.000327],[-62.255393,-80.863178],[-64.488125,-80.921934],[-65.741666,-80.588827],[-65.741666,-80.549657],[-66.290031,-80.255773],[-64.037688,-80.294944],[-61.883246,-80.39287],[-61.138976,-79.981371],[-60.610119,-79.628679],[-59.572095,-80.040179]]],[[[-159.208184,-79.497059],[-161.127601,-79.634209],[-162.439847,-79.281465],[-163.027408,-78.928774],[-163.066604,-78.869966],[-163.712896,-78.595667],[-163.712896,-78.595667],[-163.105801,-78.223338],[-161.245113,-78.380176],[-160.246208,-78.693645],[-159.482405,-79.046338],[-159.208184,-79.497059]]],[[[-45.154758,-78.04707],[-43.920828,-78.478103],[-43.48995,-79.08556],[-43.372438,-79.516645],[-43.333267,-80.026123],[-44.880537,-80.339644],[-46.506174,-80.594357],[-48.386421,-80.829485],[-50.482107,-81.025442],[-52.851988,-80.966685],[-54.164259,-80.633528],[-53.987991,-80.222028],[-51.853134,-79.94773],[-50.991326,-79.614623],[-50.364595,-79.183487],[-49.914131,-78.811209],[-49.306959,-78.458569],[-48.660616,-78.047018],[-48.660616,-78.047019],[-48.151396,-78.04707],[-46.662857,-77.831476],[-45.154758,-78.04707]]],[[[-121.211511,-73.50099],[-119.918851,-73.657725],[-118.724143,-73.481353],[-119.292119,-73.834097],[-120.232217,-74.08881],[-121.62283,-74.010468],[-122.621735,-73.657778],[-122.621735,-73.657777],[-122.406245,-73.324619],[-121.211511,-73.50099]]],[[[-125.559566,-73.481353],[-124.031882,-73.873268],[-124.619469,-73.834097],[-125.912181,-73.736118],[-127.28313,-73.461769],[-127.28313,-73.461768],[-126.558472,-73.246226],[-125.559566,-73.481353]]],[[[-98.98155,-71.933334],[-97.884743,-72.070535],[-96.787937,-71.952971],[-96.20035,-72.521205],[-96.983765,-72.442864],[-98.198083,-72.482035],[-99.432013,-72.442864],[-100.783455,-72.50162],[-101.801868,-72.305663],[-102.330725,-71.894164],[-102.330725,-71.894164],[-101.703967,-71.717792],[-100.430919,-71.854993],[-98.98155,-71.933334]]],[[[-68.451346,-70.955823],[-68.333834,-71.406493],[-68.510128,-71.798407],[-68.784297,-72.170736],[-69.959471,-72.307885],[-71.075889,-72.503842],[-72.388134,-72.484257],[-71.8985,-72.092343],[-73.073622,-72.229492],[-74.19004,-72.366693],[-74.953895,-72.072757],[-75.012625,-71.661258],[-73.915819,-71.269345],[-73.915819,-71.269344],[-73.230331,-71.15178],[-72.074717,-71.190951],[-71.780962,-70.681473],[-71.72218,-70.309196],[-71.741791,-69.505782],[-71.173815,-69.035475],[-70.253252,-68.87874],[-69.724447,-69.251017],[-69.489422,-69.623346],[-69.058518,-70.074016],[-68.725541,-70.505153],[-68.451346,-70.955823]]],[[[-58.614143,-64.152467],[-59.045073,-64.36801],[-59.789342,-64.211223],[-60.611928,-64.309202],[-61.297416,-64.54433],[-62.0221,-64.799094],[-62.51176,-65.09303],[-62.648858,-65.484942],[-62.590128,-65.857219],[-62.120079,-66.190326],[-62.805567,-66.425505],[-63.74569,-66.503847],[-64.294106,-66.837004],[-64.881693,-67.150474],[-65.508425,-67.58161],[-65.665082,-67.953887],[-65.312545,-68.365335],[-64.783715,-68.678908],[-63.961103,-68.913984],[-63.1973,-69.227556],[-62.785955,-69.619419],[-62.570516,-69.991747],[-62.276736,-70.383661],[-61.806661,-70.716768],[-61.512906,-71.089045],[-61.375809,-72.010074],[-61.081977,-72.382351],[-61.003661,-72.774265],[-60.690269,-73.166179],[-60.827367,-73.695242],[-61.375809,-74.106742],[-61.96337,-74.439848],[-63.295201,-74.576997],[-63.74569,-74.92974],[-64.352836,-75.262847],[-65.860987,-75.635124],[-67.192818,-75.79191],[-68.446282,-76.007452],[-69.797724,-76.222995],[-70.600724,-76.634494],[-72.206776,-76.673665],[-73.969536,-76.634494],[-75.555977,-76.712887],[-77.24037,-76.712887],[-76.926979,-77.104802],[-75.399294,-77.28107],[-74.282876,-77.55542],[-73.656119,-77.908112],[-74.772536,-78.221633],[-76.4961,-78.123654],[-77.925858,-78.378419],[-77.984666,-78.789918],[-78.023785,-79.181833],[-76.848637,-79.514939],[-76.633224,-79.887216],[-75.360097,-80.259545],[-73.244852,-80.416331],[-71.442946,-80.69063],[-70.013163,-81.004151],[-68.191646,-81.317672],[-65.704279,-81.474458],[-63.25603,-81.748757],[-61.552026,-82.042692],[-59.691416,-82.37585],[-58.712121,-82.846106],[-58.222487,-83.218434],[-57.008117,-82.865691],[-55.362894,-82.571755],[-53.619771,-82.258235],[-51.543644,-82.003521],[-49.76135,-81.729171],[-47.273931,-81.709586],[-44.825708,-81.846735],[-42.808363,-82.081915],[-42.16202,-81.65083],[-40.771433,-81.356894],[-38.244818,-81.337309],[-36.26667,-81.121715],[-34.386397,-80.906172],[-32.310296,-80.769023],[-30.097098,-80.592651],[-28.549802,-80.337938],[-29.254901,-79.985195],[-29.685805,-79.632503],[-29.685805,-79.260226],[-31.624808,-79.299397],[-33.681324,-79.456132],[-35.639912,-79.456132],[-35.914107,-79.083855],[-35.77701,-78.339248],[-35.326546,-78.123654],[-33.896763,-77.888526],[-32.212369,-77.65345],[-30.998051,-77.359515],[-29.783732,-77.065579],[-28.882779,-76.673665],[-27.511752,-76.497345],[-26.160336,-76.360144],[-25.474822,-76.281803],[-23.927552,-76.24258],[-22.458598,-76.105431],[-21.224694,-75.909474],[-20.010375,-75.674346],[-18.913543,-75.439218],[-17.522982,-75.125698],[-16.641589,-74.79254],[-15.701491,-74.498604],[-15.40771,-74.106742],[-16.46532,-73.871614],[-16.112784,-73.460114],[-15.446855,-73.146542],[-14.408805,-72.950585],[-13.311973,-72.715457],[-12.293508,-72.401936],[-11.510067,-72.010074],[-11.020433,-71.539767],[-10.295774,-71.265416],[-9.101015,-71.324224],[-8.611381,-71.65733],[-7.416622,-71.696501],[-7.377451,-71.324224],[-6.868232,-70.93231],[-5.790985,-71.030289],[-5.536375,-71.402617],[-4.341667,-71.461373],[-3.048981,-71.285053],[-1.795492,-71.167438],[-0.659489,-71.226246],[-0.228637,-71.637745],[0.868195,-71.304639],[1.886686,-71.128267],[3.022638,-70.991118],[4.139055,-70.853917],[5.157546,-70.618789],[6.273912,-70.462055],[7.13572,-70.246512],[7.742866,-69.893769],[8.48711,-70.148534],[9.525135,-70.011333],[10.249845,-70.48164],[10.817821,-70.834332],[11.953824,-70.638375],[12.404287,-70.246512],[13.422778,-69.972162],[14.734998,-70.030918],[15.126757,-70.403247],[15.949342,-70.030918],[17.026589,-69.913354],[18.201711,-69.874183],[19.259373,-69.893769],[20.375739,-70.011333],[21.452985,-70.07014],[21.923034,-70.403247],[22.569403,-70.697182],[23.666184,-70.520811],[24.841357,-70.48164],[25.977309,-70.48164],[27.093726,-70.462055],[28.09258,-70.324854],[29.150242,-70.20729],[30.031583,-69.93294],[30.971733,-69.75662],[31.990172,-69.658641],[32.754053,-69.384291],[33.302443,-68.835642],[33.870419,-68.502588],[34.908495,-68.659271],[35.300202,-69.012014],[36.16201,-69.247142],[37.200035,-69.168748],[37.905108,-69.52144],[38.649404,-69.776205],[39.667894,-69.541077],[40.020431,-69.109941],[40.921358,-68.933621],[41.959434,-68.600514],[42.938702,-68.463313],[44.113876,-68.267408],[44.897291,-68.051866],[45.719928,-67.816738],[46.503343,-67.601196],[47.44344,-67.718759],[48.344419,-67.366068],[48.990736,-67.091718],[49.930885,-67.111303],[50.753471,-66.876175],[50.949325,-66.523484],[51.791547,-66.249133],[52.614133,-66.053176],[53.613038,-65.89639],[54.53355,-65.818049],[55.414943,-65.876805],[56.355041,-65.974783],[57.158093,-66.249133],[57.255968,-66.680218],[58.137361,-67.013324],[58.744508,-67.287675],[59.939318,-67.405239],[60.605221,-67.679589],[61.427806,-67.953887],[62.387489,-68.012695],[63.19049,-67.816738],[64.052349,-67.405239],[64.992447,-67.620729],[65.971715,-67.738345],[66.911864,-67.855909],[67.891133,-67.934302],[68.890038,-67.934302],[69.712624,-68.972791],[69.673453,-69.227556],[69.555941,-69.678226],[68.596258,-69.93294],[67.81274,-70.305268],[67.949889,-70.697182],[69.066307,-70.677545],[68.929157,-71.069459],[68.419989,-71.441788],[67.949889,-71.853287],[68.71377,-72.166808],[69.869307,-72.264787],[71.024895,-72.088415],[71.573285,-71.696501],[71.906288,-71.324224],[72.454627,-71.010703],[73.08141,-70.716768],[73.33602,-70.364024],[73.864877,-69.874183],[74.491557,-69.776205],[75.62756,-69.737034],[76.626465,-69.619419],[77.644904,-69.462684],[78.134539,-69.07077],[78.428371,-68.698441],[79.113859,-68.326216],[80.093127,-68.071503],[80.93535,-67.875546],[81.483792,-67.542388],[82.051767,-67.366068],[82.776426,-67.209282],[83.775331,-67.30726],[84.676206,-67.209282],[85.655527,-67.091718],[86.752359,-67.150474],[87.477017,-66.876175],[87.986289,-66.209911],[88.358411,-66.484261],[88.828408,-66.954568],[89.67063,-67.150474],[90.630365,-67.228867],[91.5901,-67.111303],[92.608539,-67.189696],[93.548637,-67.209282],[94.17542,-67.111303],[95.017591,-67.170111],[95.781472,-67.385653],[96.682399,-67.248504],[97.759646,-67.248504],[98.68021,-67.111303],[99.718182,-67.248504],[100.384188,-66.915346],[100.893356,-66.58224],[101.578896,-66.30789],[102.832411,-65.563284],[103.478676,-65.700485],[104.242557,-65.974783],[104.90846,-66.327527],[106.181561,-66.934931],[107.160881,-66.954568],[108.081393,-66.954568],[109.15864,-66.837004],[110.235835,-66.699804],[111.058472,-66.425505],[111.74396,-66.13157],[112.860378,-66.092347],[113.604673,-65.876805],[114.388088,-66.072762],[114.897308,-66.386283],[115.602381,-66.699804],[116.699161,-66.660633],[117.384701,-66.915346],[118.57946,-67.170111],[119.832924,-67.268089],[120.871,-67.189696],[121.654415,-66.876175],[122.320369,-66.562654],[123.221296,-66.484261],[124.122274,-66.621462],[125.160247,-66.719389],[126.100396,-66.562654],[127.001427,-66.562654],[127.882768,-66.660633],[128.80328,-66.758611],[129.704259,-66.58224],[130.781454,-66.425505],[131.799945,-66.386283],[132.935896,-66.386283],[133.85646,-66.288304],[134.757387,-66.209963],[135.031582,-65.72007],[135.070753,-65.308571],[135.697485,-65.582869],[135.873805,-66.033591],[136.206705,-66.44509],[136.618049,-66.778197],[137.460271,-66.954568],[138.596223,-66.895761],[139.908442,-66.876175],[140.809421,-66.817367],[142.121692,-66.817367],[143.061842,-66.797782],[144.374061,-66.837004],[145.490427,-66.915346],[146.195552,-67.228867],[145.999699,-67.601196],[146.646067,-67.895131],[147.723263,-68.130259],[148.839629,-68.385024],[150.132314,-68.561292],[151.483705,-68.71813],[152.502247,-68.874813],[153.638199,-68.894502],[154.284567,-68.561292],[155.165857,-68.835642],[155.92979,-69.149215],[156.811132,-69.384291],[158.025528,-69.482269],[159.181013,-69.599833],[159.670699,-69.991747],[160.80665,-70.226875],[161.570479,-70.579618],[162.686897,-70.736353],[163.842434,-70.716768],[164.919681,-70.775524],[166.11444,-70.755938],[167.309095,-70.834332],[168.425616,-70.971481],[169.463589,-71.20666],[170.501665,-71.402617],[171.20679,-71.696501],[171.089227,-72.088415],[170.560422,-72.441159],[170.109958,-72.891829],[169.75737,-73.24452],[169.287321,-73.65602],[167.975101,-73.812806],[167.387489,-74.165498],[166.094803,-74.38104],[165.644391,-74.772954],[164.958851,-75.145283],[164.234193,-75.458804],[163.822797,-75.870303],[163.568239,-76.24258],[163.47026,-76.693302],[163.489897,-77.065579],[164.057873,-77.457442],[164.273363,-77.82977],[164.743464,-78.182514],[166.604126,-78.319611],[166.995781,-78.750748],[165.193876,-78.907483],[163.666217,-79.123025],[161.766385,-79.162248],[160.924162,-79.730482],[160.747894,-80.200737],[160.316964,-80.573066],[159.788211,-80.945395],[161.120016,-81.278501],[161.629287,-81.690001],[162.490992,-82.062278],[163.705336,-82.395435],[165.095949,-82.708956],[166.604126,-83.022477],[168.895665,-83.335998],[169.404782,-83.825891],[172.283934,-84.041433],[172.477049,-84.117914],[173.224083,-84.41371],[175.985672,-84.158997],[178.277212,-84.472518],[180,-84.71338],[-179.942499,-84.721443],[-179.058677,-84.139412],[-177.256772,-84.452933],[-177.140807,-84.417941],[-176.084673,-84.099259],[-175.947235,-84.110449],[-175.829882,-84.117914],[-174.382503,-84.534323],[-173.116559,-84.117914],[-172.889106,-84.061019],[-169.951223,-83.884647],[-168.999989,-84.117914],[-168.530199,-84.23739],[-167.022099,-84.570497],[-164.182144,-84.82521],[-161.929775,-85.138731],[-158.07138,-85.37391],[-155.192253,-85.09956],[-150.942099,-85.295517],[-148.533073,-85.609038],[-145.888918,-85.315102],[-143.107718,-85.040752],[-142.892279,-84.570497],[-146.829068,-84.531274],[-150.060732,-84.296146],[-150.902928,-83.904232],[-153.586201,-83.68869],[-153.409907,-83.23802],[-153.037759,-82.82652],[-152.665637,-82.454192],[-152.861517,-82.042692],[-154.526299,-81.768394],[-155.29018,-81.41565],[-156.83745,-81.102129],[-154.408787,-81.160937],[-152.097662,-81.004151],[-150.648293,-81.337309],[-148.865998,-81.043373],[-147.22075,-80.671045],[-146.417749,-80.337938],[-146.770286,-79.926439],[-148.062947,-79.652089],[-149.531901,-79.358205],[-151.588416,-79.299397],[-153.390322,-79.162248],[-155.329376,-79.064269],[-155.975668,-78.69194],[-157.268302,-78.378419],[-158.051768,-78.025676],[-158.365134,-76.889207],[-157.875474,-76.987238],[-156.974573,-77.300759],[-155.329376,-77.202728],[-153.742832,-77.065579],[-152.920247,-77.496664],[-151.33378,-77.398737],[-150.00195,-77.183143],[-148.748486,-76.908845],[-147.612483,-76.575738],[-146.104409,-76.47776],[-146.143528,-76.105431],[-146.496091,-75.733154],[-146.20231,-75.380411],[-144.909624,-75.204039],[-144.322037,-75.537197],[-142.794353,-75.34124],[-141.638764,-75.086475],[-140.209007,-75.06689],[-138.85759,-74.968911],[-137.5062,-74.733783],[-136.428901,-74.518241],[-135.214583,-74.302699],[-134.431194,-74.361455],[-133.745654,-74.439848],[-132.257168,-74.302699],[-130.925311,-74.479019],[-129.554284,-74.459433],[-128.242038,-74.322284],[-126.890622,-74.420263],[-125.402082,-74.518241],[-124.011496,-74.479019],[-122.562152,-74.498604],[-121.073613,-74.518241],[-119.70256,-74.479019],[-118.684145,-74.185083],[-117.469801,-74.028348],[-116.216312,-74.243891],[-115.021552,-74.067519],[-113.944331,-73.714828],[-113.297988,-74.028348],[-112.945452,-74.38104],[-112.299083,-74.714198],[-111.261059,-74.420263],[-110.066325,-74.79254],[-108.714909,-74.910103],[-107.559346,-75.184454],[-106.149148,-75.125698],[-104.876074,-74.949326],[-103.367949,-74.988497],[-102.016507,-75.125698],[-100.645531,-75.302018],[-100.1167,-74.870933],[-100.763043,-74.537826],[-101.252703,-74.185083],[-102.545337,-74.106742],[-103.113313,-73.734413],[-103.328752,-73.362084],[-103.681289,-72.61753],[-102.917485,-72.754679],[-101.60524,-72.813436],[-100.312528,-72.754679],[-99.13738,-72.911414],[-98.118889,-73.20535],[-97.688037,-73.558041],[-96.336595,-73.616849],[-95.043961,-73.4797],[-93.672907,-73.283743],[-92.439003,-73.166179],[-91.420564,-73.401307],[-90.088733,-73.322914],[-89.226951,-72.558722],[-88.423951,-73.009393],[-87.268337,-73.185764],[-86.014822,-73.087786],[-85.192236,-73.4797],[-83.879991,-73.518871],[-82.665646,-73.636434],[-81.470913,-73.851977],[-80.687447,-73.4797],[-80.295791,-73.126956],[-79.296886,-73.518871],[-77.925858,-73.420892],[-76.907367,-73.636434],[-76.221879,-73.969541],[-74.890049,-73.871614],[-73.852024,-73.65602],[-72.833533,-73.401307],[-71.619215,-73.264157],[-70.209042,-73.146542],[-68.935916,-73.009393],[-67.956622,-72.79385],[-67.369061,-72.480329],[-67.134036,-72.049244],[-67.251548,-71.637745],[-67.56494,-71.245831],[-67.917477,-70.853917],[-68.230843,-70.462055],[-68.485452,-70.109311],[-68.544209,-69.717397],[-68.446282,-69.325535],[-67.976233,-68.953206],[-67.5845,-68.541707],[-67.427843,-68.149844],[-67.62367,-67.718759],[-67.741183,-67.326845],[-67.251548,-66.876175],[-66.703184,-66.58224],[-66.056815,-66.209963],[-65.371327,-65.89639],[-64.568276,-65.602506],[-64.176542,-65.171423],[-63.628152,-64.897073],[-63.001394,-64.642308],[-62.041686,-64.583552],[-61.414928,-64.270031],[-60.709855,-64.074074],[-59.887269,-63.95651],[-59.162585,-63.701745],[-58.594557,-63.388224],[-57.811143,-63.27066],[-57.223582,-63.525425],[-57.59573,-63.858532],[-58.614143,-64.152467]]]]}},
{"type":"Feature","id":"ATF","properties":{"name":"French Southern and Antarctic Lands"},"geometry":{"type":"Polygon","coordinates":[[[68.935,-48.625],[69.58,-48.94],[70.525,-49.065],[70.56,-49.255],[70.28,-49.71],[68.745,-49.775],[68.72,-49.2425],[68.8675,-48.83],[68.935,-48.625]]]}},
{"type":"Feature","id":"AUS","properties":{"name":"Australia"},"geometry":{"type":"MultiPolygon","coordinates":[[[[145.397978,-40.792549],[146.364121,-41.137695],[146.908584,-41.000546],[147.689259,-40.808258],[148.289068,-40.875438],[148.359865,-42.062445],[148.017301,-42.407024],[147.914052,-43.211522],[147.564564,-42.937689],[146.870343,-43.634597],[146.663327,-43.580854],[146.048378,-43.549745],[145.43193,-42.693776],[145.29509,-42.03361],[144.718071,-41.162552],[144.743755,-40.703975],[145.397978,-40.792549]]],[[[143.561811,-13.763656],[143.922099,-14.548311],[144.563714,-14.171176],[144.894908,-14.594458],[145.374724,-14.984976],[145.271991,-15.428205],[145.48526,-16.285672],[145.637033,-16.784918],[145.888904,-16.906926],[146.160309,-17.761655],[146.063674,-18.280073],[146.387478,-18.958274],[147.471082,-19.480723],[148.177602,-19.955939],[148.848414,-20.39121],[148.717465,-20.633469],[149.28942,-21.260511],[149.678337,-22.342512],[150.077382,-22.122784],[150.482939,-22.556142],[150.727265,-22.402405],[150.899554,-23.462237],[151.609175,-24.076256],[152.07354,-24.457887],[152.855197,-25.267501],[153.136162,-26.071173],[153.161949,-26.641319],[153.092909,-27.2603],[153.569469,-28.110067],[153.512108,-28.995077],[153.339095,-29.458202],[153.069241,-30.35024],[153.089602,-30.923642],[152.891578,-31.640446],[152.450002,-32.550003],[151.709117,-33.041342],[151.343972,-33.816023],[151.010555,-34.31036],[150.714139,-35.17346],[150.32822,-35.671879],[150.075212,-36.420206],[149.946124,-37.109052],[149.997284,-37.425261],[149.423882,-37.772681],[148.304622,-37.809061],[147.381733,-38.219217],[146.922123,-38.606532],[146.317922,-39.035757],[145.489652,-38.593768],[144.876976,-38.417448],[145.032212,-37.896188],[144.485682,-38.085324],[143.609974,-38.809465],[142.745427,-38.538268],[142.17833,-38.380034],[141.606582,-38.308514],[140.638579,-38.019333],[139.992158,-37.402936],[139.806588,-36.643603],[139.574148,-36.138362],[139.082808,-35.732754],[138.120748,-35.612296],[138.449462,-35.127261],[138.207564,-34.384723],[137.71917,-35.076825],[136.829406,-35.260535],[137.352371,-34.707339],[137.503886,-34.130268],[137.890116,-33.640479],[137.810328,-32.900007],[136.996837,-33.752771],[136.372069,-34.094766],[135.989043,-34.890118],[135.208213,-34.47867],[135.239218,-33.947953],[134.613417,-33.222778],[134.085904,-32.848072],[134.273903,-32.617234],[132.990777,-32.011224],[132.288081,-31.982647],[131.326331,-31.495803],[129.535794,-31.590423],[128.240938,-31.948489],[127.102867,-32.282267],[126.148714,-32.215966],[125.088623,-32.728751],[124.221648,-32.959487],[124.028947,-33.483847],[123.659667,-33.890179],[122.811036,-33.914467],[122.183064,-34.003402],[121.299191,-33.821036],[120.580268,-33.930177],[119.893695,-33.976065],[119.298899,-34.509366],[119.007341,-34.464149],[118.505718,-34.746819],[118.024972,-35.064733],[117.295507,-35.025459],[116.625109,-35.025097],[115.564347,-34.386428],[115.026809,-34.196517],[115.048616,-33.623425],[115.545123,-33.487258],[115.714674,-33.259572],[115.679379,-32.900369],[115.801645,-32.205062],[115.689611,-31.612437],[115.160909,-30.601594],[114.997043,-30.030725],[115.040038,-29.461095],[114.641974,-28.810231],[114.616498,-28.516399],[114.173579,-28.118077],[114.048884,-27.334765],[113.477498,-26.543134],[113.338953,-26.116545],[113.778358,-26.549025],[113.440962,-25.621278],[113.936901,-25.911235],[114.232852,-26.298446],[114.216161,-25.786281],[113.721255,-24.998939],[113.625344,-24.683971],[113.393523,-24.384764],[113.502044,-23.80635],[113.706993,-23.560215],[113.843418,-23.059987],[113.736552,-22.475475],[114.149756,-21.755881],[114.225307,-22.517488],[114.647762,-21.82952],[115.460167,-21.495173],[115.947373,-21.068688],[116.711615,-20.701682],[117.166316,-20.623599],[117.441545,-20.746899],[118.229559,-20.374208],[118.836085,-20.263311],[118.987807,-20.044203],[119.252494,-19.952942],[119.805225,-19.976506],[120.85622,-19.683708],[121.399856,-19.239756],[121.655138,-18.705318],[122.241665,-18.197649],[122.286624,-17.798603],[122.312772,-17.254967],[123.012574,-16.4052],[123.433789,-17.268558],[123.859345,-17.069035],[123.503242,-16.596506],[123.817073,-16.111316],[124.258287,-16.327944],[124.379726,-15.56706],[124.926153,-15.0751],[125.167275,-14.680396],[125.670087,-14.51007],[125.685796,-14.230656],[126.125149,-14.347341],[126.142823,-14.095987],[126.582589,-13.952791],[127.065867,-13.817968],[127.804633,-14.276906],[128.35969,-14.86917],[128.985543,-14.875991],[129.621473,-14.969784],[129.4096,-14.42067],[129.888641,-13.618703],[130.339466,-13.357376],[130.183506,-13.10752],[130.617795,-12.536392],[131.223495,-12.183649],[131.735091,-12.302453],[132.575298,-12.114041],[132.557212,-11.603012],[131.824698,-11.273782],[132.357224,-11.128519],[133.019561,-11.376411],[133.550846,-11.786515],[134.393068,-12.042365],[134.678632,-11.941183],[135.298491,-12.248606],[135.882693,-11.962267],[136.258381,-12.049342],[136.492475,-11.857209],[136.95162,-12.351959],[136.685125,-12.887223],[136.305407,-13.29123],[135.961758,-13.324509],[136.077617,-13.724278],[135.783836,-14.223989],[135.428664,-14.715432],[135.500184,-14.997741],[136.295175,-15.550265],[137.06536,-15.870762],[137.580471,-16.215082],[138.303217,-16.807604],[138.585164,-16.806622],[139.108543,-17.062679],[139.260575,-17.371601],[140.215245,-17.710805],[140.875463,-17.369069],[141.07111,-16.832047],[141.274095,-16.38887],[141.398222,-15.840532],[141.702183,-15.044921],[141.56338,-14.561333],[141.63552,-14.270395],[141.519869,-13.698078],[141.65092,-12.944688],[141.842691,-12.741548],[141.68699,-12.407614],[141.928629,-11.877466],[142.118488,-11.328042],[142.143706,-11.042737],[142.51526,-10.668186],[142.79731,-11.157355],[142.866763,-11.784707],[143.115947,-11.90563],[143.158632,-12.325656],[143.522124,-12.834358],[143.597158,-13.400422],[143.561811,-13.763656]]]]}},
{"type":"Feature","id":"AUT","properties":{"name":"Austria"},"geometry":{"type":"Polygon","coordinates":[[[16.979667,48.123497],[16.903754,47.714866],[16.340584,47.712902],[16.534268,47.496171],[16.202298,46.852386],[16.011664,46.683611],[15.137092,46.658703],[14.632472,46.431817],[13.806475,46.509306],[12.376485,46.767559],[12.153088,47.115393],[11.164828,46.941579],[11.048556,46.751359],[10.442701,46.893546],[9.932448,46.920728],[9.47997,47.10281],[9.632932,47.347601],[9.594226,47.525058],[9.896068,47.580197],[10.402084,47.302488],[10.544504,47.566399],[11.426414,47.523766],[12.141357,47.703083],[12.62076,47.672388],[12.932627,47.467646],[13.025851,47.637584],[12.884103,48.289146],[13.243357,48.416115],[13.595946,48.877172],[14.338898,48.555305],[14.901447,48.964402],[15.253416,49.039074],[16.029647,48.733899],[16.499283,48.785808],[16.960288,48.596982],[16.879983,48.470013],[16.979667,48.123497]]]}},
{"type":"Feature","id":"AZE","properties":{"name":"Azerbaijan"},"geometry":{"type":"MultiPolygon","coordinates":[[[[45.001987,39.740004],[45.298145,39.471751],[45.739978,39.473999],[45.735379,39.319719],[46.143623,38.741201],[45.457722,38.874139],[44.952688,39.335765],[44.79399,39.713003],[45.001987,39.740004]]],[[[47.373315,41.219732],[47.815666,41.151416],[47.987283,41.405819],[48.584353,41.80887],[49.110264,41.282287],[49.618915,40.572924],[50.08483,40.526157],[50.392821,40.256561],[49.569202,40.176101],[49.395259,39.399482],[49.223228,39.049219],[48.856532,38.815486],[48.883249,38.320245],[48.634375,38.270378],[48.010744,38.794015],[48.355529,39.288765],[48.060095,39.582235],[47.685079,39.508364],[46.50572,38.770605],[46.483499,39.464155],[46.034534,39.628021],[45.610012,39.899994],[45.891907,40.218476],[45.359175,40.561504],[45.560351,40.81229],[45.179496,40.985354],[44.97248,41.248129],[45.217426,41.411452],[45.962601,41.123873],[46.501637,41.064445],[46.637908,41.181673],[46.145432,41.722802],[46.404951,41.860675],[46.686071,41.827137],[47.373315,41.219732]]]]}},
{"type":"Feature","id":"BDI","properties":{"name":"Burundi"},"geometry":{"type":"Polygon","coordinates":[[[29.339998,-4.499983],[29.276384,-3.293907],[29.024926,-2.839258],[29.632176,-2.917858],[29.938359,-2.348487],[30.469696,-2.413858],[30.527677,-2.807632],[30.743013,-3.034285],[30.752263,-3.35933],[30.50556,-3.568567],[30.116333,-4.090138],[29.753512,-4.452389],[29.339998,-4.499983]]]}},
{"type":"Feature","id":"BEL","properties":{"name":"Belgium"},"geometry":{"type":"Polygon","coordinates":[[[3.314971,51.345781],[4.047071,51.267259],[4.973991,51.475024],[5.606976,51.037298],[6.156658,50.803721],[6.043073,50.128052],[5.782417,50.090328],[5.674052,49.529484],[4.799222,49.985373],[4.286023,49.907497],[3.588184,50.378992],[3.123252,50.780363],[2.658422,50.796848],[2.513573,51.148506],[3.314971,51.345781]]]}},
{"type":"Feature","id":"BEN","properties":{"name":"Benin"},"geometry":{"type":"Polygon","coordinates":[[[2.691702,6.258817],[1.865241,6.142158],[1.618951,6.832038],[1.664478,9.12859],[1.463043,9.334624],[1.425061,9.825395],[1.077795,10.175607],[0.772336,10.470808],[0.899563,10.997339],[1.24347,11.110511],[1.447178,11.547719],[1.935986,11.64115],[2.154474,11.94015],[2.490164,12.233052],[2.848643,12.235636],[3.61118,11.660167],[3.572216,11.327939],[3.797112,10.734746],[3.60007,10.332186],[3.705438,10.06321],[3.220352,9.444153],[2.912308,9.137608],[2.723793,8.506845],[2.749063,7.870734],[2.691702,6.258817]]]}},
{"type":"Feature","id":"BFA","properties":{"name":"Burkina Faso"},"geometry":{"type":"Polygon","coordinates":[[[-2.827496,9.642461],[-3.511899,9.900326],[-3.980449,9.862344],[-4.330247,9.610835],[-4.779884,9.821985],[-4.954653,10.152714],[-5.404342,10.370737],[-5.470565,10.95127],[-5.197843,11.375146],[-5.220942,11.713859],[-4.427166,12.542646],[-4.280405,13.228444],[-4.006391,13.472485],[-3.522803,13.337662],[-3.103707,13.541267],[-2.967694,13.79815],[-2.191825,14.246418],[-2.001035,14.559008],[-1.066363,14.973815],[-0.515854,15.116158],[-0.266257,14.924309],[0.374892,14.928908],[0.295646,14.444235],[0.429928,13.988733],[0.993046,13.33575],[1.024103,12.851826],[2.177108,12.625018],[2.154474,11.94015],[1.935986,11.64115],[1.447178,11.547719],[1.24347,11.110511],[0.899563,10.997339],[0.023803,11.018682],[-0.438702,11.098341],[-0.761576,10.93693],[-1.203358,11.009819],[-2.940409,10.96269],[-2.963896,10.395335],[-2.827496,9.642461]]]}},
{"type":"Feature","id":"BGD","properties":{"name":"Bangladesh"},"geometry":{"type":"Polygon","coordinates":[[[92.672721,22.041239],[92.652257,21.324048],[92.303234,21.475485],[92.368554,20.670883],[92.082886,21.192195],[92.025215,21.70157],[91.834891,22.182936],[91.417087,22.765019],[90.496006,22.805017],[90.586957,22.392794],[90.272971,21.836368],[89.847467,22.039146],[89.70205,21.857116],[89.418863,21.966179],[89.031961,22.055708],[88.876312,22.879146],[88.52977,23.631142],[88.69994,24.233715],[88.084422,24.501657],[88.306373,24.866079],[88.931554,25.238692],[88.209789,25.768066],[88.563049,26.446526],[89.355094,26.014407],[89.832481,25.965082],[89.920693,25.26975],[90.872211,25.132601],[91.799596,25.147432],[92.376202,24.976693],[91.915093,24.130414],[91.46773,24.072639],[91.158963,23.503527],[91.706475,22.985264],[91.869928,23.624346],[92.146035,23.627499],[92.672721,22.041239]]]}},
{"type":"Feature","id":"BGR","properties":{"name":"Bulgaria"},"geometry":{"type":"Polygon","coordinates":[[[22.65715,44.234923],[22.944832,43.823785],[23.332302,43.897011],[24.100679,43.741051],[25.569272,43.688445],[26.065159,43.943494],[27.2424,44.175986],[27.970107,43.812468],[28.558081,43.707462],[28.039095,43.293172],[27.673898,42.577892],[27.99672,42.007359],[27.135739,42.141485],[26.117042,41.826905],[26.106138,41.328899],[25.197201,41.234486],[24.492645,41.583896],[23.692074,41.309081],[22.952377,41.337994],[22.881374,41.999297],[22.380526,42.32026],[22.545012,42.461362],[22.436595,42.580321],[22.604801,42.898519],[22.986019,43.211161],[22.500157,43.642814],[22.410446,44.008063],[22.65715,44.234923]]]}},
{"type":"Feature","id":"BHS","properties":{"name":"The Bahamas"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-77.53466,23.75975],[-77.78,23.71],[-78.03405,24.28615],[-78.40848,24.57564],[-78.19087,25.2103],[-77.89,25.17],[-77.54,24.34],[-77.53466,23.75975]]],[[[-77.82,26.58],[-78.91,26.42],[-78.98,26.79],[-78.51,26.87],[-77.85,26.84],[-77.82,26.58]]],[[[-77,26.59],[-77.17255,25.87918],[-77.35641,26.00735],[-77.34,26.53],[-77.78802,26.92516],[-77.79,27.04],[-77,26.59]]]]}},
{"type":"Feature","id":"BIH","properties":{"name":"Bosnia and Herzegovina"},"geometry":{"type":"Polygon","coordinates":[[[19.005486,44.860234],[19.36803,44.863],[19.11761,44.42307],[19.59976,44.03847],[19.454,43.5681],[19.21852,43.52384],[19.03165,43.43253],[18.70648,43.20011],[18.56,42.65],[17.674922,43.028563],[17.297373,43.446341],[16.916156,43.667722],[16.456443,44.04124],[16.23966,44.351143],[15.750026,44.818712],[15.959367,45.233777],[16.318157,45.004127],[16.534939,45.211608],[17.002146,45.233777],[17.861783,45.06774],[18.553214,45.08159],[19.005486,44.860234]]]}},
{"type":"Feature","id":"BLR","properties":{"name":"Belarus"},"geometry":{"type":"Polygon","coordinates":[[[23.484128,53.912498],[24.450684,53.905702],[25.536354,54.282423],[25.768433,54.846963],[26.588279,55.167176],[26.494331,55.615107],[27.10246,55.783314],[28.176709,56.16913],[29.229513,55.918344],[29.371572,55.670091],[29.896294,55.789463],[30.873909,55.550976],[30.971836,55.081548],[30.757534,54.811771],[31.384472,54.157056],[31.791424,53.974639],[31.731273,53.794029],[32.405599,53.618045],[32.693643,53.351421],[32.304519,53.132726],[31.497644,53.167427],[31.305201,53.073996],[31.540018,52.742052],[31.785998,52.101678],[30.927549,52.042353],[30.619454,51.822806],[30.555117,51.319503],[30.157364,51.416138],[29.254938,51.368234],[28.992835,51.602044],[28.617613,51.427714],[28.241615,51.572227],[27.454066,51.592303],[26.337959,51.832289],[25.327788,51.910656],[24.553106,51.888461],[24.005078,51.617444],[23.527071,51.578454],[23.508002,52.023647],[23.199494,52.486977],[23.799199,52.691099],[23.804935,53.089731],[23.527536,53.470122],[23.484128,53.912498]]]}},
{"type":"Feature","id":"BLZ","properties":{"name":"Belize"},"geometry":{"type":"Polygon","coordinates":[[[-89.14308,17.808319],[-89.150909,17.955468],[-89.029857,18.001511],[-88.848344,17.883198],[-88.490123,18.486831],[-88.300031,18.499982],[-88.296336,18.353273],[-88.106813,18.348674],[-88.123479,18.076675],[-88.285355,17.644143],[-88.197867,17.489475],[-88.302641,17.131694],[-88.239518,17.036066],[-88.355428,16.530774],[-88.551825,16.265467],[-88.732434,16.233635],[-88.930613,15.887273],[-89.229122,15.886938],[-89.150806,17.015577],[-89.14308,17.808319]]]}},
{"type":"Feature","id":"BMU","properties":{"name":"Bermuda"},"geometry":{"type":"Polygon","coordinates":[[[-64.7799734332998,32.3072000581802],[-64.7873319183061,32.3039237143428],[-64.7946942710173,32.3032682700388],[-64.8094297981283,32.3098175728414],[-64.8167896352437,32.3058845718466],[-64.8101968029642,32.3022833180511],[-64.7962291465484,32.2934409732427],[-64.7815086336978,32.2868973114514],[-64.7997025513437,32.2796896417328],[-64.8066707691087,32.2747767569465],[-64.8225587873683,32.2669111289395],[-64.8287548840306,32.2669075473817],[-64.8306732143498,32.2583944840235],[-64.8399924854972,32.254782282336],[-64.8566090462354,32.2547740387514],[-64.8682296789446,32.2616393614322],[-64.8628241459563,32.2724481933959],[-64.8748651338951,32.2757120264753],[-64.8717752856644,32.2819371582026],[-64.8671422127295,32.2930760547989],[-64.8559068764437,32.2960321186471],[-64.8597429072279,32.3015842021933],[-64.8439233486717,32.3140553852543],[-64.8350242329311,32.3242161760006],[-64.8338690593672,32.3294587561557],[-64.8520298651164,32.3110911879954],[-64.8635922932573,32.3048469433363],[-64.8686668994079,32.30910745083],[-64.8721354593415,32.3041908606301],[-64.8779667328485,32.3038632800462],[-64.8780046844321,32.2907757831692],[-64.8849776658292,32.2819261366004],[-64.8783230004629,32.2613001418681],[-64.863194968877,32.2465799485801],[-64.8519819555722,32.2485519134663],[-64.842311980074,32.2492123317296],[-64.8388242605209,32.2475773472534],[-64.8334002575532,32.2462714714698],[-64.8256389530584,32.2472637398594],[-64.8205697556026,32.2531698880328],[-64.8105087275579,32.2561208974156],[-64.7900177727338,32.2659446936992],[-64.7745415970416,32.2718413023427],[-64.7644742436426,32.2855931353214],[-64.7551803442276,32.2908326702531],[-64.7423982971436,32.2996734994024],[-64.7206991797682,32.3137542201258],[-64.7117851247134,32.3176823360806],[-64.6962778813133,32.3275029115532],[-64.6768921127452,32.3324095397555],[-64.6567136927777,32.3451776458469],[-64.6532168823499,32.3494356627941],[-64.6605720384429,32.3589423487763],[-64.65125819471,32.3615600906466],[-64.6462011670816,32.36975169749],[-64.6613227512832,32.3763135008721],[-64.6690666074397,32.388444543924],[-64.6834270548595,32.3854968316788],[-64.6954617672714,32.3763221285869],[-64.70438689565,32.3704254760469],[-64.7117569982798,32.368132600249],[-64.7061764744404,32.3600110593559],[-64.700531552697,32.3590601356818],[-64.6940348033967,32.3640708659835],[-64.6895164826082,32.3633598579866],[-64.6864150099255,32.3547797587266],[-64.6824635995504,32.3540628176846],[-64.6835876652835,32.3626447677968],[-64.6801998697415,32.3631199096979],[-64.6672170444687,32.3597751617473],[-64.6598811264978,32.3497625771755],[-64.6737331235384,32.3390281851635],[-64.6887090648183,32.3342439408053],[-64.706732854446,32.3429010723036],[-64.7149301576112,32.3552188753513],[-64.7185967666669,32.3552239212394],[-64.7214189847314,32.3518830231342],[-64.7270616067222,32.3466461715475],[-64.734962460882,32.3442819830499],[-64.7383521549094,32.3407216514918],[-64.7411729976333,32.3311790864627],[-64.7423019216485,32.323311561213],[-64.7462482354281,32.318538611581],[-64.7566773739613,32.3130509130175],[-64.768738200563,32.3088369816572],[-64.7799734332998,32.3072000581802]]]}},
{"type":"Feature","id":"BOL","properties":{"name":"Bolivia"},"geometry":{"type":"Polygon","coordinates":[[[-62.846468,-22.034985],[-63.986838,-21.993644],[-64.377021,-22.798091],[-64.964892,-22.075862],[-66.273339,-21.83231],[-67.106674,-22.735925],[-67.82818,-22.872919],[-68.219913,-21.494347],[-68.757167,-20.372658],[-68.442225,-19.405068],[-68.966818,-18.981683],[-69.100247,-18.260125],[-69.590424,-17.580012],[-68.959635,-16.500698],[-69.389764,-15.660129],[-69.160347,-15.323974],[-69.339535,-14.953195],[-68.948887,-14.453639],[-68.929224,-13.602684],[-68.88008,-12.899729],[-68.66508,-12.5613],[-69.529678,-10.951734],[-68.786158,-11.03638],[-68.271254,-11.014521],[-68.048192,-10.712059],[-67.173801,-10.306812],[-66.646908,-9.931331],[-65.338435,-9.761988],[-65.444837,-10.511451],[-65.321899,-10.895872],[-65.402281,-11.56627],[-64.316353,-12.461978],[-63.196499,-12.627033],[-62.80306,-13.000653],[-62.127081,-13.198781],[-61.713204,-13.489202],[-61.084121,-13.479384],[-60.503304,-13.775955],[-60.459198,-14.354007],[-60.264326,-14.645979],[-60.251149,-15.077219],[-60.542966,-15.09391],[-60.15839,-16.258284],[-58.24122,-16.299573],[-58.388058,-16.877109],[-58.280804,-17.27171],[-57.734558,-17.552468],[-57.498371,-18.174188],[-57.676009,-18.96184],[-57.949997,-19.400004],[-57.853802,-19.969995],[-58.166392,-20.176701],[-58.183471,-19.868399],[-59.115042,-19.356906],[-60.043565,-19.342747],[-61.786326,-19.633737],[-62.265961,-20.513735],[-62.291179,-21.051635],[-62.685057,-22.249029],[-62.846468,-22.034985]]]}},
{"type":"Feature","id":"BRA","properties":{"name":"Brazil"},"geometry":{"type":"Polygon","coordinates":[[[-57.625133,-30.216295],[-56.2909,-28.852761],[-55.162286,-27.881915],[-54.490725,-27.474757],[-53.648735,-26.923473],[-53.628349,-26.124865],[-54.13005,-25.547639],[-54.625291,-25.739255],[-54.428946,-25.162185],[-54.293476,-24.5708],[-54.29296,-24.021014],[-54.652834,-23.839578],[-55.027902,-24.001274],[-55.400747,-23.956935],[-55.517639,-23.571998],[-55.610683,-22.655619],[-55.797958,-22.35693],[-56.473317,-22.0863],[-56.88151,-22.282154],[-57.937156,-22.090176],[-57.870674,-20.732688],[-58.166392,-20.176701],[-57.853802,-19.969995],[-57.949997,-19.400004],[-57.676009,-18.96184],[-57.498371,-18.174188],[-57.734558,-17.552468],[-58.280804,-17.27171],[-58.388058,-16.877109],[-58.24122,-16.299573],[-60.15839,-16.258284],[-60.542966,-15.09391],[-60.251149,-15.077219],[-60.264326,-14.645979],[-60.459198,-14.354007],[-60.503304,-13.775955],[-61.084121,-13.479384],[-61.713204,-13.489202],[-62.127081,-13.198781],[-62.80306,-13.000653],[-63.196499,-12.627033],[-64.316353,-12.461978],[-65.402281,-11.56627],[-65.321899,-10.895872],[-65.444837,-10.511451],[-65.338435,-9.761988],[-66.646908,-9.931331],[-67.173801,-10.306812],[-68.048192,-10.712059],[-68.271254,-11.014521],[-68.786158,-11.03638],[-69.529678,-10.951734],[-70.093752,-11.123972],[-70.548686,-11.009147],[-70.481894,-9.490118],[-71.302412,-10.079436],[-72.184891,-10.053598],[-72.563033,-9.520194],[-73.226713,-9.462213],[-73.015383,-9.032833],[-73.571059,-8.424447],[-73.987235,-7.52383],[-73.723401,-7.340999],[-73.724487,-6.918595],[-73.120027,-6.629931],[-73.219711,-6.089189],[-72.964507,-5.741251],[-72.891928,-5.274561],[-71.748406,-4.593983],[-70.928843,-4.401591],[-70.794769,-4.251265],[-69.893635,-4.298187],[-69.444102,-1.556287],[-69.420486,-1.122619],[-69.577065,-0.549992],[-70.020656,-0.185156],[-70.015566,0.541414],[-69.452396,0.706159],[-69.252434,0.602651],[-69.218638,0.985677],[-69.804597,1.089081],[-69.816973,1.714805],[-67.868565,1.692455],[-67.53781,2.037163],[-67.259998,1.719999],[-67.065048,1.130112],[-66.876326,1.253361],[-66.325765,0.724452],[-65.548267,0.789254],[-65.354713,1.095282],[-64.611012,1.328731],[-64.199306,1.492855],[-64.083085,1.916369],[-63.368788,2.2009],[-63.422867,2.411068],[-64.269999,2.497006],[-64.408828,3.126786],[-64.368494,3.79721],[-64.816064,4.056445],[-64.628659,4.148481],[-63.888343,4.02053],[-63.093198,3.770571],[-62.804533,4.006965],[-62.08543,4.162124],[-60.966893,4.536468],[-60.601179,4.918098],[-60.733574,5.200277],[-60.213683,5.244486],[-59.980959,5.014061],[-60.111002,4.574967],[-59.767406,4.423503],[-59.53804,3.958803],[-59.815413,3.606499],[-59.974525,2.755233],[-59.718546,2.24963],[-59.646044,1.786894],[-59.030862,1.317698],[-58.540013,1.268088],[-58.429477,1.463942],[-58.11345,1.507195],[-57.660971,1.682585],[-57.335823,1.948538],[-56.782704,1.863711],[-56.539386,1.899523],[-55.995698,1.817667],[-55.9056,2.021996],[-56.073342,2.220795],[-55.973322,2.510364],[-55.569755,2.421506],[-55.097587,2.523748],[-54.524754,2.311849],[-54.088063,2.105557],[-53.778521,2.376703],[-53.554839,2.334897],[-53.418465,2.053389],[-52.939657,2.124858],[-52.556425,2.504705],[-52.249338,3.241094],[-51.657797,4.156232],[-51.317146,4.203491],[-51.069771,3.650398],[-50.508875,1.901564],[-49.974076,1.736483],[-49.947101,1.04619],[-50.699251,0.222984],[-50.388211,-0.078445],[-48.620567,-0.235489],[-48.584497,-1.237805],[-47.824956,-0.581618],[-46.566584,-0.941028],[-44.905703,-1.55174],[-44.417619,-2.13775],[-44.581589,-2.691308],[-43.418791,-2.38311],[-41.472657,-2.912018],[-39.978665,-2.873054],[-38.500383,-3.700652],[-37.223252,-4.820946],[-36.452937,-5.109404],[-35.597796,-5.149504],[-35.235389,-5.464937],[-34.89603,-6.738193],[-34.729993,-7.343221],[-35.128212,-8.996401],[-35.636967,-9.649282],[-37.046519,-11.040721],[-37.683612,-12.171195],[-38.423877,-13.038119],[-38.673887,-13.057652],[-38.953276,-13.79337],[-38.882298,-15.667054],[-39.161092,-17.208407],[-39.267339,-17.867746],[-39.583521,-18.262296],[-39.760823,-19.599113],[-40.774741,-20.904512],[-40.944756,-21.937317],[-41.754164,-22.370676],[-41.988284,-22.97007],[-43.074704,-22.967693],[-44.647812,-23.351959],[-45.352136,-23.796842],[-46.472093,-24.088969],[-47.648972,-24.885199],[-48.495458,-25.877025],[-48.641005,-26.623698],[-48.474736,-27.175912],[-48.66152,-28.186135],[-48.888457,-28.674115],[-49.587329,-29.224469],[-50.696874,-30.984465],[-51.576226,-31.777698],[-52.256081,-32.24537],[-52.7121,-33.196578],[-53.373662,-33.768378],[-53.650544,-33.202004],[-53.209589,-32.727666],[-53.787952,-32.047243],[-54.572452,-31.494511],[-55.60151,-30.853879],[-55.973245,-30.883076],[-56.976026,-30.109686],[-57.625133,-30.216295]]]}},
{"type":"Feature","id":"BRN","properties":{"name":"Brunei"},"geometry":{"type":"Polygon","coordinates":[[[114.204017,4.525874],[114.599961,4.900011],[115.45071,5.44773],[115.4057,4.955228],[115.347461,4.316636],[114.869557,4.348314],[114.659596,4.007637],[114.204017,4.525874]]]}},
{"type":"Feature","id":"BTN","properties":{"name":"Bhutan"},"geometry":{"type":"Polygon","coordinates":[[[91.696657,27.771742],[92.103712,27.452614],[92.033484,26.83831],[91.217513,26.808648],[90.373275,26.875724],[89.744528,26.719403],[88.835643,27.098966],[88.814248,27.299316],[89.47581,28.042759],[90.015829,28.296439],[90.730514,28.064954],[91.258854,28.040614],[91.696657,27.771742]]]}},
{"type":"Feature","id":"BWA","properties":{"name":"Botswana"},"geometry":{"type":"Polygon","coordinates":[[[25.649163,-18.536026],[25.850391,-18.714413],[26.164791,-19.293086],[27.296505,-20.39152],[27.724747,-20.499059],[27.727228,-20.851802],[28.02137,-21.485975],[28.794656,-21.639454],[29.432188,-22.091313],[28.017236,-22.827754],[27.11941,-23.574323],[26.786407,-24.240691],[26.485753,-24.616327],[25.941652,-24.696373],[25.765849,-25.174845],[25.664666,-25.486816],[25.025171,-25.71967],[24.211267,-25.670216],[23.73357,-25.390129],[23.312097,-25.26869],[22.824271,-25.500459],[22.579532,-25.979448],[22.105969,-26.280256],[21.605896,-26.726534],[20.889609,-26.828543],[20.66647,-26.477453],[20.758609,-25.868136],[20.165726,-24.917962],[19.895768,-24.76779],[19.895458,-21.849157],[20.881134,-21.814327],[20.910641,-18.252219],[21.65504,-18.219146],[23.196858,-17.869038],[23.579006,-18.281261],[24.217365,-17.889347],[24.520705,-17.887125],[25.084443,-17.661816],[25.264226,-17.73654],[25.649163,-18.536026]]]}},
{"type":"Feature","id":"CAF","properties":{"name":"Central African Republic"},"geometry":{"type":"Polygon","coordinates":[[[15.27946,7.421925],[16.106232,7.497088],[16.290562,7.754307],[16.456185,7.734774],[16.705988,7.508328],[17.96493,7.890914],[18.389555,8.281304],[18.911022,8.630895],[18.81201,8.982915],[19.094008,9.074847],[20.059685,9.012706],[21.000868,9.475985],[21.723822,10.567056],[22.231129,10.971889],[22.864165,11.142395],[22.977544,10.714463],[23.554304,10.089255],[23.55725,9.681218],[23.394779,9.265068],[23.459013,8.954286],[23.805813,8.666319],[24.567369,8.229188],[25.114932,7.825104],[25.124131,7.500085],[25.796648,6.979316],[26.213418,6.546603],[26.465909,5.946717],[27.213409,5.550953],[27.374226,5.233944],[27.044065,5.127853],[26.402761,5.150875],[25.650455,5.256088],[25.278798,5.170408],[25.128833,4.927245],[24.805029,4.897247],[24.410531,5.108784],[23.297214,4.609693],[22.84148,4.710126],[22.704124,4.633051],[22.405124,4.02916],[21.659123,4.224342],[20.927591,4.322786],[20.290679,4.691678],[19.467784,5.031528],[18.932312,4.709506],[18.542982,4.201785],[18.453065,3.504386],[17.8099,3.560196],[17.133042,3.728197],[16.537058,3.198255],[16.012852,2.26764],[15.907381,2.557389],[15.862732,3.013537],[15.405396,3.335301],[15.03622,3.851367],[14.950953,4.210389],[14.478372,4.732605],[14.558936,5.030598],[14.459407,5.451761],[14.53656,6.226959],[14.776545,6.408498],[15.27946,7.421925]]]}},
{"type":"Feature","id":"CAN","properties":{"name":"Canada"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-63.6645,46.55001],[-62.9393,46.41587],[-62.01208,46.44314],[-62.50391,46.03339],[-62.87433,45.96818],[-64.1428,46.39265],[-64.39261,46.72747],[-64.01486,47.03601],[-63.6645,46.55001]]],[[[-61.806305,49.10506],[-62.29318,49.08717],[-63.58926,49.40069],[-64.51912,49.87304],[-64.17322,49.95718],[-62.85829,49.70641],[-61.835585,49.28855],[-61.806305,49.10506]]],[[[-123.510002,48.510011],[-124.012891,48.370846],[-125.655013,48.825005],[-125.954994,49.179996],[-126.850004,49.53],[-127.029993,49.814996],[-128.059336,49.994959],[-128.444584,50.539138],[-128.358414,50.770648],[-127.308581,50.552574],[-126.695001,50.400903],[-125.755007,50.295018],[-125.415002,49.950001],[-124.920768,49.475275],[-123.922509,49.062484],[-123.510002,48.510011]]],[[[-56.134036,50.68701],[-56.795882,49.812309],[-56.143105,50.150117],[-55.471492,49.935815],[-55.822401,49.587129],[-54.935143,49.313011],[-54.473775,49.556691],[-53.476549,49.249139],[-53.786014,48.516781],[-53.086134,48.687804],[-52.958648,48.157164],[-52.648099,47.535548],[-53.069158,46.655499],[-53.521456,46.618292],[-54.178936,46.807066],[-53.961869,47.625207],[-54.240482,47.752279],[-55.400773,46.884994],[-55.997481,46.91972],[-55.291219,47.389562],[-56.250799,47.632545],[-57.325229,47.572807],[-59.266015,47.603348],[-59.419494,47.899454],[-58.796586,48.251525],[-59.231625,48.523188],[-58.391805,49.125581],[-57.35869,50.718274],[-56.73865,51.287438],[-55.870977,51.632094],[-55.406974,51.588273],[-55.600218,51.317075],[-56.134036,50.68701]]],[[[-132.710008,54.040009],[-132.710009,54.040009],[-132.710008,54.040009],[-132.710008,54.040009],[-131.74999,54.120004],[-132.04948,52.984621],[-131.179043,52.180433],[-131.57783,52.182371],[-132.180428,52.639707],[-132.549992,53.100015],[-133.054611,53.411469],[-133.239664,53.85108],[-133.180004,54.169975],[-132.710008,54.040009]]],[[[-79.26582,62.158675],[-79.65752,61.63308],[-80.09956,61.7181],[-80.36215,62.01649],[-80.315395,62.085565],[-79.92939,62.3856],[-79.52002,62.36371],[-79.26582,62.158675]]],[[[-81.89825,62.7108],[-83.06857,62.15922],[-83.77462,62.18231],[-83.99367,62.4528],[-83.25048,62.91409],[-81.87699,62.90458],[-81.89825,62.7108]]],[[[-85.161308,65.657285],[-84.975764,65.217518],[-84.464012,65.371772],[-83.882626,65.109618],[-82.787577,64.766693],[-81.642014,64.455136],[-81.55344,63.979609],[-80.817361,64.057486],[-80.103451,63.725981],[-80.99102,63.411246],[-82.547178,63.651722],[-83.108798,64.101876],[-84.100417,63.569712],[-85.523405,63.052379],[-85.866769,63.637253],[-87.221983,63.541238],[-86.35276,64.035833],[-86.224886,64.822917],[-85.883848,65.738778],[-85.161308,65.657285]]],[[[-75.86588,67.14886],[-76.98687,67.09873],[-77.2364,67.58809],[-76.81166,68.14856],[-75.89521,68.28721],[-75.1145,68.01036],[-75.10333,67.58202],[-75.21597,67.44425],[-75.86588,67.14886]]],[[[-95.647681,69.10769],[-96.269521,68.75704],[-97.617401,69.06003],[-98.431801,68.9507],[-99.797401,69.40003],[-98.917401,69.71003],[-98.218261,70.14354],[-97.157401,69.86003],[-96.557401,69.68003],[-96.257401,69.49003],[-95.647681,69.10769]]],[[[-90.5471,69.49766],[-90.55151,68.47499],[-89.21515,69.25873],[-88.01966,68.61508],[-88.31749,67.87338],[-87.35017,67.19872],[-86.30607,67.92146],[-85.57664,68.78456],[-85.52197,69.88211],[-84.10081,69.80539],[-82.62258,69.65826],[-81.28043,69.16202],[-81.2202,68.66567],[-81.96436,68.13253],[-81.25928,67.59716],[-81.38653,67.11078],[-83.34456,66.41154],[-84.73542,66.2573],[-85.76943,66.55833],[-86.0676,66.05625],[-87.03143,65.21297],[-87.32324,64.77563],[-88.48296,64.09897],[-89.91444,64.03273],[-90.70398,63.61017],[-90.77004,62.96021],[-91.93342,62.83508],[-93.15698,62.02469],[-94.24153,60.89865],[-94.62931,60.11021],[-94.6846,58.94882],[-93.21502,58.78212],[-92.76462,57.84571],[-92.29703,57.08709],[-90.89769,57.28468],[-89.03953,56.85172],[-88.03978,56.47162],[-87.32421,55.99914],[-86.07121,55.72383],[-85.01181,55.3026],[-83.36055,55.24489],[-82.27285,55.14832],[-82.4362,54.28227],[-82.12502,53.27703],[-81.40075,52.15788],[-79.91289,51.20842],[-79.14301,51.53393],[-78.60191,52.56208],[-79.12421,54.14145],[-79.82958,54.66772],[-78.22874,55.13645],[-77.0956,55.83741],[-76.54137,56.53423],[-76.62319,57.20263],[-77.30226,58.05209],[-78.51688,58.80458],[-77.33676,59.85261],[-77.77272,60.75788],[-78.10687,62.31964],[-77.41067,62.55053],[-75.69621,62.2784],[-74.6682,62.18111],[-73.83988,62.4438],[-72.90853,62.10507],[-71.67708,61.52535],[-71.37369,61.13717],[-69.59042,61.06141],[-69.62033,60.22125],[-69.2879,58.95736],[-68.37455,58.80106],[-67.64976,58.21206],[-66.20178,58.76731],[-65.24517,59.87071],[-64.58352,60.33558],[-63.80475,59.4426],[-62.50236,58.16708],[-61.39655,56.96745],[-61.79866,56.33945],[-60.46853,55.77548],[-59.56962,55.20407],[-57.97508,54.94549],[-57.3332,54.6265],[-56.93689,53.78032],[-56.15811,53.64749],[-55.75632,53.27036],[-55.68338,52.14664],[-56.40916,51.7707],[-57.12691,51.41972],[-58.77482,51.0643],[-60.03309,50.24277],[-61.72366,50.08046],[-63.86251,50.29099],[-65.36331,50.2982],[-66.39905,50.22897],[-67.23631,49.51156],[-68.51114,49.06836],[-69.95362,47.74488],[-71.10458,46.82171],[-70.25522,46.98606],[-68.65,48.3],[-66.55243,49.1331],[-65.05626,49.23278],[-64.17099,48.74248],[-65.11545,48.07085],[-64.79854,46.99297],[-64.47219,46.23849],[-63.17329,45.73902],[-61.52072,45.88377],[-60.51815,47.00793],[-60.4486,46.28264],[-59.80287,45.9204],[-61.03988,45.26525],[-63.25471,44.67014],[-64.24656,44.26553],[-65.36406,43.54523],[-66.1234,43.61867],[-66.16173,44.46512],[-64.42549,45.29204],[-66.02605,45.25931],[-67.13741,45.13753],[-67.79134,45.70281],[-67.79046,47.06636],[-68.23444,47.35486],[-68.905,47.185],[-69.237216,47.447781],[-69.99997,46.69307],[-70.305,45.915],[-70.66,45.46],[-71.08482,45.30524],[-71.405,45.255],[-71.50506,45.0082],[-73.34783,45.00738],[-74.867,45.00048],[-75.31821,44.81645],[-76.375,44.09631],[-76.5,44.018459],[-76.820034,43.628784],[-77.737885,43.629056],[-78.72028,43.625089],[-79.171674,43.466339],[-79.01,43.27],[-78.92,42.965],[-78.939362,42.863611],[-80.247448,42.3662],[-81.277747,42.209026],[-82.439278,41.675105],[-82.690089,41.675105],[-83.02981,41.832796],[-83.142,41.975681],[-83.12,42.08],[-82.9,42.43],[-82.43,42.98],[-82.137642,43.571088],[-82.337763,44.44],[-82.550925,45.347517],[-83.592851,45.816894],[-83.469551,45.994686],[-83.616131,46.116927],[-83.890765,46.116927],[-84.091851,46.275419],[-84.14212,46.512226],[-84.3367,46.40877],[-84.6049,46.4396],[-84.543749,46.538684],[-84.779238,46.637102],[-84.87608,46.900083],[-85.652363,47.220219],[-86.461991,47.553338],[-87.439793,47.94],[-88.378114,48.302918],[-89.272917,48.019808],[-89.6,48.01],[-90.83,48.27],[-91.64,48.14],[-92.61,48.45],[-93.63087,48.60926],[-94.32914,48.67074],[-94.64,48.84],[-94.81758,49.38905],[-95.15609,49.38425],[-95.15907,49],[-97.22872,49.0007],[-100.65,49],[-104.04826,48.99986],[-107.05,49],[-110.05,49],[-113,49],[-116.04818,49],[-117.03121,49],[-120,49],[-122.84,49],[-122.97421,49.002538],[-124.91024,49.98456],[-125.62461,50.41656],[-127.43561,50.83061],[-127.99276,51.71583],[-127.85032,52.32961],[-129.12979,52.75538],[-129.30523,53.56159],[-130.51497,54.28757],[-130.53611,54.80278],[-129.98,55.285],[-130.00778,55.91583],[-131.70781,56.55212],[-132.73042,57.69289],[-133.35556,58.41028],[-134.27111,58.86111],[-134.945,59.27056],[-135.47583,59.78778],[-136.47972,59.46389],[-137.4525,58.905],[-138.34089,59.56211],[-139.039,60],[-140.013,60.27682],[-140.99778,60.30639],[-140.9925,66.00003],[-140.986,69.712],[-139.12052,69.47102],[-137.54636,68.99002],[-136.50358,68.89804],[-135.62576,69.31512],[-134.41464,69.62743],[-132.92925,69.50534],[-131.43136,69.94451],[-129.79471,70.19369],[-129.10773,69.77927],[-128.36156,70.01286],[-128.13817,70.48384],[-127.44712,70.37721],[-125.75632,69.48058],[-124.42483,70.1584],[-124.28968,69.39969],[-123.06108,69.56372],[-122.6835,69.85553],[-121.47226,69.79778],[-119.94288,69.37786],[-117.60268,69.01128],[-116.22643,68.84151],[-115.2469,68.90591],[-113.89794,68.3989],[-115.30489,67.90261],[-113.49727,67.68815],[-110.798,67.80612],[-109.94619,67.98104],[-108.8802,67.38144],[-107.79239,67.88736],[-108.81299,68.31164],[-108.16721,68.65392],[-106.95,68.7],[-106.15,68.8],[-105.34282,68.56122],[-104.33791,68.018],[-103.22115,68.09775],[-101.45433,67.64689],[-99.90195,67.80566],[-98.4432,67.78165],[-98.5586,68.40394],[-97.66948,68.57864],[-96.11991,68.23939],[-96.12588,67.29338],[-95.48943,68.0907],[-94.685,68.06383],[-94.23282,69.06903],[-95.30408,69.68571],[-96.47131,70.08976],[-96.39115,71.19482],[-95.2088,71.92053],[-93.88997,71.76015],[-92.87818,71.31869],[-91.51964,70.19129],[-92.40692,69.69997],[-90.5471,69.49766]]],[[[-114.16717,73.12145],[-114.66634,72.65277],[-112.44102,72.9554],[-111.05039,72.4504],[-109.92035,72.96113],[-109.00654,72.63335],[-108.18835,71.65089],[-107.68599,72.06548],[-108.39639,73.08953],[-107.51645,73.23598],[-106.52259,73.07601],[-105.40246,72.67259],[-104.77484,71.6984],[-104.46476,70.99297],[-102.78537,70.49776],[-100.98078,70.02432],[-101.08929,69.58447],[-102.73116,69.50402],[-102.09329,69.11962],[-102.43024,68.75282],[-104.24,68.91],[-105.96,69.18],[-107.12254,69.11922],[-109,68.78],[-111.534149,68.630059],[-113.3132,68.53554],[-113.85496,69.00744],[-115.22,69.28],[-116.10794,69.16821],[-117.34,69.96],[-116.67473,70.06655],[-115.13112,70.2373],[-113.72141,70.19237],[-112.4161,70.36638],[-114.35,70.6],[-116.48684,70.52045],[-117.9048,70.54056],[-118.43238,70.9092],[-116.11311,71.30918],[-117.65568,71.2952],[-119.40199,71.55859],[-118.56267,72.30785],[-117.86642,72.70594],[-115.18909,73.31459],[-114.16717,73.12145]]],[[[-104.5,73.42],[-105.38,72.76],[-106.94,73.46],[-106.6,73.6],[-105.26,73.64],[-104.5,73.42]]],[[[-76.34,73.102685],[-76.251404,72.826385],[-77.314438,72.855545],[-78.39167,72.876656],[-79.486252,72.742203],[-79.775833,72.802902],[-80.876099,73.333183],[-80.833885,73.693184],[-80.353058,73.75972],[-78.064438,73.651932],[-76.34,73.102685]]],[[[-86.562179,73.157447],[-85.774371,72.534126],[-84.850112,73.340278],[-82.31559,73.750951],[-80.600088,72.716544],[-80.748942,72.061907],[-78.770639,72.352173],[-77.824624,72.749617],[-75.605845,72.243678],[-74.228616,71.767144],[-74.099141,71.33084],[-72.242226,71.556925],[-71.200015,70.920013],[-68.786054,70.525024],[-67.91497,70.121948],[-66.969033,69.186087],[-68.805123,68.720198],[-66.449866,68.067163],[-64.862314,67.847539],[-63.424934,66.928473],[-61.851981,66.862121],[-62.163177,66.160251],[-63.918444,64.998669],[-65.14886,65.426033],[-66.721219,66.388041],[-68.015016,66.262726],[-68.141287,65.689789],[-67.089646,65.108455],[-65.73208,64.648406],[-65.320168,64.382737],[-64.669406,63.392927],[-65.013804,62.674185],[-66.275045,62.945099],[-68.783186,63.74567],[-67.369681,62.883966],[-66.328297,62.280075],[-66.165568,61.930897],[-68.877367,62.330149],[-71.023437,62.910708],[-72.235379,63.397836],[-71.886278,63.679989],[-73.378306,64.193963],[-74.834419,64.679076],[-74.818503,64.389093],[-77.70998,64.229542],[-78.555949,64.572906],[-77.897281,65.309192],[-76.018274,65.326969],[-73.959795,65.454765],[-74.293883,65.811771],[-73.944912,66.310578],[-72.651167,67.284576],[-72.92606,67.726926],[-73.311618,68.069437],[-74.843307,68.554627],[-76.869101,68.894736],[-76.228649,69.147769],[-77.28737,69.76954],[-78.168634,69.826488],[-78.957242,70.16688],[-79.492455,69.871808],[-81.305471,69.743185],[-84.944706,69.966634],[-87.060003,70.260001],[-88.681713,70.410741],[-89.51342,70.762038],[-88.467721,71.218186],[-89.888151,71.222552],[-90.20516,72.235074],[-89.436577,73.129464],[-88.408242,73.537889],[-85.826151,73.803816],[-86.562179,73.157447]]],[[[-100.35642,73.84389],[-99.16387,73.63339],[-97.38,73.76],[-97.12,73.47],[-98.05359,72.99052],[-96.54,72.56],[-96.72,71.66],[-98.35966,71.27285],[-99.32286,71.35639],[-100.01482,71.73827],[-102.5,72.51],[-102.48,72.83],[-100.43836,72.70588],[-101.54,73.36],[-100.35642,73.84389]]],[[[-93.196296,72.771992],[-94.269047,72.024596],[-95.409856,72.061881],[-96.033745,72.940277],[-96.018268,73.43743],[-95.495793,73.862417],[-94.503658,74.134907],[-92.420012,74.100025],[-90.509793,73.856732],[-92.003965,72.966244],[-93.196296,72.771992]]],[[[-120.46,71.383602],[-123.09219,70.90164],[-123.62,71.34],[-125.928949,71.868688],[-125.5,72.292261],[-124.80729,73.02256],[-123.94,73.68],[-124.91775,74.29275],[-121.53788,74.44893],[-120.10978,74.24135],[-117.55564,74.18577],[-116.58442,73.89607],[-115.51081,73.47519],[-116.76794,73.22292],[-119.22,72.52],[-120.46,71.82],[-120.46,71.383602]]],[[[-93.612756,74.979997],[-94.156909,74.592347],[-95.608681,74.666864],[-96.820932,74.927623],[-96.288587,75.377828],[-94.85082,75.647218],[-93.977747,75.29649],[-93.612756,74.979997]]],[[[-98.5,76.72],[-97.735585,76.25656],[-97.704415,75.74344],[-98.16,75],[-99.80874,74.89744],[-100.88366,75.05736],[-100.86292,75.64075],[-102.50209,75.5638],[-102.56552,76.3366],[-101.48973,76.30537],[-99.98349,76.64634],[-98.57699,76.58859],[-98.5,76.72]]],[[[-108.21141,76.20168],[-107.81943,75.84552],[-106.92893,76.01282],[-105.881,75.9694],[-105.70498,75.47951],[-106.31347,75.00527],[-109.7,74.85],[-112.22307,74.41696],[-113.74381,74.39427],[-113.87135,74.72029],[-111.79421,75.1625],[-116.31221,75.04343],[-117.7104,75.2222],[-116.34602,76.19903],[-115.40487,76.47887],[-112.59056,76.14134],[-110.81422,75.54919],[-109.0671,75.47321],[-110.49726,76.42982],[-109.5811,76.79417],[-108.54859,76.67832],[-108.21141,76.20168]]],[[[-94.684086,77.097878],[-93.573921,76.776296],[-91.605023,76.778518],[-90.741846,76.449597],[-90.969661,76.074013],[-89.822238,75.847774],[-89.187083,75.610166],[-87.838276,75.566189],[-86.379192,75.482421],[-84.789625,75.699204],[-82.753445,75.784315],[-81.128531,75.713983],[-80.057511,75.336849],[-79.833933,74.923127],[-80.457771,74.657304],[-81.948843,74.442459],[-83.228894,74.564028],[-86.097452,74.410032],[-88.15035,74.392307],[-89.764722,74.515555],[-92.422441,74.837758],[-92.768285,75.38682],[-92.889906,75.882655],[-93.893824,76.319244],[-95.962457,76.441381],[-97.121379,76.751078],[-96.745123,77.161389],[-94.684086,77.097878]]],[[[-116.198587,77.645287],[-116.335813,76.876962],[-117.106051,76.530032],[-118.040412,76.481172],[-119.899318,76.053213],[-121.499995,75.900019],[-122.854924,76.116543],[-122.854925,76.116543],[-121.157535,76.864508],[-119.103939,77.51222],[-117.570131,77.498319],[-116.198587,77.645287]]],[[[-93.840003,77.519997],[-94.295608,77.491343],[-96.169654,77.555111],[-96.436304,77.834629],[-94.422577,77.820005],[-93.720656,77.634331],[-93.840003,77.519997]]],[[[-110.186938,77.697015],[-112.051191,77.409229],[-113.534279,77.732207],[-112.724587,78.05105],[-111.264443,78.152956],[-109.854452,77.996325],[-110.186938,77.697015]]],[[[-109.663146,78.601973],[-110.881314,78.40692],[-112.542091,78.407902],[-112.525891,78.550555],[-111.50001,78.849994],[-110.963661,78.804441],[-109.663146,78.601973]]],[[[-95.830295,78.056941],[-97.309843,77.850597],[-98.124289,78.082857],[-98.552868,78.458105],[-98.631984,78.87193],[-97.337231,78.831984],[-96.754399,78.765813],[-95.559278,78.418315],[-95.830295,78.056941]]],[[[-100.060192,78.324754],[-99.670939,77.907545],[-101.30394,78.018985],[-102.949809,78.343229],[-105.176133,78.380332],[-104.210429,78.67742],[-105.41958,78.918336],[-105.492289,79.301594],[-103.529282,79.165349],[-100.825158,78.800462],[-100.060192,78.324754]]],[[[-87.02,79.66],[-85.81435,79.3369],[-87.18756,79.0393],[-89.03535,78.28723],[-90.80436,78.21533],[-92.87669,78.34333],[-93.95116,78.75099],[-93.93574,79.11373],[-93.14524,79.3801],[-94.974,79.37248],[-96.07614,79.70502],[-96.70972,80.15777],[-96.01644,80.60233],[-95.32345,80.90729],[-94.29843,80.97727],[-94.73542,81.20646],[-92.40984,81.25739],[-91.13289,80.72345],[-89.45,80.509322],[-87.81,80.32],[-87.02,79.66]]],[[[-68.5,83.106322],[-65.82735,83.02801],[-63.68,82.9],[-61.85,82.6286],[-61.89388,82.36165],[-64.334,81.92775],[-66.75342,81.72527],[-67.65755,81.50141],[-65.48031,81.50657],[-67.84,80.9],[-69.4697,80.61683],[-71.18,79.8],[-73.2428,79.63415],[-73.88,79.430162],[-76.90773,79.32309],[-75.52924,79.19766],[-76.22046,79.01907],[-75.39345,78.52581],[-76.34354,78.18296],[-77.88851,77.89991],[-78.36269,77.50859],[-79.75951,77.20968],[-79.61965,76.98336],[-77.91089,77.022045],[-77.88911,76.777955],[-80.56125,76.17812],[-83.17439,76.45403],[-86.11184,76.29901],[-87.6,76.42],[-89.49068,76.47239],[-89.6161,76.95213],[-87.76739,77.17833],[-88.26,77.9],[-87.65,77.970222],[-84.97634,77.53873],[-86.34,78.18],[-87.96192,78.37181],[-87.15198,78.75867],[-85.37868,78.9969],[-85.09495,79.34543],[-86.50734,79.73624],[-86.93179,80.25145],[-84.19844,80.20836],[-83.408696,80.1],[-81.84823,80.46442],[-84.1,80.58],[-87.59895,80.51627],[-89.36663,80.85569],[-90.2,81.26],[-91.36786,81.5531],[-91.58702,81.89429],[-90.1,82.085],[-88.93227,82.11751],[-86.97024,82.27961],[-85.5,82.652273],[-84.260005,82.6],[-83.18,82.32],[-82.42,82.86],[-81.1,83.02],[-79.30664,83.13056],[-76.25,83.172059],[-75.71878,83.06404],[-72.83153,83.23324],[-70.665765,83.169781],[-68.5,83.106322]]]]}},
{"type":"Feature","id":"CHE","properties":{"name":"Switzerland"},"geometry":{"type":"Polygon","coordinates":[[[9.594226,47.525058],[9.632932,47.347601],[9.47997,47.10281],[9.932448,46.920728],[10.442701,46.893546],[10.363378,46.483571],[9.922837,46.314899],[9.182882,46.440215],[8.966306,46.036932],[8.489952,46.005151],[8.31663,46.163642],[7.755992,45.82449],[7.273851,45.776948],[6.843593,45.991147],[6.5001,46.429673],[6.022609,46.27299],[6.037389,46.725779],[6.768714,47.287708],[6.736571,47.541801],[7.192202,47.449766],[7.466759,47.620582],[8.317301,47.61358],[8.522612,47.830828],[9.594226,47.525058]]]}},
{"type":"Feature","id":"CHL","properties":{"name":"Chile"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-68.63401,-52.63637],[-68.63335,-54.8695],[-67.56244,-54.87001],[-66.95992,-54.89681],[-67.29103,-55.30124],[-68.14863,-55.61183],[-68.639991,-55.580018],[-69.2321,-55.49906],[-69.95809,-55.19843],[-71.00568,-55.05383],[-72.2639,-54.49514],[-73.2852,-53.95752],[-74.66253,-52.83749],[-73.8381,-53.04743],[-72.43418,-53.7154],[-71.10773,-54.07433],[-70.59178,-53.61583],[-70.26748,-52.93123],[-69.34565,-52.5183],[-68.63401,-52.63637]]],[[[-68.219913,-21.494347],[-67.82818,-22.872919],[-67.106674,-22.735925],[-66.985234,-22.986349],[-67.328443,-24.025303],[-68.417653,-24.518555],[-68.386001,-26.185016],[-68.5948,-26.506909],[-68.295542,-26.89934],[-69.001235,-27.521214],[-69.65613,-28.459141],[-70.01355,-29.367923],[-69.919008,-30.336339],[-70.535069,-31.36501],[-70.074399,-33.09121],[-69.814777,-33.273886],[-69.817309,-34.193571],[-70.388049,-35.169688],[-70.364769,-36.005089],[-71.121881,-36.658124],[-71.118625,-37.576827],[-70.814664,-38.552995],[-71.413517,-38.916022],[-71.680761,-39.808164],[-71.915734,-40.832339],[-71.746804,-42.051386],[-72.148898,-42.254888],[-71.915424,-43.408565],[-71.464056,-43.787611],[-71.793623,-44.207172],[-71.329801,-44.407522],[-71.222779,-44.784243],[-71.659316,-44.973689],[-71.552009,-45.560733],[-71.917258,-46.884838],[-72.447355,-47.738533],[-72.331161,-48.244238],[-72.648247,-48.878618],[-73.415436,-49.318436],[-73.328051,-50.378785],[-72.975747,-50.74145],[-72.309974,-50.67701],[-72.329404,-51.425956],[-71.914804,-52.009022],[-69.498362,-52.142761],[-68.571545,-52.299444],[-69.461284,-52.291951],[-69.94278,-52.537931],[-70.845102,-52.899201],[-71.006332,-53.833252],[-71.429795,-53.856455],[-72.557943,-53.53141],[-73.702757,-52.835069],[-73.702757,-52.83507],[-74.946763,-52.262754],[-75.260026,-51.629355],[-74.976632,-51.043396],[-75.479754,-50.378372],[-75.608015,-48.673773],[-75.18277,-47.711919],[-74.126581,-46.939253],[-75.644395,-46.647643],[-74.692154,-45.763976],[-74.351709,-44.103044],[-73.240356,-44.454961],[-72.717804,-42.383356],[-73.3889,-42.117532],[-73.701336,-43.365776],[-74.331943,-43.224958],[-74.017957,-41.794813],[-73.677099,-39.942213],[-73.217593,-39.258689],[-73.505559,-38.282883],[-73.588061,-37.156285],[-73.166717,-37.12378],[-72.553137,-35.50884],[-71.861732,-33.909093],[-71.43845,-32.418899],[-71.668721,-30.920645],[-71.370083,-30.095682],[-71.489894,-28.861442],[-70.905124,-27.64038],[-70.724954,-25.705924],[-70.403966,-23.628997],[-70.091246,-21.393319],[-70.16442,-19.756468],[-70.372572,-18.347975],[-69.858444,-18.092694],[-69.590424,-17.580012],[-69.100247,-18.260125],[-68.966818,-18.981683],[-68.442225,-19.405068],[-68.757167,-20.372658],[-68.219913,-21.494347]]]]}},
]}

3. 导入依赖

GeoJSON作为一种矢量数据格式,可以使用矢量库OGR进行处理,以实现GeoJSON数据从文本格式转换为Shp格式。其中还涉及坐标定义,所以还需要引入osr模块。

from osgeo import ogr,osr
import os

4. 数据读取与转换

定义一个方法GeoJSON2Shp(geoPath,shpPath)用于将GeoJSON数据转换为Shp数据。

"""
说明:将 GeoJSON 文件转换为 Shapfile 文件
参数:
    -geoPath:GeoJSON 文件路径
    -shpPath:Shp 文件路径
"""
def GeoJSON2Shp(geoPath,shpPath):

在进行GeoJSON数据格式转换之前,需要检查数据路径是否存在。

# 检查文件是否存在
if os.path.exists(geoPath):
    print("GeoJSON 文件存在。")
else:
    print("GeoJSON 文件不存在,请重新选择文件!")
    return

打开GeoJSON数据。

# 打开JSON文件
jsonDataSource = ogr.Open(geoPath)
jsonLayer = jsonDataSource.GetLayer()

使用os.path.exists方法检查Shp文件是否已经创建,如果存在则将其删除。

if os.path.exists(shpPath):
    try:
        shpDriver.DeleteDataSource(shpPath)
        print("文件已删除!")
    except Exception as e:
        print(f"文件删除出错:{e}")
        return False

通过GetDriverByName获取Shp数据驱动,并创建Shp数据源。

# 创建Shp数据源
shpDriver = ogr.GetDriverByName("ESRI Shapefile")
shpDataSource = shpDriver.CreateDataSource(shpPath)

添加Shp图层属性。获取源数据坐标系、属性字段以及几何对象,并将其复制到目标图层中。属性结构显示如下:

# 获取坐标系
srs = jsonLayer.GetSpatialRef()

# 创建Shp数据图层
shpLayer = shpDataSource.CreateLayer(
    "layer",
    srs=srs,
    geom_type=jsonLayer.GetGeomType()
)

# 获取字段属性
layerDefn = jsonLayer.GetLayerDefn()
for i in range(layerDefn.GetFieldCount()):
    fieldDefn = layerDefn.GetFieldDefn(i)
    shpLayer.CreateField(fieldDefn)

# 获取几何属性
for feature in jsonLayer:
    shpLayer.CreateFeature(feature)

在数据读取完成之后关闭数据源。

# 关闭数据源
jsonDataSource = shpDataSource = None

数据转换完成之后在ArcMap中打开的显示效果:数据属性:

鸿蒙主题切换:一个开关搞定白天/黑夜模式

作者 90后晨仔
2025年12月29日 21:05

我强烈推荐使用鸿蒙原生资源限定词方案,配合一个简单的开关控制。这是最优雅、最高效的实现方式。让我用一个完整可运行的示例告诉你为什么。

一、完整实现:一个开关控制白天/黑夜模式

1.1 项目结构

project/
├── AppScope/resources/
│   ├── base/element/color.json      # 白天模式颜色
│   └── dark/element/color.json      # 夜间模式颜色
├── entry/src/main/ets/
│   ├── pages/
│   │   └── Index.ets               # 主页面
│   └── utils/
│       └── ThemeUtils.ets          # 主题管理工具
└── entry/src/main/resources/
    ├── base/media/                 # 白天模式图标
    └── dark/media/                 # 夜间模式图标

1.2 颜色资源定义

白天主题颜色 (base/element/color.json):

{
  "color": [
    {
      "name": "app_bg_primary",
      "value": "#F8F9FA"
    },
    {
      "name": "app_text_primary",
      "value": "#212529"
    },
    {
      "name": "app_card_bg",
      "value": "#FFFFFF"
    },
    {
      "name": "app_accent_primary",
      "value": "#0D6EFD"
    },
    {
      "name": "app_switch_track_on",
      "value": "#34C759"
    },
    {
      "name": "app_switch_track_off",
      "value": "#E9ECEF"
    }
  ]
}

夜间主题颜色 (dark/element/color.json):

{
  "color": [
    {
      "name": "app_bg_primary",
      "value": "#121212"
    },
    {
      "name": "app_text_primary",
      "value": "#E9ECEF"
    },
    {
      "name": "app_card_bg",
      "value": "#1E1E1E"
    },
    {
      "name": "app_accent_primary",
      "value": "#6EA8FE"
    },
    {
      "name": "app_switch_track_on",
      "value": "#30D158"
    },
    {
      "name": "app_switch_track_off",
      "value": "#3A3A3C"
    }
  ]
}

1.3 主题管理工具

// utils/ThemeUtils.ets
import common from '@ohos.app.ability.common';
import Configuration from '@ohos.app.ability.Configuration';
import Preferences from '@ohos.data.preferences';

export enum ThemeMode {
  LIGHT = 'light',
  DARK = 'dark',
  SYSTEM = 'system'
}

export class ThemeUtils {
  private static instance: ThemeUtils;
  private appContext: common.ApplicationContext | null = null;
  private preferences: Preferences | null = null;
  private readonly PREF_NAME = 'app_theme_prefs';
  private readonly THEME_KEY = 'current_theme';
  
  // 主题变化监听器
  private listeners: Array<(mode: ThemeMode) => void> = [];
  
  // 单例模式
  static getInstance(): ThemeUtils {
    if (!ThemeUtils.instance) {
      ThemeUtils.instance = new ThemeUtils();
    }
    return ThemeUtils.instance;
  }
  
  // 初始化
  async initialize(context: common.Context): Promise<void> {
    try {
      // 获取应用上下文
      const abilityContext = context as common.UIAbilityContext;
      this.appContext = abilityContext.configuration.appContext;
      
      // 初始化Preferences存储
      this.preferences = await Preferences.getPreferences(context, this.PREF_NAME);
      
      console.log('ThemeUtils 初始化完成');
    } catch (error) {
      console.error('ThemeUtils 初始化失败:', error);
    }
  }
  
  // 获取当前主题
  async getCurrentTheme(): Promise<ThemeMode> {
    if (!this.preferences) {
      return ThemeMode.SYSTEM;
    }
    
    try {
      const theme = await this.preferences.get(this.THEME_KEY, ThemeMode.SYSTEM) as string;
      return theme as ThemeMode;
    } catch (error) {
      console.error('获取主题失败:', error);
      return ThemeMode.SYSTEM;
    }
  }
  
  // 切换主题
  async toggleTheme(): Promise<void> {
    const current = await this.getCurrentTheme();
    let newTheme: ThemeMode;
    
    // 简单切换逻辑:白天 ↔ 夜间
    if (current === ThemeMode.LIGHT) {
      newTheme = ThemeMode.DARK;
    } else if (current === ThemeMode.DARK) {
      newTheme = ThemeMode.LIGHT;
    } else {
      // 如果是跟随系统,默认切换到夜间
      newTheme = ThemeMode.DARK;
    }
    
    await this.setTheme(newTheme);
  }
  
  // 设置主题
  async setTheme(mode: ThemeMode): Promise<void> {
    if (!this.appContext || !this.preferences) {
      console.error('ThemeUtils 未初始化');
      return;
    }
    
    try {
      // 保存到Preferences
      await this.preferences.put(this.THEME_KEY, mode);
      await this.preferences.flush();
      
      // 应用到系统
      let colorMode: Configuration.ColorMode;
      
      switch (mode) {
        case ThemeMode.LIGHT:
          colorMode = Configuration.ColorMode.COLOR_MODE_LIGHT;
          break;
        case ThemeMode.DARK:
          colorMode = Configuration.ColorMode.COLOR_MODE_DARK;
          break;
        case ThemeMode.SYSTEM:
          colorMode = Configuration.ColorMode.COLOR_MODE_SYSTEM;
          break;
        default:
          colorMode = Configuration.ColorMode.COLOR_MODE_LIGHT;
      }
      
      this.appContext.setColorMode(colorMode);
      
      console.log(`主题已切换为: ${mode}`);
      
      // 通知所有监听器
      this.notifyListeners(mode);
      
    } catch (error) {
      console.error('设置主题失败:', error);
    }
  }
  
  // 监听主题变化
  addListener(callback: (mode: ThemeMode) => void): void {
    this.listeners.push(callback);
  }
  
  // 移除监听器
  removeListener(callback: (mode: ThemeMode) => void): void {
    const index = this.listeners.indexOf(callback);
    if (index > -1) {
      this.listeners.splice(index, 1);
    }
  }
  
  // 通知所有监听器
  private notifyListeners(mode: ThemeMode): void {
    for (const listener of this.listeners) {
      try {
        listener(mode);
      } catch (error) {
        console.error('监听器执行失败:', error);
      }
    }
  }
}

1.4 主页面实现(包含切换开关)

// pages/Index.ets
import { ThemeUtils, ThemeMode } from '../utils/ThemeUtils';

@Entry
@Component
struct Index {
  @State currentTheme: ThemeMode = ThemeMode.LIGHT;
  @State isDarkMode: boolean = false;
  
  // 在页面显示时初始化主题
  aboutToAppear(): void {
    this.initTheme();
  }
  
  // 初始化主题
  async initTheme(): Promise<void> {
    const themeUtils = ThemeUtils.getInstance();
    this.currentTheme = await themeUtils.getCurrentTheme();
    this.isDarkMode = this.currentTheme === ThemeMode.DARK;
    
    // 监听主题变化
    themeUtils.addListener((mode: ThemeMode) => {
      this.currentTheme = mode;
      this.isDarkMode = mode === ThemeMode.DARK;
    });
  }
  
  // 切换主题
  async toggleTheme(): Promise<void> {
    const themeUtils = ThemeUtils.getInstance();
    await themeUtils.toggleTheme();
  }
  
  // 获取开关状态文字
  getSwitchText(): string {
    return this.isDarkMode ? '夜间模式' : '白天模式';
  }
  
  // 获取模式图标
  getModeIcon(): Resource {
    return this.isDarkMode 
      ? $r('app.media.icon_moon')   // 月亮图标(夜间)
      : $r('app.media.icon_sun');   // 太阳图标(白天)
  }
  
  // 构建界面
  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('主题设置')
          .fontSize(24)
          .fontColor($r('app.color.app_text_primary'))
          .fontWeight(FontWeight.Medium)
        
        Blank()
        
        Image(this.getModeIcon())
          .width(24)
          .height(24)
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 12, bottom: 12 })
      .backgroundColor($r('app.color.app_card_bg'))
      
      // 主要内容区域
      Scroll() {
        Column() {
          // 主题切换卡片
          Column() {
            Row() {
              Column() {
                Text('外观模式')
                  .fontSize(18)
                  .fontColor($r('app.color.app_text_primary'))
                  .fontWeight(FontWeight.Medium)
                
                Text(this.isDarkMode ? '深色主题,保护眼睛' : '浅色主题,清晰明亮')
                  .fontSize(14)
                  .fontColor($r('app.color.app_text_primary'))
                  .opacity(0.6)
                  .margin({ top: 4 })
              }
              .flexGrow(1)
              
              // 主题切换开关
              Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
                .selectedColor($r('app.color.app_switch_track_on'))
                .switchPointColor($r('app.color.app_card_bg'))
                .onChange((isOn: boolean) => {
                  this.toggleTheme();
                })
            }
            .padding(20)
          }
          .backgroundColor($r('app.color.app_card_bg'))
          .borderRadius(12)
          .margin({ top: 20, left: 20, right: 20 })
          
          // 示例内容区域
          Column() {
            Text('示例内容')
              .fontSize(20)
              .fontColor($r('app.color.app_text_primary'))
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 16 })
            
            Text('这是一个文本示例,用于展示不同主题下的显示效果。在白天模式下,文字为深色;在夜间模式下,文字为浅色。')
              .fontSize(16)
              .fontColor($r('app.color.app_text_primary'))
              .opacity(0.8)
              .lineHeight(24)
            
            Divider()
              .strokeWidth(1)
              .color($r('app.color.app_text_primary'))
              .opacity(0.1)
              .margin({ top: 20, bottom: 20 })
            
            Row() {
              Button('主要按钮')
                .backgroundColor($r('app.color.app_accent_primary'))
                .fontColor('#FFFFFF')
                .borderRadius(8)
                .padding({ left: 20, right: 20 })
              
              Button('次要按钮')
                .backgroundColor($r('app.color.app_card_bg'))
                .fontColor($r('app.color.app_text_primary'))
                .border({
                  color: $r('app.color.app_text_primary'),
                  width: 1,
                  style: BorderStyle.Solid
                })
                .borderRadius(8)
                .margin({ left: 12 })
                .padding({ left: 20, right: 20 })
            }
            .margin({ top: 16 })
          }
          .padding(24)
          .backgroundColor($r('app.color.app_card_bg'))
          .borderRadius(12)
          .margin({ top: 20, left: 20, right: 20, bottom: 40 })
        }
        .width('100%')
      }
      .flexGrow(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.app_bg_primary'))
  }
}

1.5 在EntryAbility中初始化

// entry/src/main/ets/entryability/EntryAbility.ets
import { ThemeUtils } from '../utils/ThemeUtils';
import UIAbility from '@ohos.app.ability.UIAbility';

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    console.log('EntryAbility onCreate');
    
    // 初始化主题工具
    ThemeUtils.getInstance().initialize(this.context);
  }
}

二、为什么这是最佳方案?

2.1 简单直观,一行代码切换

// 核心切换逻辑,只需一行代码!
this.appContext.setColorMode(colorMode);

2.2 自动适配所有组件

一旦切换系统颜色模式,所有使用资源引用的组件都会自动更新:

  • 文本颜色
  • 背景颜色
  • 边框颜色
  • 图标资源
  • 甚至图片资源(如果有dark目录版本)

2.3 零耦合,高内聚

  • 业务组件:只关心$r('app.color.xxx'),不关心当前主题
  • 主题管理:集中在ThemeUtils,统一管理状态
  • 资源定义:设计师可以直接修改JSON文件

2.4 持久化存储

使用Preferences自动保存用户选择,下次启动自动恢复:

// 保存主题选择
await preferences.put('current_theme', mode);
await preferences.flush();

// 读取主题选择
const theme = await preferences.get('current_theme', 'light');

三、对比工具类方案的劣势

如果用工具类方案,你需要:

3.1 繁琐的状态管理

// 每个组件都需要监听主题变化
@Component
struct MyComponent {
  @State textColor: string = '#000000';
  
  aboutToAppear() {
    // 监听主题变化
    ThemeManager.addListener(() => {
      this.textColor = ThemeManager.getTextColor();
    });
  }
  
  build() {
    Text('示例')
      .fontColor(this.textColor)  // 需要状态变量
  }
}

3.2 手动更新所有组件

// 切换主题时需要手动更新所有组件
class ThemeManager {
  static toggleTheme() {
    this.isDark = !this.isDark;
    
    // 需要手动触发所有组件更新
    for (const component of this.registeredComponents) {
      component.updateTheme();
    }
  }
}

3.3 性能问题

  • 每次切换都要重新计算所有颜色
  • 组件需要频繁重绘
  • 无法利用系统的优化机制

四、高级功能扩展

4.1 添加跟随系统选项

// 在ThemeUtils中添加
async setFollowSystem(enable: boolean): Promise<void> {
  if (enable) {
    await this.setTheme(ThemeMode.SYSTEM);
    
    // 监听系统主题变化
    this.appContext.on('colorModeChange', (newMode: Configuration.ColorMode) => {
      console.log('系统主题已改变:', newMode);
      this.notifyListeners(this.mapSystemMode(newMode));
    });
  }
}

private mapSystemMode(mode: Configuration.ColorMode): ThemeMode {
  switch (mode) {
    case Configuration.ColorMode.COLOR_MODE_DARK:
      return ThemeMode.DARK;
    case Configuration.ColorMode.COLOR_MODE_LIGHT:
      return ThemeMode.LIGHT;
    default:
      return ThemeMode.LIGHT;
  }
}

4.2 添加动画效果

// 在切换主题时添加过渡动画
async toggleThemeWithAnimation(): Promise<void> {
  // 先设置半透明
  this.applyOpacityAnimation();
  
  // 延迟切换主题
  setTimeout(async () => {
    await this.toggleTheme();
    
    // 恢复不透明
    this.removeOpacityAnimation();
  }, 300);
}

4.3 多主题支持(节日主题等)

// 扩展支持更多主题
enum ExtendedThemeMode {
  LIGHT = 'light',
  DARK = 'dark',
  SYSTEM = 'system',
  FESTIVAL = 'festival',  // 节日主题
  HIGH_CONTRAST = 'high_contrast'  // 高对比度
}

// 创建对应的资源目录
// resources/festival/element/color.json
// resources/high_contrast/element/color.json

五、实践建议

5.1 新项目:直接使用资源限定词方案

  • 从第一天就建立base/dark/目录
  • 所有颜色使用$r('app.color.xxx')
  • 一个开关控制全部

5.2 老项目迁移:三步走

  1. 第一步:创建dark/color.json,复制所有颜色
  2. 第二步:逐步替换硬编码为资源引用
  3. 第三步:添加主题切换开关

5.3 设计规范

  1. 使用语义化颜色名称:primary_textsecondary_bg
  2. 建立颜色设计系统文档
  3. 定期同步设计和开发的颜色值

总结

对于"一个开关控制白天/黑夜模式"的需求,鸿蒙的资源限定词方案是最佳选择。

它提供了:

  • 一键切换:一个开关控制全局
  • 自动适配:所有组件自动更新
  • 零代码侵入:业务组件无需修改
  • 高性能:系统级优化
  • 易维护:颜色集中管理
  • 可扩展:支持多主题

而工具类方案需要:

  • ❌ 每个组件监听主题变化
  • ❌ 手动更新所有颜色
  • ❌ 性能损耗
  • ❌ 维护困难

选择资源限定词方案,让你的主题切换像呼吸一样自然!

胜通能源:股票短期内价格涨幅较大,明起停牌核查

2025年12月29日 20:57
36氪获悉,胜通能源公告,公司股票自2025年12月12日至29日期间价格涨幅为213.97%,已严重背离公司基本面。为维护投资者利益,公司将就股票交易波动情况进行停牌核查。公司提醒广大投资者注意二级市场交易风险,近期公司股票价格显著偏离大盘指数和行业指数,短期波动幅度较大,已明显偏离市场走势,存在较高的炒作风险。公司主营业务未发生重大变化,股价短期内连续上涨,可能存在市场情绪过热、非理性炒作风险。公司不存在应披露而未披露的重大信息。公司股票自2025年12月30日起停牌,预计停牌时间不超过3个交易日。

Luckysheet 远程搜索下拉 控件开发 : 揭秘二开全流程

作者 朴shu
2025年12月29日 20:47

前言

远程搜索下拉控件是非常常见的控件,应用在表单填写场景下,也是非常合适的,本例将带领大家实现以下效果,并真实了解并熟悉 luckysheet 的二开流程。

在这里插入图片描述

拓展单元格属性

在官网上,有一个常见问题,既然已知批注是直接加到单元格属性上,那么,我们拓展属性,是不是可以参考批注的实现方案?

在这里插入图片描述 我们设定以下是远程搜索下拉的单元格拓展属性:

// 扩展后的单元格对象
const cell = {
    v: null, // 原始值
    m: null, // 显示值
    ct: { fa: "General", t: "s" },
    // ... other properties

    // 新增远程搜索下拉配置
    remoteSelect: {
        // 是否启用远程搜索,[必传]
        enable: boolean,

        // 用户输入时的回调函数,用于获取远程数据
        onInput(value): Promise<Array<string>> {
            // 这个函数需要返回一个Promise对象,resolve一个数组,数组的元素为下拉选项对象
        }

        // 用户选定某个选项后的回调函数
        onSelect(item): void {
            // item 为用户选定的选项对象,值是onInput函数返回的数组中的某个元素
        }

        // 是否需要设定当前输入单元格的值 [可选]
        setValue(item): string{
           // item 为用户选定的选项对象,需要返回一个字符串,用于设定单元格的值
        },

        // 自定义类名  [可选]
        popperClass: string,
    },
};

何时触发?

在这个场景中,我们需要在用户输入值时,进行远程数据获取,渲染列表,因此,需要在用户进行编辑时,进行处理:

  1. 一个方案,是在初始化 inputHTML 时,将相关事件处理: 在这里插入图片描述
 $("body").append(inputHTML)
 // 进行相关事件绑定: [伪代码]
 $input.on('input',function(){
 // 这里是未知具体单元格信息的,需要通过外部传递
 const remoteSelectOptions = Store.flowdata[r][c]
 // 执行后续操作
 })
  1. 另一个方案,是用户双击时,唤起输入框后,进行判断:
// handler.js
$("#luckysheet-cell-main, #luckysheetTableContent").dblclick(function(){
// 这个函数是内置的哈,因此,可以直接获取到 r c
// 判断当前单元格是否需要初始化远程搜索控件
RemoteSelect.checkCellNeedRemoteSelect(row_index, col_index)
})

两种方案我都试过了,实现略有不同,但都可以实现,还是借助原生的 dblclick 好处理些。

校验远程搜索下拉

既然每一个单元格双击,都会触发这个校验,那么何时才需要初始化这个下拉框呢?

// 判断当前单元格是否需要显示下拉框
checkCellNeedRemoteSelect: function(r, c) {
    // 通过 r c 获取单元格信息
    let cell = Store.flowdata[r][c];

    // 如果 cell 没有配置 remoteSelect 或者 remoteSelect.enable 为 false 则返回
    if (!cell || !cell.remoteSelect || !cell.remoteSelect.enable) {
        return;
    }
}

检验完成后,如果需要初始化校验,别忘了, 我们底层还要监听输入框事件哈:

// 不然,开始监听 input 输入事件
$("#luckysheet-input-box .luckysheet-cell-input").off("input").on("input", function() {
        let value = $(this).text().trim(); // 获取用户输入的值
      
        if (cell.remoteSelect && cell.remoteSelect.onInput && typeof cell.remoteSelect.onInput == "function") {
            // 调用外部接口
            cell.remoteSelect
                .onInput(value)
                .then(function(dataList) {
                    $this.loading = false;
                    $this.showRemoteSelect(dataList);
                })
                .catch((error) => {
                    $this.loading = false;
                    $this.hideRemoteSelect();
                    console.log("请求接口错误", error);
                    tooltip.info('<i class="fa fa-exclamation-triangle"></i>', "接口请求失败");
                });
        }
    });

初始化下拉框

远程搜索的核心,就是下拉选项,当远程接口初始化完成后,执行如下:

dataList.forEach((item) => {
const $item = $(`<div class="${this.selectItemClass}">`)
.text(item)
.click(() => {
// 触发选择回调
if (cell.remoteSelect && cell.remoteSelect.onSelect && typeof cell.remoteSelect.onSelect === "function") {
    cell.remoteSelect.onSelect(item);
}
})

这里就一个难点要处理,如何将下拉框定位到输入框的位置:

// 这里采用巧方案,直接取输入框的位置
const $input = $("#luckysheet-input-box");
const left = parseInt($input.css("left")) || 0;
const top = parseInt($input.css("top")) || 0;

// 获取当前单元格的高度 行高
const [_row_pre, rowHeight] = rowLocation(this.r);

// 设置下拉框位置和宽度
$selectBox.css({
    top: `${top + rowHeight + 4}px`, // 在单元格下方显示
    left: `${left}px`,
});

总结

虽然代码看起来不复杂,但是了解luckysheet 的源码、实现思路,以及单元格拓展实现方案是非常重要的。

通过本次实现,我们初步了解了luckysheet的二次开发流程,在不破坏原有功能的基础上添加新特性。远程搜索下拉控件的实现展示了如何在现有表格组件基础上,通过合理的架构设计和代码组织,添加复杂交互功能。

紫光国微:拟购买瑞能半导控股权或全部股权,股票停牌

2025年12月29日 20:46
36氪获悉,紫光国微公告,公司正在筹划以发行股份及支付现金的方式,购买南昌建恩半导体产业投资中心(有限合伙)、北京广盟半导体产业投资中心(有限合伙)、天津瑞芯半导体产业投资中心(有限合伙)等交易对方持有的瑞能半导体科技股份有限公司控股权或全部股权,并募集配套资金。本次交易预计不构成重大资产重组,构成关联交易。公司股票及可转债自2025年12月30日起开始停牌,预计在不超过10个交易日的时间内披露本次交易方案。

天普股份:实际控制人已变更为杨龚轶凡,提前开展董事会换届选举工作

2025年12月29日 20:38
36氪获悉,天普股份公告,公司于2025年12月22日收到控股股东浙江天普控股有限公司通知,中昊芯英(杭州)科技有限公司、海南芯繁企业管理合伙企业(有限合伙)和方东晖对天普控股的增资款已足额支付,并完成工商变更登记,公司实际控制人变更为杨龚轶凡。为保障公司治理结构稳定性,公司提前开展董事会换届选举工作,提名杨龚轶凡、李琛龄和康啸为第四届董事会非独立董事候选人,马莹、沈百鑫为独立董事候选人。

外汇局:要促进跨境贸易投融资便利化,防范跨境资金流动风险

2025年12月29日 20:34
36氪获悉,国家外汇管理局近日组织2025年新任职司处级领导干部召开集体谈话会议。外汇局局长朱鹤新强调,要履职尽责敢担当,围绕防风险、强监管、促高质量发展工作主线,强化责任意识,提升履职能力,真抓实干,持续深化外汇领域改革开放,促进跨境贸易投融资便利化,防范跨境资金流动风险,保障外汇储备资产安全、流动和保值增值,切实维护外汇市场稳定和国家经济金融安全。

Midscene v1.0 发布 - 视觉驱动,UI 自动化体验跃迁

2025年12月29日 19:15

文章来源|ByteDance Web Infra 团队

Midscene 自 2024 年开源发布以来,已经在 Github 斩获 11k star 、Trending 榜第二名等成绩,并在互联网、金融、政企、汽车等大量应用场景下完成落地。

本月,我们正式宣布 Midscene v1.0 发布!本文将为你介绍:

  • 案例回顾:Midscene 在 PC、Android、iOS 等场景的任务能力;
  • 社区案例:社区开发者基于 Midscene 与任意界面集成的特性,扩展了机械臂 + 视觉模型 + 语音模型等模块,完成车机测试;
  • 1.0 版本的模型路线:拥抱纯视觉;
  • 1.0 版本的特性优化:报告优化、MCP 架构、跨端增强、API 变更等。

案例回顾

社区案例:视觉模型 + 机械臂

演示视频请在公众号查看:mp.weixin.qq.com/s/24rFtAfih…

有社区开发者成功基于 Midscene 与任意界面集成的特性,扩展了机械臂 + 视觉模型 + 语音模型等模块,运用于车机大屏测试场景中。

移动端案例:外卖下单

打开美团,帮我下单一杯 manner 超大杯冰美式咖啡,要加浓少冰喔,到结算页面让我确认。

演示视频请在公众号查看:mp.weixin.qq.com/s/24rFtAfih…

在我们的 Midscene 官网上,还有更多实战案例:

  1. iOS 自动化 - Twitter 自动点赞 @midscene_ai 首条推文;
  2. Android 自动化 - 懂车帝查看小米 SU7 参数;
  3. Android 自动化 - Booking 预订圣诞酒店;
  4. MCP 集成 - Midscene MCP 操作界面发布 prepatch 版本。

1.0 版本的模型路线

从 V1.0 开始,Midscene 全面转向视觉理解方案,提供更稳定可靠的 UI 自动化能力。

视觉模型有以下特点:

  • 效果稳定 :业界领先的视觉模型(如 Doubao Seed 1.6、Qwen3-VL 等)表现足够稳定,已经可以满足大多数业务需求;

  • UI 操作规划 :视觉模型通常具备较强的 UI 操作规划能力,能够完成不少复杂的任务流程;

  • 适用于任意系统 :自动化框架不再依赖 UI 渲染的技术栈,无论是 Android、iOS、桌面应用,还是浏览器中的 <canvas>,只要能获取截图,Midscene 即可完成交互操作;

  • 易于编写 :抛弃各类 selector 和 DOM 之后,开发者与模型的“磨合”会变得更简单,不熟悉渲染技术的新人也能很快上手;

  • token 量显著下降 :在去除 DOM 提取之后,视觉方案的 token 使用量可以减少 80%,成本更低,且本地运行速度也变得更快

  • 有开源模型解决方案 :开源模型表现渐佳,开发者开始有机会进行私有化部署模型,如 Qwen3-VL 提供的 8B、30B 等版本在不少项目中都有着不错的效果。

详情请阅读我们更新版的模型策略[1]。

🚀 多模型组合,为复杂任务带来更好效果

除了默认的交互场景,Midscene 还定义了 Planning(规划)和 Insight(洞察)两种意图,开发者可以按需为它们启用独立的模型。例如,用 GPT 模型做规划,同时使用默认的 Doubao 模型做元素定位。

多模型组合让开发者可以按需提升复杂需求的处理能力。

🚀 运行时架构优化

针对 Midscene 的运行时表现,我们进行了以下优化:

  • 减少对设备信息接口的调用,在确保安全的情况下复用部分上下文信息,提升运行时性能,让大多数的时间消耗集中在模型端;
  • 优化 Web 及移动端环境下的 Action Space 组合,向模型开放更合理、更清晰的工具集。

🚀 回放报告优化

回放报告是 Midscene 开发者非常依赖的一个特性,它能有效提升脚本的调试效率。

在 v1.0 中,我们更新了回放报告:

  • 参数视图:标记出交互参数的位置信息,合并截图信息,快速识别模型的规划结果;
  • 样式调整:支持以深色模式展示报告,更美观;
  • Token 消耗的展示:支持按模型汇总 Token 消耗量,分析不同场景的成本情况。

🚀 MCP 架构重构

我们重新定义了 Midscene MCP 服务的定位。Midscene MCP 的职责是围绕着视觉驱动的 UI 操作展开,将 iOS / Android / Web 设备 Action Space 中的每个 Action 操作暴露为 MCP 工具,也就是提供各类“原子操作”。

通过这种形式,开发者可以更专注于构建自己的高阶 Agent,而无需关心底层 UI 操作的实现细节,并且时刻获得满意的成功率。

详情请阅读 MCP 文档[2]。

🚀 移动端能力增强

iOS 改进

  • 新增 WebDriverAgent 5.x-7.x 全版本兼容;

  • 新增 WebDriver Clear API 支持,解决动态输入框问题;

  • 提升设备兼容性。

Android 改进

  • 新增截图轮询回退机制,提升远程设备稳定性;

  • 新增屏幕方向自动适配(displayId 截图);

  • 新增 YAML 脚本 runAdbShell 支持。

跨平台

  • 在 Agent 实例上暴露系统操作接口,包括 Home、Back、RecentApp 等。

🚧 API 变更

方法重命名(向后兼容):

  • 改名 aiAction() → aiAct()(旧方法保留,有弃用警告);

  • 改名 logScreenshot() → recordToReport()(旧方法保留,有弃用警告)。

环境变量重命名(向后兼容):

  • 改名 OPENAI_API_KEY → MODEL_API_KEY(新变量优先,旧变量作为备选);
  • 改名 OPENAI_BASE_URL → MODEL_BASE_URL(新变量优先,旧变量作为备选)。

⬆️ 升级到最新版

升级项目中的依赖,例如:

  • npm install @midscene/web@latest --save-dev

  • npm install @midscene/android@latest --save-dev

  • npm install @midscene/ios@latest --save-dev

如果使用全局安装的命令行版本:npm i -g @midscene/cli

了解更多

参考资料

[1] 模型策略: https://midscenejs.com/zh/model-strategy

[2] MCP 文档: https://midscenejs.com/zh/mcp

React 跨层级组件通信:从 Props Drilling 到 useContext 的实战剖析

2025年12月29日 18:28

在 React 开发中,组件通信是日常中最常见的任务之一。父子组件间通过 props 传递数据简单高效,但当数据需要传递到多层嵌套的子组件时,“Props Drilling”(属性穿透)问题就会显现:中间层组件明明不需要这些数据,却不得不被动接收并向下传递。这不仅让代码冗余,还降低了可维护性。

React 官方提供的 Context API 正是为此而生。它允许我们在组件树的最外层“提供”数据,任何深层组件都可以直接“消费”它,而无需层层传递。本文将通过一个用户信息的实际例子,对比两种方式,帮助你理解何时、何地该使用 useContext 解决跨层级通信问题。

Props Drilling:传统方式的痛点

假设我们有一个应用,需要在最顶层 App 组件持有用户信息(如登录后的用户数据),然后在深层嵌套的 UserInfo 组件中显示用户名。

传统方式是层层通过 props 传递:

jsx

// App.jsx
export default function App() {
  const user = { name: "Andrew" };

  return (
    <Page user={user} />
  );
}

// views/Page.jsx
function Page({ user }) {
  return <Header user={user} />;
}

// components/Header.jsx
function Header({ user }) {
  return <UserInfo user={user} />;
}

// components/UserInfo.jsx
function UserInfo({ user }) {
  return <div>{user.name}</div>;
}

这种方式在层级较浅时没问题,但想象一下如果组件树更深(比如 Page → Layout → Sidebar → Header → UserInfo),就需要在每一层都添加 user prop:

jsx

<Page user={user} />
<Layout user={user} />
<Sidebar user={user} />
<Header user={user} />
<UserInfo user={user} />

中间的 Layout、Sidebar 等组件根本不需要 user 数据,却被迫成为“快递员”。这就是典型的 Props Drilling:

  • 代码冗余,维护成本高(修改一次要改多处)。
  • 中间组件耦合度增加,重构困难。
  • 阅读性差,难以快速定位数据来源。

Context API:优雅解决跨层级通信

Context API 的核心思想是:数据在最外层提供,任何子组件主动消费。这样,数据持有和改变的逻辑依然在外层组件,但消费方可以直接获取,无需中间传递。

步骤一:创建 Context

通常在独立文件中创建(推荐实践,便于复用和维护),但简单示例可放在 App 中。

jsx

// App.jsx
import { createContext } from 'react';
import Page from './views/Page';

// 创建 Context,defaultValue 为 null(生产中可设为默认值)
export const UserContext = createContext(null);

export default function App() {
  const user = { name: "Andrew" };

  return (
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
}
  • createContext 创建一个上下文对象。
  • Provider 组件包裹需要共享数据的组件树。
  • value 属性就是共享的数据(可以是对象、函数、状态等)。

步骤二:消费 Context

在任何子组件中使用 useContext Hook 直接读取:

jsx

// components/UserInfo.jsx
import { useContext } from 'react';
import { UserContext } from '../App';  // 根据实际路径调整

export default function UserInfo() {
  const user = useContext(UserContext);

  return <div>{user.name}</div>;
}

中间组件无需任何修改:

jsx

// views/Page.jsx
import Header from '../components/Header';

export default function Page() {
  return <Header />;
}

// components/Header.jsx
import UserInfo from './UserInfo';

export default function Header() {
  return <UserInfo />;
}

效果完全相同,但代码干净多了!UserInfo 组件主动“找”数据,而不是被动接收。

完整目录结构示例

text

src/
├── App.jsx
├── views/
│   └── Page.jsx
└── components/
    ├── Header.jsx
    └── UserInfo.jsx

为什么 useContext 更优?

  1. 避免 Props Drilling:中间层组件无需关心数据传递。
  2. 数据来源清晰:消费组件直接导入 Context,一目了然。
  3. 灵活性高:Provider 可以包裹任意子树,支持局部共享。
  4. 性能友好(注意事项见下文):React 会优化只重渲染实际消费的组件。

进阶:动态更新 Context 数据

单纯的对象共享已足够强大,但真实场景中用户数据往往需要更新(如登录/退出)。

推荐将状态和更新函数一起提供:

jsx

// App.jsx
import { useState, createContext } from 'react';

export const UserContext = createContext(null);

export default function App() {
  const [user, setUser] = useState({ name: "Andrew" });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Page />
    </UserContext.Provider>
  );
}

消费方:

jsx

// UserInfo.jsx
const { user, setUser } = useContext(UserContext);

// 示例:退出登录
<button onClick={() => setUser(null)}>退出</button>
{user ? <div>{user.name}</div> : <div>未登录</div>}

这样,任何组件都能读取并修改全局用户状态。

最佳实践与注意事项

  1. 单独文件管理 Context:大型项目中,将 createContext、Provider 封装成独立文件(如 UserContext.jsx),便于团队协作。

  2. 避免频繁更新大对象:Context 使用引用相等性判断重渲染。如果每次 Provider value 都是新对象(如 {...}),会导致所有消费者重渲染。解决办法:

    • 使用 useMemo 稳定 value:

      jsx

      const value = useMemo(() => ({ user, setUser }), [user]);
      <Provider value={value}>
      
    • 或拆分多个 Context(主题、用户、配置分开)。

  3. 不要滥用:Context 适合“全局性”低频变化数据(如用户、主题、语言)。高频变化或复杂状态推荐 Zustand、Jotai 或 Redux。

  4. 结合 React.memo 优化:如果消费者不依赖 Context,可用 React.memo 防止不必要重渲染。

  5. TypeScript 支持:createContext 时可指定类型,提升类型安全。

实际应用场景举例

  • 用户信息:登录状态、头像、权限。
  • 主题切换:dark/light mode。
  • 国际化:当前语言包。
  • 布局配置:侧边栏展开状态。

这些数据往往被多个深层组件使用,使用 Context 能极大简化代码。

结语

Props Drilling 是 React 新手最先接触的方式,但随着项目规模增长,它会成为维护的枷锁。Context API + useContext 提供了原生、轻量级的解决方案,让跨层级通信变得优雅而高效。

记住核心原则:数据在外层提供,消费方主动获取。这不仅解决了 Props Drilling,还为未来扩展(如结合 Reducer 实现小型状态管理)打下基础。

在 2025 年的 React 生态中,Context API 依然是中小型项目全局状态管理的首选。合理使用它,你的组件树将更清晰、可维护性更强。

赶紧在你的项目中试试吧——从一个简单的用户上下文开始,你会爱上这种“跳跃式”数据传递的自由!

滑动窗口详解:原理+分类+场景+模板+例题(视频贼清晰)

作者 颜酱
2025年12月29日 18:28

滑动窗口详解:原理+分类+场景+模板+例题

📺 推荐视频滑动窗口算法详解 - 视频解释非常清晰,建议先看视频再阅读本文!

在算法面试中,子串、子数组相关的问题频繁出现,暴力枚举往往因 O(n²) 时间复杂度超时。而滑动窗口算法,凭借其 O(n) 的高效性能,成为解决这类问题的"神兵利器"。本文将从原理本质出发,梳理滑动窗口的分类、适用场景,提炼通用模板,并结合经典例题实战拆解,帮你彻底掌握这一核心算法。

一、滑动窗口核心原理:用单调性压缩遍历维度

滑动窗口的本质,是利用区间的单调性,将原本需要嵌套遍历(O(n²))的连续区间问题,转化为单轮双指针遍历(O(n))。其核心逻辑基于对“窗口状态”的精准把控,通过两个指针(left 左边界、right 右边界)的协同移动,跳过无效区间(剪枝),实现高效枚举。

1.1 先搞懂:暴力枚举的痛点

以“无重复字符的最长子串”为例,暴力思路是枚举所有子串的起点 i 和终点 j(i≤j),检查子串 s[i..j] 是否无重复,最终记录最长长度。这种方式需要遍历所有 i、j 组合,时间复杂度 O(n²),且存在大量无效计算:比如当 s[0..3] 存在重复时,s[0..4]、s[0..5] 等包含该区间的子串必然也重复,无需再检查。

1.2 滑动窗口的核心洞察:区间单调性

滑动窗口能优化的关键,是抓住了「窗口状态的单调性」—— 窗口的状态(如是否含重复、和/积是否满足条件)会随窗口的扩展/缩小呈现单向变化,具体可总结为两条核心规律:

  • 规律1(坏状态的包含性):若窗口 [left, right] 处于“坏状态”(如含重复字符、和≥target、积≥K),则所有包含该窗口的更大窗口 [left, right+1]、[left, right+2]... 必然也是“坏状态”。此时无需继续扩展 right,应移动 left 缩小窗口,跳过无效区间。

  • 规律2(好状态的被包含性):若窗口 [left, right] 处于“好状态”(如无重复、和<target、积<K),则所有被该窗口包含的更小窗口 [left+1, right]、[left+2, right]... 必然也是“好状态”。此时无需缩小窗口,应继续扩展 right 寻找更优解。

1.3 一句话总结原理

滑动窗口通过 right 指针“扩窗口”探索新的区间,通过 left 指针“缩窗口”剔除无效区间,每个元素最多被加入窗口(right 移动)和移出窗口(left 移动)各一次,最终以 O(n) 时间完成所有有效区间的枚举。

二、滑动窗口的分类:按目标场景划分

滑动窗口的核心逻辑一致,但根据问题目标(求最长、求最短、求计数)的不同,缩窗口的条件和更新答案的时机会有差异。按目标可分为三大类,覆盖绝大多数经典场景:

分类 核心目标 缩窗口条件 更新答案时机 典型问题
类型1:求最长/最大区间 找到满足“好状态”的最长连续区间 窗口进入“坏状态”时,缩 left 至回到“好状态” 缩窗口完成后,每次扩展 right 后更新 无重复字符的最长子串、最长重复子数组
类型2:求最短/最小区间 找到满足“好状态”的最短连续区间 窗口进入“好状态”时,缩 left 至回到“坏状态”(尽可能缩小窗口) 缩窗口过程中,每次缩小 left 后更新 长度最小的子数组、最小覆盖子串
类型3:求计数/统计区间 统计所有满足“好状态”的连续区间个数 窗口进入“坏状态”时,缩 left 至回到“好状态” 缩窗口完成后,累加当前 right 对应的有效区间数(right-left+1) 乘积小于 K 的子数组、找到字符串中所有字母异位词

三、适用场景:3个核心判断标准

并非所有子串/子数组问题都能用滑动窗口,需满足以下 3 个核心条件,缺一不可:

  1. 问题对象是连续区间:滑动窗口仅适用于“连续子串”或“连续子数组”问题,非连续区间(如子序列)不适用。

  2. 窗口状态具有单调性:需满足前文提到的两条规律之一,即扩展/缩小窗口时,状态变化是单向的。反例:“找和为 target 的子数组(含负数值)”,窗口 [left, right] 和为 target 时,扩展 right 可能因负数导致和变小,打破单调性,无法用滑动窗口。

  3. 状态可快速更新:加入 right 元素或移出 left 元素时,窗口的状态(如和、积、字符频率)能在 O(1) 时间内更新,无需重新计算整个窗口状态。

四、通用模板:3类场景统一框架

基于上述分类,提炼出通用模板,只需根据目标调整「缩窗口条件」和「更新答案时机」即可。模板核心步骤:初始化变量 → 扩窗口 → 缩窗口 → 更新答案。

4.0 快速参考表

类型 初始 ans 缩窗口条件 更新答案时机 关键代码
类型1:求最长 0 进入坏状态 缩窗口后,每次扩展 right 后 ans = Math.max(ans, right - left + 1)
类型2:求最短 Infinity 进入好状态 缩窗口过程中 ans = Math.min(ans, right - left + 1)
类型3:求计数 0 进入坏状态 缩窗口后 ans += right - left + 1

4.1 通用模板(TypeScript/JavaScript)

function slidingWindowTemplate<T>(data: T[], targetParam: any): number {
  // 1. 初始化变量
  let left = 0; // 左窗口边界
  let ans = 初始值; // 答案变量(最长→0,最短→Infinity,计数→0)
  let status = 初始状态; // 如对象(字符频率)、sum=0、prod=1

  // 2. 扩窗口:right 遍历所有元素
  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    // 加入右元素,更新状态
    // status.update(rightVal); // 根据具体类型更新

    // 3. 缩窗口:根据目标和当前状态判断是否缩左
    while (缩窗口条件) {
      // 核心差异点:不同类型场景条件不同
      const leftVal = data[left];
      // 移出左元素,更新状态
      // status.remove(leftVal); // 根据具体类型更新
      left++; // 缩小窗口
    }

    // 4. 更新答案:根据类型调整时机
    // 答案更新逻辑
    // 核心差异点:不同类型场景时机不同
  }

  // 5. 处理边界情况(如无满足条件的窗口)
  return 处理后的 ans;
}

4.2 分类型模板细化

类型1:求最长/最大区间

function maxLengthTemplate<T>(data: T[], param: any): number {
  let left = 0;
  let ans = 0; // 最长初始为0
  const status: Record<string, number> = {}; // 对象:记录字符频率

  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    // 更新状态
    status[rightVal as string] = status[rightVal as string] ? status[rightVal as string] + 1 : 1;

    // 缩窗口条件:进入坏状态
    while (坏状态判断) {
      // 如 status[rightVal] > 1(重复字符)
      const leftVal = data[left];
      status[leftVal as string]--;
      left++;
    }

    // 更新答案:缩窗口后,当前窗口是有效最长窗口
    ans = Math.max(ans, right - left + 1);
  }

  return ans;
}

类型2:求最短/最小区间

function minLengthTemplate(data: number[], param: any): number {
  let left = 0;
  let ans = Infinity; // 最短初始为无穷大
  let status = 0; // 如 sumWindow = 0

  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    status += rightVal; // 更新状态

    // 缩窗口条件:进入好状态(尽可能缩小窗口)
    while (好状态判断) {
      // 如 status >= target(和≥目标)
      // 缩窗口时更新答案
      ans = Math.min(ans, right - left + 1);
      const leftVal = data[left];
      status -= leftVal;
      left++;
    }
  }

  // 处理边界:无满足条件的窗口返回0
  return ans !== Infinity ? ans : 0;
}

类型3:求计数/统计区间

function countTemplate(data: number[], param: any): number {
  let left = 0;
  let ans = 0; // 计数初始为0
  let status = 1; // 如 prod = 1

  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    status *= rightVal; // 更新状态

    // 缩窗口条件:进入坏状态
    while (坏状态判断) {
      // 如 status >= K(乘积≥K)
      const leftVal = data[left];
      status /= leftVal;
      left++;
    }

    // 更新答案:当前right对应的有效区间数 = right-left+1
    ans += right - left + 1;
  }

  return ans;
}

五、经典例题实战:逐行拆解

结合模板,拆解 3 类场景的经典例题,帮你理解如何将模板落地到具体问题。

例题1:无重复字符的最长子串(类型1:求最长)

题目描述

给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

解题思路

  • 窗口状态(坏):窗口内存在重复字符;
  • 状态统计:用对象记录窗口内字符的出现次数;
  • 缩窗口条件:当前加入的字符出现次数>1(进入坏状态);
  • 更新答案:缩窗口完成后,计算当前窗口长度,更新最大值。

代码实现

function lengthOfLongestSubstring(s: string): number {
  let left = 0;
  let ans = 0; // 最长子串长度初始为0
  const window: Record<string, number> = {}; // 对象:记录窗口内字符出现次数

  for (let right = 0; right < s.length; right++) {
    const rightChar = s[right];
    // 加入右字符,更新状态
    window[rightChar] = window[rightChar] ? window[rightChar] + 1 : 1;

    // 缩窗口:当当前字符出现次数>1(坏状态),缩左直到无重复
    while (window[rightChar] > 1) {
      const leftChar = s[left];
      window[leftChar]--; // 移出左字符,更新状态
      left++;
    }

    // 更新答案:当前窗口是无重复的有效窗口,计算长度
    ans = Math.max(ans, right - left + 1);
  }

  return ans;
}

复杂度分析

时间复杂度 O(n):每个字符被 right 加入、left 移出各一次;空间复杂度 O(min(m, n))):m 是字符集大小,窗口内字符数不超过 min(m, n)。

例题2:长度最小的子数组(类型2:求最短)

题目描述

给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其和 ≥ target 的长度最小的 连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

解题思路

  • 窗口状态(好):窗口和≥target;
  • 状态统计:用 sumWindow 记录窗口内元素和;
  • 缩窗口条件:sumWindow≥target(进入好状态),缩左以寻找更短窗口;
  • 更新答案:缩窗口过程中,每次缩小后计算窗口长度,更新最小值。

代码实现

function minSubArrayLen(target: number, nums: number[]): number {
  let left = 0;
  let ans = Infinity; // 最短长度初始为无穷大
  let sumWindow = 0; // 窗口内元素和

  for (let right = 0; right < nums.length; right++) {
    sumWindow += nums[right]; // 加入右元素,更新和

    // 缩窗口:和≥target时,尽可能缩小窗口
    while (sumWindow >= target) {
      // 缩窗口时更新答案:当前窗口是有效最短窗口候选
      ans = Math.min(ans, right - left + 1);
      sumWindow -= nums[left]; // 移出左元素,更新和
      left++;
    }
  }

  // 处理边界:无满足条件的窗口返回0
  return ans !== Infinity ? ans : 0;
}

复杂度分析

时间复杂度 O(n):每个元素最多被遍历两次;空间复杂度 O(1):仅用常数级变量。

例题3:乘积小于 K 的子数组(类型3:求计数)

题目描述

给你一个整数数组 nums 和一个整数 k,统计并返回该数组中乘积小于 k 的连续子数组的个数。

解题思路

  • 窗口状态(坏):窗口乘积≥k;
  • 状态统计:用 prod 记录窗口内元素乘积;
  • 缩窗口条件:prod≥k(进入坏状态),缩左直到乘积<k;
  • 更新答案:缩窗口完成后,当前 right 对应的有效子数组数为 right-left+1(即 [left,right]、[left+1,right]...[right,right])。

代码实现

function numSubarrayProductLessThanK(nums: number[], k: number): number {
  // 边界条件:k≤1时,所有正整数乘积≥1,无满足条件的子数组
  if (k <= 1) {
    return 0;
  }
  let left = 0;
  let ans = 0; // 计数初始为0
  let prod = 1; // 窗口内元素乘积

  for (let right = 0; right < nums.length; right++) {
    prod *= nums[right]; // 加入右元素,更新乘积

    // 缩窗口:乘积≥k时,缩左直到乘积<k
    while (prod >= k) {
      prod /= nums[left]; // 移出左元素,更新乘积
      left++;
    }

    // 累加当前right对应的有效子数组数
    // 当窗口 [left, right] 的乘积 < k 时,以 right 结尾的所有子数组都满足条件
    // 即 [left,right]、[left+1,right]...[right,right] 共 right-left+1 个
    ans += right - left + 1;
  }

  return ans;
}

复杂度分析

时间复杂度 O(n):每个元素最多被遍历两次;空间复杂度 O(1):仅用常数级变量。

六、新手避坑指南

  1. 窗口边界统一:建议全程使用「左闭右闭」或「左闭右开」边界定义,不要混用。本文所有例题均采用「左闭右闭」,窗口长度为 right-left+1。

  2. 状态更新顺序:缩窗口时,需先更新状态(如减 sum、除 prod),再移动 left 指针,避免漏算或多算。

  3. 边界条件处理

    • 求最短时,初始 ans 设为无穷大,最后需判断是否更新过(未更新则返回 0);

    • 乘积问题需注意 k≤1 的情况(正整数乘积最小为 1,直接返回 0);

    • 空字符串/空数组需提前返回 0。

  4. 单调性验证:遇到子串/子数组问题时,先手动模拟 2-3 个案例,确认状态是否满足单调性,再决定是否用滑动窗口。

七、总结

滑动窗口的核心是「用单调性压缩遍历维度」,剪枝只是优化手段。掌握它的关键在于:

  1. 判断问题是否满足「连续区间+状态单调性+状态可快速更新」;

  2. 根据目标(最长/最短/计数)确定「缩窗口条件」和「更新答案时机」;

  3. 套用通用模板,灵活调整状态统计工具(哈希表/和/积)。

只要抓住这三点,无论是简单的“无重复子串”,还是复杂的“最小覆盖子串”,都能按此逻辑拆解。建议多做几道经典例题,固化模板思维,面试时就能快速反应。

练习题推荐

按难度和类型分类,建议按顺序练习:

基础题(必做)

进阶题(推荐)

扩展题(挑战)

React自定义Hooks

2025年12月29日 18:25

自定义一些常见的hook,方便在工作中使用,以下内容都是本人工作中的使用经验,不足之处,欢迎指正

useBooleans

import { useState } from 'react'

/**
 * @description 切换true/false的公共hook
 * @param {Boolean} initValue 默认值
 */

export interface UseBooleansActions {
  toggle: () => void // 切换值
  set: (value: boolean) => void // 设置值
  setTrue: () => void // 设置为true
  setFalse: () => void // 设置值为false
  reset: () => void // 重置为初始值
}

function useBooleans(initValue = false): [boolean, UseBooleansActions] {
  const [value, setValue] = useState<boolean>(initValue)

  /** 切换值 */
  const toggle = () => {
    setValue((pre) => !pre)
  }

  /** 设置值 */
  const set = (value: boolean) => {
    setValue(value)
  }

  /** 设置为true */
  const setTrue = () => {
    setValue(true)
  }

  /** 设置为false */
  const setFalse = () => {
    setValue(false)
  }

  /** 重置为初始值 */
  const reset = () => {
    setValue(initValue)
  }

  return [value, { toggle, set, setTrue, setFalse, reset }]
}

export default useBooleans

// 使用
const [booleanValue, { toggle, setTrue, setFalse, reset, set }] = useBooleans(true)

useToggle

import { useMemo, useState } from 'react'

// 定义操作方法的接口
interface Actions<T, U> {
  toggle: () => void
  set: (value: T | U) => void
  setLeft: () => void // 设置为默认值
  setRight: () => void // 设置为取反值
}

function useToggle<T = boolean>(
  defaultValue?: T,
  reverseValue?: T,
): [T, Actions<T, T>]

function useToggle<T, U>(
  defaultValue: T,
  reverseValue: U,
): [T | U, Actions<T | U, T | U>]

function useToggle<D, R>(
  defaultValue: D = false as unknown as D,
  reverseValue?: R,
) {
  // 状态管理:支持默认值和取反值
  const [state, setState] = useState<D | R>(defaultValue)

  // 计算实际的取反值(若未提供则使用布尔取反)
  const reverseValueOrigin =
    reverseValue === undefined ? !defaultValue : reverseValue

  // 缓存操作方法避免重复创建
  const actions = useMemo(() => {
    // 核心切换逻辑:在默认值和取反值之间切换
    const toggle = () =>
      setState(
        (s) =>
          (s === defaultValue ? reverseValueOrigin : defaultValue) as D | R,
      )
    const set = (value: D | R) => setState(value)
    const setLeft = () => setState(defaultValue)
    const setRight = () => setState(reverseValueOrigin as D | R)

    return { toggle, set, setLeft, setRight }
  }, [defaultValue, reverseValueOrigin])

  return [state, actions]
}

export default useToggle

// 使用
const [toggleValue, { toggle: toggle1, set: set1, setLeft, setRight }] =
    useToggle<'left', 'right'>('left', 'right')

useExcel

基于xlsx的一些常见的导入导出功能封装

import { getTypeOf, isAvailableArr } from '@/utils'
import { message } from 'antd'
import type {
  BookType,
  ColInfo,
  Range,
  RowInfo,
  Sheet2JSONOpts,
  WorkBook,
} from 'xlsx'
import * as XLSX from 'xlsx'

// 导出文件配置
export type IExportConfig = {
  name?: string // 导出文件名称
  bookType?: BookType // 导出文件类型
  sheetName?: string // sheet名称
  errorMsg?: string // 错误提示
  headers?: Record<string, string> // 自定义表头,导出的文件里面只有在定义中的字段,并且如果数据为空的话,只生成一个表头。示例:{id: 'ID', name: '链接名称', site_type: '官网类型'}
  merges?: Range[] // 单元格合并
  colInfo?: ColInfo[] // 列属性
  rowInfo?: RowInfo[] // 行属性
}

// 多sheet导出
export type IExtraSheetConfig = {
  name?: string // 导出文件名称
  bookType?: BookType // 导出文件类型
  sheets: ({ json: any[] } & Omit<IExportConfig, 'name' | 'bookType'>)[]
}

/**
 * @desc 自定义导出文件hook
 */
const useExcel = () => {
  /**
   * @desc 一维数组导出Excel文件
   *
   * 此函数将一个一维数组转换为Excel文件并下载
   * 支持自定义表头、单元格合并、列属性和行属性
   *
   * @param {any[]} json - 要导出的数据数组
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.sheetName='Sheet1'] - 工作表名称
   * @param {Record<string, string>} [config.headers] - 自定义表头映射
   * @param {Range[]} [config.merges] - 单元格合并范围
   * @param {ColInfo[]} [config.colInfo] - 列属性配置
   * @param {RowInfo[]} [config.rowInfo] - 行属性配置
   * @returns {void}
   */
  function exportJson2Excel<T = any>(json: T[], config?: IExportConfig) {
    const {
      name = '导出',
      sheetName = 'Sheet1',
      bookType = 'xlsx',
      headers,
      merges,
      colInfo,
      rowInfo,
    } = config || {}
    let lists = [...json]
    if (
      headers &&
      getTypeOf(headers) === 'Object' &&
      isAvailableArr(Object.keys(headers))
    ) {
      // 没有数据的时候用header去生成一个空表头
      if (!isAvailableArr(lists)) {
        const headersField = Object.values(headers)
        const headerObj: Record<string, any> = {}
        headersField.forEach((f) => {
          headerObj[f] = null
        })
        lists = [headerObj as T]
      } else {
        // 有数据的时候根据header去生成
        const headerFields = Object.entries(headers)
        lists = json.map((j) => {
          const obj: Record<string, any> = {}
          for (const [key, value] of headerFields) {
            obj[value] = j[key as keyof T] ?? null
          }
          return obj
        }) as T[]
      }
    }

    const wb = XLSX.utils.book_new()
    const ws = XLSX.utils.json_to_sheet(lists)
    if (merges) {
      ws['!merges'] = merges
    }
    if (colInfo) {
      ws['!cols'] = colInfo
    }
    if (rowInfo) {
      ws['!rows'] = rowInfo
    }
    XLSX.utils.book_append_sheet(wb, ws, sheetName)
    XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
  }

  /**
   * @desc 一维数组多sheet导出Excel文件
   *
   * 此函数将多个一维数组分别放在不同的工作表中导出为一个Excel文件
   * 支持自定义表头、单元格合并、列属性和行属性
   *
   * @param {IExtraSheetConfig} params - 多sheet导出配置项
   * @param {string} [params.name='导出'] - 导出文件名称
   * @param {BookType} [params.bookType='xlsx'] - 导出文件类型
   * @param {Array} params.sheets - 工作表配置数组
   * @param {any[]} params.sheets[].json - 要导出的数据数组
   * @param {string} [params.sheets[].sheetName] - 工作表名称
   * @param {Record<string, string>} [params.sheets[].headers] - 自定义表头映射
   * @param {Range[]} [params.sheets[].merges] - 单元格合并范围
   * @param {ColInfo[]} [params.sheets[].colInfo] - 列属性配置
   * @param {RowInfo[]} [params.sheets[].rowInfo] - 行属性配置
   * @returns {void}
   */
  function exportJson2ExcelSheets<T = any>(params: IExtraSheetConfig) {
    const { name = '导出', bookType = 'xlsx', sheets } = params || {}
    const wb = XLSX.utils.book_new()
    if (isAvailableArr(sheets)) {
      sheets?.forEach((s) => {
        const { json, headers, merges, colInfo, rowInfo, sheetName } = s || {}
        let lists = [...json]
        if (
          headers &&
          getTypeOf(headers) === 'Object' &&
          isAvailableArr(Object.keys(headers))
        ) {
          // 没有数据的时候用header去生成一个空表头
          if (!isAvailableArr(lists)) {
            const headersField = Object.values(headers)
            const headerObj: Record<string, any> = {}
            headersField.forEach((f) => {
              headerObj[f] = null
            })
            lists = [headerObj as T]
          } else {
            // 有数据的时候根据header去生成
            const headerFields = Object.entries(headers)
            lists = json.map((j) => {
              const obj: Record<string, any> = {}
              for (const [key, value] of headerFields) {
                obj[value] = j[key] ?? null
              }
              return obj
            }) as T[]
          }
        }

        const ws = XLSX.utils.json_to_sheet(lists)
        if (merges) {
          ws['!merges'] = merges
        }
        if (colInfo) {
          ws['!cols'] = colInfo
        }
        if (rowInfo) {
          ws['!rows'] = rowInfo
        }
        XLSX.utils.book_append_sheet(wb, ws, sheetName)
      })
    } else {
      const ws = XLSX.utils.json_to_sheet([])
      XLSX.utils.book_append_sheet(wb, ws)
    }

    XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
  }

  /**
   * @desc 二维数组导出Excel文件
   *
   * 此函数将一个二维数组转换为Excel文件并下载
   * 支持单元格合并、列属性和行属性
   *
   * @param {any[][]} aoas - 要导出的二维数组
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.sheetName='Sheet1'] - 工作表名称
   * @param {Range[]} [config.merges] - 单元格合并范围
   * @param {ColInfo[]} [config.colInfo] - 列属性配置
   * @param {RowInfo[]} [config.rowInfo] - 行属性配置
   * @returns {void}
   */
  function exportAoa2Excel<T = any>(aoas: T[][], config?: IExportConfig) {
    const {
      name = '导出',
      sheetName = 'Sheet1',
      bookType = 'xlsx',
      merges,
      colInfo,
      rowInfo,
    } = config || {}

    const wb = XLSX.utils.book_new()
    const ws = XLSX.utils.aoa_to_sheet(aoas)
    if (merges) {
      ws['!merges'] = merges
    }
    if (colInfo) {
      ws['!cols'] = colInfo
    }
    if (rowInfo) {
      ws['!rows'] = rowInfo
    }

    XLSX.utils.book_append_sheet(wb, ws, sheetName)
    XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
  }

  /**
   * @desc 从本地文件读取Excel工作簿
   *
   * 此函数从File或Blob对象读取Excel文件并返回工作簿对象
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @returns {Promise<WorkBook | false>} 返回工作簿对象或false(读取失败时)
   */
  function readWorkbookFromLocalFile(
    file: File | Blob,
  ): Promise<WorkBook | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })
        resolve(workbook)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 从本地Excel文件读取数据为JSON格式
   *
   * 此函数从File或Blob对象读取Excel文件并返回第一个工作表的数据为JSON数组
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @param {Sheet2JSONOpts} [options] - 工作表转JSON的选项
   * @returns {Promise<any[] | false>} 返回JSON数组或false(读取失败时)
   */
  function readFileToJson<T = any>(
    file: File | Blob,
    options?: Sheet2JSONOpts,
  ): Promise<T[] | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })
        const json = XLSX.utils.sheet_to_json<T>(
          workbook.Sheets[workbook.SheetNames[0]],
          options,
        )
        resolve(json)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 从本地Excel文件读取多个工作表的数据为JSON格式
   *
   * 此函数从File或Blob对象读取Excel文件并返回所有工作表的数据为JSON对象
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @param {Sheet2JSONOpts} [options] - 工作表转JSON的选项
   * @returns {Promise<Record<string, any[]> | false>} 返回包含所有工作表数据的对象或false(读取失败时)
   */
  function readFileToJsons(
    file: File | Blob,
    options?: Sheet2JSONOpts,
  ): Promise<Record<string, any[]> | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = e.target?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })

        const sheetNames = workbook.SheetNames
        if (!sheetNames?.length) {
          resolve(false)
        }

        const jsons = sheetNames.reduce(
          (pre: Record<string, any[]>, cur: string) => {
            const json = XLSX.utils.sheet_to_json(workbook.Sheets[cur], options)
            pre[cur] = json
            return pre
          },
          {},
        )

        resolve(jsons)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 文件流导出Excel文件
   *
   * 此函数从File或Blob对象读取Excel文件流并导出为Excel文件
   *
   * @param {File | Blob} file - 要导出的Excel文件流
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.errorMsg='下载失败'] - 错误提示信息
   * @returns {Promise<void>}
   */
  async function exportBuffer2Excel(file: File | Blob, config?: IExportConfig) {
    const {
      name = '导出',
      bookType = 'xlsx',
      errorMsg = '下载失败',
    } = config || {}
    const wb: WorkBook | false = await readWorkbookFromLocalFile(file)
    if (wb) {
      XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
    } else {
      message.error(errorMsg)
    }
  }

  /**
   * @desc 从URL导出Excel文件
   *
   * 此函数从URL获取Excel文件并导出
   *
   * @param {string} url - Excel文件的URL地址
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.errorMsg='下载失败'] - 错误提示信息
   * @returns {Promise<void>}
   */
  async function exportUrl2Excel(url: string, config?: IExportConfig) {
    fetch(url)
      .then((response) => response.blob())
      .then((blob) => {
        exportBuffer2Excel(new Blob([blob]), config)
      })
      .catch((error) => {
        throw error
      })
  }

  /**
   * @desc 从工作表中读取指定列的数据
   *
   * 此函数从Excel文件的指定工作表中读取指定列的数据
   *
   * @param {File | Blob} file - Excel文件
   * @param {number} [sheetIndex=0] - 工作表索引
   * @param {string[]} [columns=['A']] - 要读取的列数组(如['A', 'B'])
   * @param {Record<string, string>} [fieldsMap={}] - 列名到字段名的映射
   * @returns {Promise<any[] | false>} 返回包含指定列数据的数组或false(读取失败时)
   */
  function readColumnFromSheet(
    file: File | Blob,
    sheetIndex: number = 0,
    columns: string[] = ['A'],
    fieldsMap: Record<string, string> = {},
  ): Promise<any[] | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })

        const sheetName = workbook.SheetNames[sheetIndex]
        const worksheet = workbook.Sheets[sheetName]

        // 获取 A 列的所有数据,跳过第一行(标题行)
        const columnData = []
        const range = XLSX.utils.decode_range(worksheet['!ref'] as string)

        // 从第2行开始(索引为1,跳过标题行)
        for (let row = range.s.r + 1; row <= range.e.r; row++) {
          const res: Record<string, any> = {}
          columns.forEach((col: string) => {
            const columnIndex = XLSX.utils.decode_col(col) // 将列字母转换为索引
            const cellAddress = XLSX.utils.encode_cell({
              r: row,
              c: columnIndex,
            }) // A列是第0列
            const cell = worksheet[cellAddress]
            const field = fieldsMap[col] ?? col
            res[field] = cell ? cell.v : ''
          })
          columnData.push(res)
        }

        resolve(columnData)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 从工作表中获取指定列的数据
   *
   * 此函数从Excel工作表中获取指定列的所有数据(跳过标题行)
   *
   * @param {XLSX.WorkSheet} worksheet - Excel工作表对象
   * @param {string} column - 要获取数据的列(如'A'、'B')
   * @returns {any[]} 返回指定列的数据数组
   */
  function getColumnData(worksheet: XLSX.WorkSheet, column: string) {
    const columnData = []
    const range = XLSX.utils.decode_range(worksheet['!ref'] as string)
    const columnIndex = XLSX.utils.decode_col(column) // 将列字母转换为索引

    for (let row = range.s.r + 1; row <= range.e.r; row++) {
      const cellAddress = XLSX.utils.encode_cell({ r: row, c: columnIndex })
      const cell = worksheet[cellAddress]
      columnData.push(cell ? cell.v : null)
    }

    return columnData
  }

  /**
   * @desc 从本地文件读取Excel工作簿
   *
   * 此函数从File或Blob对象读取Excel文件并返回工作簿对象
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @param {XLSX.ParsingOptions} [options] - 解析选项
   * @returns {Promise<WorkBook | false>} 返回工作簿对象或false(读取失败时)
   */
  function readWorkbookFromFile(
    file: File | Blob,
    options: XLSX.ParsingOptions = {
      type: 'binary',
      raw: true,
      cellNF: true,
    },
  ): Promise<WorkBook | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, options)
        resolve(workbook)
      }
    })
  }

  /**
   * @desc 从URL读取Excel工作簿
   *
   * 此函数从URL读取Excel文件并返回工作簿对象
   *
   * @param {string} url - Excel文件的URL地址
   * @param {XLSX.ParsingOptions} [options] - 解析选项
   * @returns {WorkBook} 返回工作簿对象
   */
  function readWorkbookFromUrl(
    url: string,
    options: XLSX.ParsingOptions = {
      type: 'string',
      raw: true,
      cellNF: true,
    },
  ): WorkBook {
    const workbook = XLSX.readFile(url, options)
    return workbook
  }

  /**
   * @desc 从URL读取Excel工作簿
   *
   * 此函数从URL读取Excel文件并返回工作簿对象
   *
   * @param {string} url - Excel文件的URL地址
   * @param {XLSX.ParsingOptions} [options] - 解析选项
   * @returns {WorkBook} 返回工作簿对象
   */
  async function readWorkbookFromUrl1(url: string): Promise<WorkBook | false> {
    try {
      const res = await fetch(url)
      const blob = await res.blob()
      const wb = readWorkbookFromFile(new Blob([blob]))
      return Promise.resolve(wb)
    } catch  {
      return Promise.resolve(false)
    }
  }

  /**
   * @desc 保存工作簿为Excel文件
   *
   * 此函数将工作簿对象保存为Excel文件
   *
   * @param {WorkBook} workbook - 要保存的工作簿对象
   * @param {string} fileName - 文件名
   * @param {XLSX.WritingOptions} [options] - 写入选项
   * @returns {void}
   */
  function saveWbToExcel(
    workbook: WorkBook,
    fileName: string,
    options?: XLSX.WritingOptions,
  ) {
    XLSX.writeFile(workbook, fileName, options)
  }

  return {
    exportJson2Excel,
    exportAoa2Excel,
    exportBuffer2Excel,
    exportJson2ExcelSheets,
    exportUrl2Excel,
    readWorkbookFromLocalFile,
    readFileToJson,
    readFileToJsons,
    readColumnFromSheet,
    getColumnData,
    readWorkbookFromFile,
    readWorkbookFromUrl,
    readWorkbookFromUrl1,
    saveWbToExcel,
  }
}

export default useExcel

// 使用
const {
    exportJson2Excel,
    readFileToJson,
    readFileToJsons,
    readColumnFromSheet,
    readWorkbookFromUrl,
    readWorkbookFromUrl1,
    readWorkbookFromFile,
    saveWbToExcel,
  } = useExcel()

useSet

import { useMemo, useState } from 'react'

// 定义操作方法的接口
interface Actions<T> {
  add: (key: T) => void // 新增一条数据
  set: (map: Set<T>) => void // 设置全部数据
  remove: (key: T) => void // 删除某条数据
  clear: () => void // 清空数据
  reset: () => void // 重置为默认值
}

function useSet<T>(defaultValue: Set<T> = new Set()): [Set<T>, Actions<T>] {
  const [sets, setSets] = useState<Set<T>>(defaultValue)

  // 缓存操作方法避免重复创建
  const actions = useMemo(() => {
    // 新增数据
    const add = (value: T) => {
      const newSet = new Set(sets)
      newSet.add(value)
      setSets(newSet)
    }

    // 设置全部数据
    const set = (sets: Set<T>) => {
      setSets(sets)
    }

    // 删除数据
    const remove = (key: T) => {
      const newSet = new Set(sets)
      newSet.delete(key)
      setSets(newSet)
    }

    // 清空数据
    const clear = () => {
      const newSet = new Set<T>()
      setSets(newSet)
    }

    // 重置为默认值
    const reset = () => {
      setSets(defaultValue)
    }

    return { add, set, remove, clear, reset }
  }, [defaultValue, sets])

  return [sets, actions] as const
}

export default useSet

useMap

import { useMemo, useState } from 'react'

// 定义操作方法的接口
interface Actions<T, U> {
  add: (key: T, value: U) => void // 新增一条数据
  get: (key: T) => U | undefined // 获取一条数据
  set: (map: Map<T, U>) => void // 设置全部数据
  remove: (key: T) => void // 删除某条数据
  clear: () => void // 清空数据
  reset: () => void // 重置为默认值
}

function useMap<T, U>(
  defaultValue: Map<T, U> = new Map(),
): [Map<T, U>, Actions<T, U>] {
  const [map, setMap] = useState<Map<T, U>>(defaultValue)

  // 缓存操作方法避免重复创建
  const actions = useMemo(() => {
    // 新增数据
    const add = (key: T, value: U) => {
      const newMap = new Map(map)
      newMap.set(key, value)
      setMap(newMap)
    }

    // 获取数据
    const get = (key: T) => {
      return map.get(key)
    }

    // 设置全部数据
    const set = (map: Map<T, U>) => {
      setMap(map)
    }

    // 删除数据
    const remove = (key: T) => {
      const newMap = new Map(map)
      newMap.delete(key)
      setMap(newMap)
    }

    // 清空数据
    const clear = () => {
      const newMap = new Map(map)
      newMap.clear()
      setMap(newMap)
    }

    // 重置为默认值
    const reset = () => {
      setMap(defaultValue)
    }

    return { add, get, set, remove, clear, reset }
  }, [defaultValue, map])

  return [map, actions] as const
}

export default useMap

usePrevious

import { useRef } from 'react'

type ShouldUpdateFn<T> = (prev: T | undefined, next: T) => boolean
const shouldUpdate = <T>(prev: T | undefined, next: T) => !Object.is(prev, next)
export default function usePrevious<T>(
  state: T,
  shouldUp: ShouldUpdateFn<T> = shouldUpdate,
): T | undefined {
  const curRef = useRef<T>() // 当前值
  const preRef = useRef<T>() // 上一个值

  if (shouldUp(curRef.current, state)) {
    preRef.current = curRef.current
    curRef.current = state
  }

  return preRef.current
}

useLatest

import { RefObject, useRef } from 'react'

// 获取某个state的最新值的hook
export default function useLatest<S>(value: S): RefObject<S> {
  // useRef 保存能保证每次获取到的都是最新的值
  const curRef = useRef<S>(value)
  curRef.current = value

  return curRef
}

useSafeState

import { useUnmount } from 'ahooks'
import { Dispatch, SetStateAction, useRef, useState } from 'react'

// 主要是实现 setRafState 方法,在外部调用 setRafState 方法时,会取消上一次的 setState 回调函数,并执行 requestAnimationFrame 来控制 setState 的执行时机
export default function useSafeState<S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>] {
  const [state, setState] = useState(initialState)
  const reqId = useRef(0)
  const setSafeState = (state: SetStateAction<S>) => {
    cancelAnimationFrame(reqId.current)

    reqId.current = requestAnimationFrame(() => {
      // 在回调执行真正的 setState
      setState(state)
    })
  }

  useUnmount(() => {
    cancelAnimationFrame(reqId.current)
  })

  return [state, setSafeState] as const
}

useSetState

import { getTypeOf } from '@/utils'
import { SetStateAction, useCallback, useRef, useState } from 'react'

// 类似于以前的setState,有一个回调函数,回调函数的参数是最新的值
export default function useSetState<S>(
  initialState: S | (() => S),
): [S, (state: SetStateAction<S>, cb?: (state: S) => void) => void] {
  const [state, set] = useState(initialState)
  const curRef = useRef<S>(state)
  curRef.current = state

  const setState = useCallback(
    (state: SetStateAction<S>, cb?: (state: S) => void) => {
      set((pre) => {
        const newState =
          getTypeOf(state) === 'Function'
            ? (state as (preState: S) => S)(pre)
            : (state as S)
        curRef.current = newState
        return newState
      })
      cb?.(curRef.current)
    },
    [],
  )

  return [state, setState] as const
}

useMergeStates

import { getTypeOf } from '@/utils'
import { Dispatch, SetStateAction, useCallback, useState } from 'react'

// 合并state属性,使用的时候不用整体重新赋值
export default function useMergeStates<S extends Record<string, any>>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<Partial<S>>>] {
  const [state, set] = useState(initialState)

  const setState = useCallback((patch: SetStateAction<Partial<S>>) => {
    set((pre) => {
      const newState =
        getTypeOf(patch) === 'Function'
          ? (patch as (preState: Partial<S>) => S)(pre)
          : (patch as S)

      return newState ? { ...pre, ...newState } : pre
    })
  }, [])

  return [state, setState] as const
}

useGetState

import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'

// 在useState的基础上增加获取方法
export default function useGetState<S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, () => S] {
  const [state, setState] = useState(initialState)
  const curRef = useRef(state)
  curRef.current = state

  const getState = useCallback(() => curRef.current, [])

  return [state, setState, getState] as const
}

useReactive

import { useRef } from 'react'
import useToggle from './useToggle'

// 响应式对象
export default function useReactive<S extends object>(initialState: S): S {
  const [, { toggle }] = useToggle(false)
  const curRef = useRef(initialState)

  const proxy = new Proxy(curRef.current, {
    set(target, key, value) {
      const ret = Reflect.set(target, key, value)
      toggle() // 属性赋值时触发回调
      return ret
    },
    get(target, key) {
      const ret = Reflect.get(target, key)
      return ret
    },
    defineProperty(target, key) {
      const ret = Reflect.deleteProperty(target, key)
      toggle() // 属性删除时触发回调
      return ret
    },
  })

  return proxy
}

useOptions

import { DefaultOptionType } from 'antd/lib/select'
import { useEffect, useState } from 'react'

// 账号列表项
export type UseOptionsProps<T = DefaultOptionType, R = any> = {
  labelField?: keyof R // 要取的作为label的字段
  labelFormat?: (record: R) => any // label字段自定义格式化函数
  valueField?: keyof R // 要取的作为value的字段
  valueFormat?: (record: R) => any // value字段自定义格式化函数
  resField?: string[] // 要取的返回值字段(比如res、list、resutl等)
  dataFn: (...args: any) => Promise<any> // 要请求的接口
  fnParams?: any // 请求的接口参数
  definedFormat?: (data: R[]) => T[] // 自定义格式化数据函数
}

/**
 * @desc 自定义获取任意接口并格式化为下拉options的hook
 * @param  T 最终返回的数据的类型
 * @param  R 接口返回的数据的类型
 */
function useOptions<T = DefaultOptionType, R = any>(
  props: UseOptionsProps<T, R>,
): [T[], R[], () => void] {
  const {
    labelField,
    valueField,
    labelFormat,
    valueFormat,
    fnParams,
    dataFn,
    resField,
    definedFormat,
  } = props || {}
  const [options, setOptions] = useState<T[]>([]) // 下拉选项
  const [datas, setDatas] = useState<R[]>([]) // 原始数据

  /** 格式化为通用下拉选项结构 */
  const formatToCommonOptions = (list: R[]): T[] => {
    return list?.map((item: R) => {
      const label = labelFormat
        ? labelFormat?.(item)
        : labelField
        ? item?.[labelField]
        : item?.['name' as keyof R] || '' // label取值
      const value = valueFormat
        ? valueFormat?.(item)
        : valueField
        ? item?.[valueField]
        : item?.['id' as keyof R] || '' // value取值

      return { label, value } as T
    })
  }

  // 获取数据
  const getDatas = async () => {
    try {
      const res = fnParams ? await dataFn(fnParams) : await dataFn()

      const list: R[] =
        resField && resField?.length
          ? resField.reduce((pre, cur) => pre?.[cur], res)
          : res
      if (list && Array.isArray(list)) {
        setDatas(list) // 保存原始数据
        setOptions(
          definedFormat ? definedFormat(list) : formatToCommonOptions(list),
        ) // 保存格式化数据
      }
    } catch {
      //
    }
  }

  useEffect(() => {
    getDatas()
  }, [])

  return [options, datas, getDatas]
}

export default useOptions

useSse

import { useEffect, useRef, useState } from 'react'

// 定时器的类型
type TimerType = ReturnType<typeof setTimeout> | undefined

/**
 * @description SSE暴露的方法
 */
export interface SSEMethods {
  close: () => void
  reconnect: () => void
}

/**
 * SSE连接状态枚举
 */
export enum SSEReadyState {
  CONNECTING, // 连接中
  OPEN, // 已连接
  CLOSED, // 已关闭
}

/**
 * @description 初始化参数
 */
export type EventSourceInit = {
  withCredentials?: boolean
  headers?: Record<string, string>
  payload?: string
}

/**
 * @description SSE连接需要的参数
 * @param T 当前SSE连接的数据结构
 */
export interface SSEProps {
  events?: string[] // 事件名称
  retryInterval?: number // 重试间隔
  heartbeatTimeout?: number // 心跳超时时间
  init?: EventSourceInit // 初始化参数
  onMessage?: (event: MessageEvent) => void // 消息回调
  autoConnect?: boolean // 是否自动连接
  maxRetries?: number // 最大重试次数
}

/**
 * @description SSE连接返回结构
 * @param T 当前SSE连接的数据结构
 */
export interface SSEReturn<T> {
  data: T | null // 当前SSE连接的数据结构
  error: Error | null // 错误
  readyState: SSEReadyState // 连接状态
  methods: SSEMethods // 暴露的方法
  retryCount: number // 当前重试次数
}

/**
 * @description SSE连接状态枚举
 */
export const READY_STATE_MAP = {
  [SSEReadyState.CONNECTING]: '连接中...',
  [SSEReadyState.OPEN]: '已连接',
  [SSEReadyState.CLOSED]: '已关闭',
}

/**
 * @description SSE连接状态枚举
 */
export const READY_STATE_TYPE_MAP = {
  [SSEReadyState.CONNECTING]: 'warning',
  [SSEReadyState.OPEN]: 'success',
  [SSEReadyState.CLOSED]: 'error',
}

/**
 * @description 通用的SSE连接HOOK
 * @param {SSEProps} 连接参数
 */
// 连接地址
const useSse = <T,>(url: string, options: SSEProps): SSEReturn<T> => {
  const {
    autoConnect,
    maxRetries,
    retryInterval = 5 * 1000,
    heartbeatTimeout,
    init,
    events,
    onMessage,
  } = options || {}

  const [readyState, setReadyState] = useState<SSEReadyState>(
    SSEReadyState.CONNECTING,
  ) // 当前连接状态
  const [data, setData] = useState<T | null>(null) // 当前SSE数据
  const [error, setError] = useState<Error | null>(null) // 错误
  const sseRef = useRef<EventSource | null>(null) // SSE实例
  const retryTimer = useRef<TimerType | null>(null) // 重试的定时器
  const heartbeatTimer = useRef<TimerType | null>(null) // 心跳的定时器
  const retryCount = useRef<number>(0) // 当前重试次数

  // 初始化SSE
  const initSSE = () => {
    try {
      const sseInstance = new EventSource(url, init)
      sseRef.current = sseInstance

      // sse连接打开事件
      sseInstance.onopen = () => {
        console.log('SSE连接已打开')
        setReadyState(SSEReadyState.OPEN)
        retryCount.current = 0 // 重置重试次数
        startHeatbeat() // 开始心跳
        bindEvents()
      }

      sseInstance.onerror = (e) => {
        setError(new Error(`SSE Error: ${e.type}`))
        setReadyState(SSEReadyState.CLOSED)
        if (autoConnect !== false) reconnect()
      }

      sseInstance.onmessage = handleMessage
    } catch (error) {
      setError(error as Error)
      setReadyState(SSEReadyState.CLOSED)
      if (autoConnect !== false) reconnect()
    }
  }

  // 消息处理事件
  const handleMessage = (event: MessageEvent) => {
    const parsedData = JSON.parse(event.data) as T
    setData(parsedData)
    onMessage?.(event)
  }

  // 重置重试的定时器
  const resetRetryTimer = () => {
    if (retryTimer.current) {
      clearTimeout(retryTimer.current)
      retryTimer.current = null
    }
  }

  // 重置心跳的定时器
  const resetHeartbeatTimer = () => {
    if (heartbeatTimer.current) {
      clearTimeout(heartbeatTimer.current)
      heartbeatTimer.current = null
    }
  }

  // 重连SSE
  const reconnect = () => {
    // 超过最大重试次数就不重试了
    if (maxRetries && retryCount.current >= maxRetries) {
      setReadyState(SSEReadyState.CLOSED)
      return
    }

    resetRetryTimer()
    resetHeartbeatTimer()

    retryTimer.current = setTimeout(() => {
      setReadyState(SSEReadyState.CONNECTING)
      retryCount.current += 1 // 重试次数+1
      initSSE()
    }, retryInterval * (retryCount.current + 1))
  }

  // 开始心跳
  const startHeatbeat = () => {
    if (heartbeatTimeout) {
      resetHeartbeatTimer()
      heartbeatTimer.current = setTimeout(() => {
        setReadyState(SSEReadyState.CLOSED)
        disconnectSSE()
        initSSE()
      }, heartbeatTimeout)
    }
  }

  // 绑定自定义事件
  const bindEvents = () => {
    if (sseRef && events?.length) {
      events?.forEach((eventName) => {
        ;(sseRef as unknown as EventSource).addEventListener(
          eventName,
          handleMessage,
        )
      })
    }
  }

  // 解绑自定义事件
  const unbindEvents = () => {
    if (sseRef && events?.length) {
      events?.forEach((eventName) => {
        ;(sseRef as unknown as EventSource).removeEventListener(
          eventName,
          handleMessage,
        )
      })
    }
  }

  useEffect(() => {
    // 如果是自动连接
    if (autoConnect !== false) {
      initSSE()
    }
    return close
  }, [])

  // 断开SSE连接
  const disconnectSSE = () => {
    const sseInstance = sseRef.current
    if (sseInstance) {
      sseInstance.close()
      sseRef.current = null
    }
  }

  // 关闭
  const close = () => {
    disconnectSSE()
    setReadyState(SSEReadyState.CLOSED)
    resetRetryTimer()
    resetHeartbeatTimer()
    retryCount.current = 0
    setData(null)
    unbindEvents()
  }

  const methods: SSEMethods = {
    close,
    reconnect: () => {
      close()
      initSSE()
    },
  }

  return { data, readyState, retryCount: retryCount.current, error, methods }
}

export default useSse

useWebSocket

import { HeartBeatType } from '@/enums/common'
import { useCallback, useMemo, useRef, useState } from 'react'

// 心跳类型枚举
enum HeartBeatType {
  Ping = 'heartBeatPing', // 心跳发送的消息
  Pong = 'heartBeatPong', // 心跳接收的消息
}

type TimerType = ReturnType<typeof setTimeout> | undefined // 定时器的类型

// useWebSocket的参数类型
export interface IWebSocketProps {
  url?: string // socket连接地址
  heartBeatInterval?: number // 心跳检测间隔
  heartBeatData?: HeartBeatType // 心跳检测发送数据
  heartBeatSendData?: any // 心跳检测实际发送数据
  maxReconnectAttempts?: number // 最大重连次数
  reconnectInterval?: number // 重连间隔
}

// 发送数据的类型
export type SendData = string | ArrayBufferLike | Blob | ArrayBufferView

/**
 * @description WebSocket hook封装
 */
export default function useWebSocket(options: IWebSocketProps) {
  const {
    url,
    heartBeatInterval = 60 * 1000,
    heartBeatData = HeartBeatType.Ping,
    heartBeatSendData,
    maxReconnectAttempts = 10,
    reconnectInterval = 5 * 1000,
  } = options

  const socket = useRef<WebSocket>() // socket实例
  const [lastMessage, setLastMessage] = useState<MessageEvent>() // socket消息
  const reconnectAttempts = useRef<number>(0) // 已经尝试重连了的次数
  const error = useRef<Event>() // error
  const curUrlRef = useRef<string>() // curUrlRef

  let heartBeatTimer: TimerType = undefined // 心跳检测的定时器
  let reconnectTimer: TimerType = undefined // 重连的定时器

  // 是否已连接
  const isConnected = useMemo(
    () => socket.current?.readyState === WebSocket.OPEN,
    [socket],
  )

  // 连接
  const connect = useCallback(
    (curUrl: string) => {
      // 如果已存在 WebSocket 实例,则先关闭它
      if (socket) {
        socket.current?.close()
      }

      if (curUrl && !curUrlRef.current) {
        curUrlRef.current = curUrl
      }

      const socketInstance = new WebSocket(curUrl || url!)
      socket.current = socketInstance
      socketInstance.onopen = onopen
      socketInstance.onmessage = onmessage
      socketInstance.onclose = onclose
      socketInstance.onerror = onerror
    },
    [url],
  )

  // 接收消息
  const onmessage = (data: any) => {
    setLastMessage(data)
  }

  // 连接成功
  const onopen = () => {
    console.log('连接成功')
    // 重置重连次数
    reconnectAttempts.current = 0
    // 开始心跳检测
    checkHealthStart()
  }

  // 连接断开
  const onclose = () => {
    console.log('连接断开')
    // 结束心跳检测
    checkHealthEnd()
  }

  // 连接出错
  const onerror = (e: Event) => {
    console.log('连接出错')
    error.current = e
    // 结束心跳检测
    checkHealthEnd()
    // 重连
    reconnect()
  }

  // 断开连接
  const disconnect = () => {
    console.log('连接断开')
    socket.current?.close()
    // 结束心跳检测
    checkHealthEnd()
  }

  // 发送消息
  const sendMessage = (data: SendData) => {
    console.log('即将发送的数据', data)
    if (socket.current?.readyState === WebSocket.OPEN) {
      console.log('真实发送的数据', data)
      socket.current?.send(data)
    }
  }

  // 心跳检测开始
  const checkHealthStart = () => {
    console.log('心跳检测开始')
    checkHealthEnd()
    heartBeatTimer = setTimeout(() => {
      if (socket.current?.readyState === WebSocket.OPEN) {
        sendMessage(
          heartBeatSendData ||
            JSON.stringify({
              type: 'ping',
              data: heartBeatData,
            }),
        )
        checkHealthStart()
      }
    }, heartBeatInterval)
  }

  // 心跳检测结束
  const checkHealthEnd = () => {
    console.log('心跳检测结束')
    clearTimeout(heartBeatTimer)
    heartBeatTimer = undefined
  }

  // 重连
  const reconnect = () => {
    console.log('开始重连')

    // 没有连接并且没有达到重连次数
    if (
      socket.current?.readyState !== WebSocket.OPEN &&
      reconnectAttempts.current <= maxReconnectAttempts
    ) {
      // 使用递增的延迟来避免频繁重连
      reconnectTimer = setTimeout(() => {
        connect(curUrlRef.current || url!)
      }, reconnectInterval * reconnectAttempts.current + 1)
      // 增加重连尝试次数
      reconnectAttempts.current += 1
    } else {
      clearTimeout(reconnectTimer)
      reconnectTimer = undefined
    }
  }

  // 取消重连
  const cancelReconnect = () => {
    clearTimeout(reconnectTimer)
    reconnectTimer = undefined
  }

  return {
    socket,
    lastMessage,
    isConnected,
    error,
    connect,
    disconnect,
    reconnect,
    checkHealthStart,
    sendMessage,
    cancelReconnect,
  }
}

useLocalforage

import localforage from 'localforage'

/**
 * @desc localForage 是一个 JavaScript 库,通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。
  localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。
  localForage 提供回调 API 同时也支持 ES6 Promises API,你可以自行选择。
  * @return getItem 获取某个key的值
  * @return setItem 设置某个key的值
  * @return removeItem 移除某个key的值
  * @return clearItems 移除所有key的值,此方法将会删除离线仓库中的所有值。谨慎使用此方法。
  * @return itemsLength 获取离线仓库中的 key 的数量(即数据仓库的“长度”)
  * @return key 根据 key 的索引获取其名
  * @return keys 获取数据仓库中所有的 key,包含所有 key 名的数组
 */
const useLocalforage = <T = any>(): [
  getItem: (key: string) => Promise<T | null>, // 获取某个key的值
  setItem: (key: string, value: T) => Promise<T>, // 设置某个key的值
  removeItem: (key: string) => Promise<boolean>, // 移除某个key的值
  clearItems: () => Promise<boolean>, // 移除所有key的值,此方法将会删除离线仓库中的所有值。谨慎使用此方法。
  itemsLength: () => Promise<number>, // 获取离线仓库中的 key 的数量(即数据仓库的“长度”)
  key: (index: number) => Promise<string | null>, // 根据 key 的索引获取其名
  keys: () => Promise<string[]>, // 获取数据仓库中所有的 key,包含所有 key 名的数组
] => {
  /**
   * @desc 获取某个key的值
   * @param {string} key 要获取的key
   * @return {Promise<T | null>} 返回一个Promise,值的类型是T或者是null
   */
  const getItem = (key: string): Promise<T | null> => {
    return localforage
      .getItem<T>(key)
      .then((value) => Promise.resolve(value))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 设置某个key的值
   * @param {string} key 要设置的key
   * @param {T} value 要设置的key的值
   * @return {Promise<T>} 返回一个Promise,值的类型是T
   */
  const setItem = (key: string, value: T): Promise<T> => {
    return localforage
      .setItem<T>(key, value)
      .then((value) => Promise.resolve(value))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 从离线仓库中删除 key 对应的值
   * @param {string} key 要删除的key
   * @return {Promise<Boolean>} 返回一个Promise,值的类型是Boolean,为了保证不阻塞代码执行,移除时报错会返回Promise<false>,各位大佬可以根据返回值来判断是否移除成功
   */
  const removeItem = (key: string): Promise<boolean> => {
    return localforage
      .removeItem(key)
      .then(() => Promise.resolve(true))
      .catch(() => Promise.resolve(false))
  }

  /**
   * @desc 从数据库中删除所有的 key,重置数据库.将会删除离线仓库中的所有值。谨慎使用此方法
   * @return {Promise<Boolean>} 返回一个Promise,值的类型是Boolean,为了保证不阻塞代码执行,移除时报错会返回Promise<false>,各位大佬可以根据返回值来判断是否移除成功
   */
  const clearItems = (): Promise<boolean> => {
    return localforage
      .clear()
      .then(() => Promise.resolve(true))
      .catch(() => Promise.resolve(false))
  }

  /**
   * @desc 获取离线仓库中的 key 的数量(即数据仓库的“长度”)
   * @return {Promise<number>} 返回一个Promise,值的类型是number
   */
  const itemsLength = (): Promise<number> => {
    return localforage
      .length()
      .then((numberOfKeys) => Promise.resolve(numberOfKeys))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 根据 key 的索引获取其名
   * @param {number} index 要删除的key
   * @return {Promise<string>} 返回一个Promise,key名,值的类型是string
   */
  const key = (index: number): Promise<string | null> => {
    return localforage
      .key(index)
      .then((keyName) => Promise.resolve(keyName))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 获取数据仓库中所有的key名数组
   * @return {Promise<string[]>} 返回一个Promise,key名数组,值的类型是string[]
   */
  const keys = (): Promise<string[]> => {
    return localforage
      .keys()
      .then((keys) => Promise.resolve(keys))
      .catch((err) => Promise.reject(err))
  }

  return [getItem, setItem, removeItem, clearItems, itemsLength, key, keys]
}

export default useLocalforage

LogicFlow 交互新体验:让锚点"活"起来,鼠标跟随动效实战!🧲

作者 橙某人
2025年12月29日 18:25

写在开头

Hey Juejin.cn community! 😀

今是2025年12月28日,距离上一次写文章,已经过去了近两个月的时间。这段时间公司业务实在繁忙,两个月十个周末里有四个都贡献给了加班,就连平日里的工作日也被紧凑的任务填满,忙得几乎脚不沾地。😵

好在一番埋头苦干后,总算能稍稍喘口气了。昨天,小编去爬了广州的南香山🌄,本以为是一座平平无奇的"小山"(低于500米海拔的山,小编基本能无压力速通,嘿嘿),想不到还有惊喜,上山的路是规整的盘山公路,沿着公路一路向上,大半个小时就登顶了;下山时,我们选了一条更野趣的原始小径,有密林、有陡坡,走起来比公路有意思多了,当然,这条路线是有前人走过的,我们跟着网友分享的轨迹,再对照着树上绑着的小红带指路,一路有惊无险地顺利下了山。💯 难受的是,我们得打车回山的另一边拿车😅,但整体来说,这次爬山的体验整体很愉快~

ad2235428be6eb20735aae76471b9532.jpgca17f1c56d240ea424322b74dadcd4b0.jpg

言归正传,最近基于 LogicFlow 开发流程图功能时,做了个自定义锚点的 "吸附" 效果:鼠标靠近节点时,锚点会自动弹出并灵动跟随鼠标移动,这个小效果挺有趣的,分享给大家参考,效果如下,请诸君按需食用哈。

122901.gif

需求背景

LogicFlow 中,锚点是静态的,固定在节点的上下左右四个位置上,这就导致了两个问题:

  1. 视觉干扰:如果一直显示锚点,画面会显得很乱。
  2. 交互困难:用户必须精确点击到锚点才能开始连线,容错率低。

其实...就是产品经理要求要炫酷一点😣,要我说静态的挺好,直观简单。

我们想要的效果是:

  • 平时隐藏锚点,保持界面整洁。
  • 鼠标移入节点区域时,显示锚点。
  • 重点来了🎯:当鼠标在节点附近移动时,锚点应该像有磁力一样,自动吸附到离鼠标最近的位置(或者跟随鼠标在一定范围内移动),让连线变得随手可得。

具体实现

要实现这个功能,我们需要深入 LogicFlow 的自定义机制

这次主要围绕到两个文件:

  • customNode.js: 自定义节点,用于集成我们写好的超级锚点。
  • customAnchor.js: 核心逻辑,实现锚点的渲染和鼠标跟随逻辑。

第1️⃣步:自定义锚点组件

首先,我们需要创建一个自定义的锚点渲染函数。这个函数会返回一个 SVG 元素(这里用 LogicFlow内置的 h 函数来创建),并且包含复杂的交互逻辑。

为什么要返回一个 SVG 元素?

这得说到 LogicFlow 的底层技术选型问题了,可以看看这篇文章:传送门

它的核心思想是:创建一个较大的透明容器(container),用来捕获鼠标事件。在这个容器内,我们放一个"小球"(ballGroup),这个小球就是我们看到的锚点。

// customAnchor.js
import { h } from "@logicflow/core";

// 定义一些常量,方便调整手感
const CONTAINER_WIDTH = 72;  // 感应区域宽度
const CONTAINER_HEIGHT = 80; // 感应区域高度
const BALL_SIZE = 20;        // 锚点小球大小

/**
 * @name 创建复杂动效锚点
 * @param {Object} params 参数对象
 * @returns {any} LogicFlow 可用的锚点渲染形状
 */
export function createCustomAnchor(params) {
  const { x, y, side, id, nodeModel, graphModel } = params || {};
  
  // 依据左右两侧计算容器左上角 (小编的业务中最多仅只有左右两个锚点)
  const halfW = CONTAINER_WIDTH / 2;
  // 如果是左侧锚点,容器应该往左偏;右侧同理(可根据自己需求调整,小编的业务是同一时间仅需展示一边的锚点即可)
  const offsetX = side === "left" ? -halfW : halfW;
  
  // 计算透明容器在画布上的绝对坐标
  const containerX = x + offsetX - CONTAINER_WIDTH / 2;
  const containerY = y - CONTAINER_HEIGHT / 2;

  // DOM 引用,用于后续直接操作 DOM 提升性能
  let containerRef = null;
  let ballGroupRef = null;

  // 核心逻辑:鼠标移动时,更新小球的位置
  function handleMouseMove(ev) {
    if (!containerRef || !ballGroupRef) return;
    
    // 获取容器相对于视口的位置
    const rect = containerRef.getBoundingClientRect();
    
    // 获取鼠标在画布上的位置(这里需要处理一下浏览器兼容性,简单起见用 clientX/Y)
    const clientX = ev.clientX;
    const clientY = ev.clientY;
    
    // 计算鼠标相对于容器左上角的偏移量
    let relX = clientX - rect.left; 
    let relY = clientY - rect.top;
    
    // 关键点:限制小球在容器内移动,防止跑出感应区
    relX = Math.max(0, Math.min(CONTAINER_WIDTH, relX));
    relY = Math.max(0, Math.min(CONTAINER_HEIGHT, relY));
    
    // 使用 setAttribute 直接更新 transform,性能最好
    ballGroupRef.setAttribute("transform", `translate(${containerX + relX}, ${containerY + relY})`);
  }
  
  // 鼠标移入:变色 + 激活动画
  function handleMouseEnter() {
    if (!ballGroupRef) return;
    ballGroupRef.style.transition = "transform 140ms ease";
    // 这里可以改变颜色,例如 ballGroupRef.style.color = 'red';
  }

  // 鼠标移出:复位
  function handleMouseLeave() {
    if (!ballGroupRef) return;
    // 鼠标离开时,平滑回到容器中心
    ballGroupRef.style.transition = "transform 160ms ease, opacity 320ms ease";
    
    // 计算中心位置
    const centerX = containerX + CONTAINER_WIDTH / 2;
    const centerY = containerY + CONTAINER_HEIGHT / 2;
    ballGroupRef.setAttribute("transform", `translate(${centerX}, ${centerY})`);
  }

  return h("g", {}, [
    // 1. 透明容器:用于扩大感应区域,这就是“吸附”的秘密
    h("rect", {
      x: containerX,
      y: containerY,
      width: CONTAINER_WIDTH,
      height: CONTAINER_HEIGHT,
      fill: "transparent", // 必须是透明但存在的
      cursor: "crosshair",
      onMouseEnter: handleMouseEnter,
      onMouseMove: handleMouseMove, // 绑定移动事件
      onMouseLeave: handleMouseLeave,
      // ... 绑定其他事件 ...
    }),
    
    // 2. 实际显示的锚点(小球)
    h("g", {
        // 初始位置居中
        transform: `translate(${containerX + CONTAINER_WIDTH / 2}, ${containerY + CONTAINER_HEIGHT / 2})`,
        "pointer-events": "none", // 让鼠标事件穿透到下方的 rect 上
        ref: (el) => { ballGroupRef = el; }
      },
      [ 
        // 这里画一个圆形和一个加号
        h("circle", { r: BALL_SIZE / 2, stroke: "currentColor", fill: "none" }),
        h("path", { d: "M-5 0 L5 0 M0 -5 L0 5", stroke: "currentColor" })
      ]
    ),
  ]);
}

这里有个小技巧⏰:我们并没有直接改变 SVG 的 cx/cy,而是通过 transform: translate(...) 来移动整个锚点组,这样性能更好,动画也更流畅。同时,pointer-events: none 确保了鼠标事件始终由底层的透明 rect 触发,避免闪烁。

第2️⃣步:在自定义节点中使用

写好了锚点逻辑,接下来要在节点中用起来,咱们需要在自定义节点类中重写 getAnchorShape 方法。

// customNode.js
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
import { createCustomAnchor } from "./customAnchor";

// 定义节点 View
class CustomNodeView extends HtmlNode {
  /**
   * @name 自定义节点锚点形状
   * @param {object} anchorData 锚点数据
   * @returns {object} 锚点形状对象
   */
  getAnchorShape(anchorData) {
    const { x, y, name, id } = anchorData;
    
    // 简单的业务逻辑:只显示左右两侧的锚点
    const side = name === "left" ? "left" : "right";
    
    // 调用我们刚才写的神器!传入必要的参数
    return createCustomAnchor({
      x,
      y,
      side,
      id,
      nodeModel: this.props.model,
      graphModel: this.props.graphModel,
    });
  }
}

// 定义节点 Model
class CustomNodeModel extends HtmlNodeModel {
  // 定义锚点位置
  getDefaultAnchor() {
    const { id, width, x, y } = this;
    return [
      { x: x - width / 2, y, name: "left", id: `${id}-L` },
      { x: x + width / 2, y, name: "right", id: `${id}-R` },
    ];
  }
}

export default {
  type: "custom-node",
  view: CustomNodeView,
  model: CustomNodeModel,
};

第3️⃣步:记录拖拽状态

在实现“手动连线”之前,我们面临一个关键问题:当我们在目标节点的锚点上松开鼠标时,我们怎么知道连线是从哪里发起的

LogicFlow 的默认行为中,customAnchor 并不知道当前的拖拽上下文。因此,我们需要借助全局状态管理(小编用的是Vue3,所以使用Pinia做的全局数据共享)和 LogicFlow 的事件系统来 "搭桥"。

1. 定义 Store

我们需要一个地方存放“当前正在拖拽的锚点信息”。

// stores/logicFlow.js
import { defineStore } from "pinia";

export const useLogicFlowStore = defineStore("logicFlow", {
  state: () => ({
    draggingInfo: null, // 存储拖拽中的连线信息
    isManualConnected: false, // 标记是否触发了手动连接
  }),
});

2. 监听 LogicFlow 事件

在 LogicFlow 初始化的地方,我们需要监听 anchor:dragstartanchor:dragend 事件,实时更新 Store。

import { useLogicFlowStore } from "@/stores/logicFlow";

export function initEvents(lf) {
  const store = useLogicFlowStore();

  // 锚点开始拖拽:记录源节点和源锚点信息
  lf.on("anchor:dragstart", (data) => {
    store.draggingInfo = data; 
    store.isManualConnected = false;
  });

  // 锚点拖拽结束:清空信息
  lf.on("anchor:dragend", () => {
    store.draggingInfo = null;
  });
}

有了这个铺垫,咱们的自定义锚点就能知道 "谁在连我" 了!😎

第4️⃣步:手动连线逻辑

你可能注意到了,锚点位置是“动”的,但 LogicFlow 的连线计算通常基于固定的锚点坐标。如果我们不做处理,可能会出现连线连不上的情况。(其实肯定是连不上的😂)

所以,我们需要在 handleMouseUp(鼠标抬起)时,手动帮 LogicFlow 建立连线。

// customAnchor.js
import { useLogicFlowStore } from "@/stores/logicFlow";

  // ... 在 createCustomAnchor 内部 ...

  function handleMouseUp() {
    const store = useLogicFlowStore();
    // 获取全局存储的拖拽信息
    const { draggingInfo } = store; 
    
    // 尝试手动建立连接
    if (draggingInfo && graphModel) {
      const sourceNode = draggingInfo.nodeModel;
      const sourceAnchor = draggingInfo.data;
      
      // 1. 基础校验:避免自连
      if (sourceAnchor.id === id) return;

      try {
        // 2. 构造边数据
        // 注意:这里我们把终点 (endPoint) 强制设为当前鼠标/锚点的视觉位置 {x, y}
        // 而不是节点原本定义的静态锚点位置
        const edgeData = {
            type: "bezier", // 贝塞尔曲线
            sourceNodeId: sourceNode.id,
            sourceAnchorId: sourceAnchor.id,
            targetNodeId: nodeModel.id,
            targetAnchorId: id,
            startPoint: { x: sourceAnchor.x, y: sourceAnchor.y },
            endPoint: { x, y }, // <--- ⏰关键!使用当前的动态坐标
        };

        // 3. 核心:手动调用 graphModel.addEdge 添加边
        graphModel.addEdge(edgeData);
      } catch (error) {
        console.error("手动连接失败", error);
      }
    }
  }

这样一来,当用户从一个节点拖拽连线到我们的动态锚点上松开鼠标时,就能精准地建立连接了!不管你的锚点 "跑" 到了哪里,连线都能准确追踪。🎯

总结

通过这次改造,咱们的流程图编辑体验得到了"质"的飞跃体验:

  1. 灵动:锚点不再是死板的钉子,而是会互动的精灵。👻
  2. 高效:增大了鼠标感应区域,用户连线更轻松,无需像素级瞄准。
  3. 美观:平时隐藏,用时显现,保持了画布的整洁。

希望这个方案能给正在使用 LogicFlow 的小伙伴们一些灵感吧!💡





至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

2025,我不再写代码,我在当代码的“审核员”

作者 黑土豆
2025年12月29日 18:00

一、 惊喜:那个瞬间,我以为自己失业了

2025年的开发节奏快得让人恍惚。

那天下午,需求文档里躺着一个让我头疼的任务:实现一个极致丝滑的流式打字机效果。要求不仅是文字蹦出来,还要带弹性动效、自动处理Markdown里的表格重绘,并且在Vue 3的响应式框架下不能掉帧。

我坐在工位上,习惯性地在IDE里敲下一行注释:// Vue 3, Composition API, 流式输出实现打字机效果

3秒钟。

屏幕像瀑布一样刷屏,几百行代码精准地跳了出来。它用了shallowRef来规避深层响应式的开销,用了 requestAnimationFrame 来平滑字符插入的频率,甚至连SSE(Server-Sent Events)截断中文字符的 Buffer处理都写得滴水不漏。

那是我那一周里的第一个时刻:惊讶。 我按下Tab键,代码跑通了,效果比我预想的还要好。那一刻,我内心是开心的,开心是这个需求又能提前实现;但是开心之余又感到一种前所未有的“失重感”。我曾引以为傲的技术壁垒,在3秒钟内被推平了。

二、 迟疑:这完美背后的“非人感”

我盯着那段代码。它写得太“干净”了,干净得不像人类的手笔。它没有我常用的命名习惯,没有我为了图省事偶尔会留下的TODO。它是一个逻辑上的闭环,但我却觉得它很陌生。

我开始测试这个打字机。字符跳跃的节奏感极强,每一个像素的位移都符合物理公式。但这种完美让我感到不安。我开始怀疑:如果我不再需要思考“如何实现”,那么我存在的价值是什么?

这种迟疑在我修改一个微小的视觉间距时达到了顶峰。我本想微调一下光标的闪烁频率,但我发现我竟然需要花十分钟去阅读AI为了实现那个“极致效果”而设计的复杂定时器逻辑。我突然意识到,我正在失去对代码的“手感”。我从一个亲手打磨零件的木匠,变成了一个操作全自动机床的工人。机床效率极高,但我却不敢轻易关掉它。

三、 认可:它确实解决了那些“该死的细节”

这种迟疑在遇到一个真正的技术硬骨头时,被硬生生地扭转成了“认可”。

打字机效果最怕的是Markdown里的表格。当流式数据只传了一半,表格的HTML结构是破碎的,页面会因为DOM结构的频繁解析而疯狂抖动。我本来准备好手动写一个状态机去拦截不完整的标签,结果我发现AI生成的那段Vue逻辑里,早已预埋了一个增量DOM更新算法

它利用Vue的模板系统,配合一个微型的“虚拟缓冲区”,优雅地解决了这个问题。那一刻,我看着屏幕上那个即便在处理复杂表格也丝滑如绸缎的打字机,内心的防线崩溃了。

我不得不承认:它写得确实比我好。

它不仅懂Vue的生命周期,它还懂那些我懒得去查的、关于浏览器重绘与回流的最底层规范。这种认可带有一种劫后余生的庆幸,也带着一种职业上的妥协——我开始接受AI作为一个比我更强大的“合作者”存在。

四、 反思:效率巅峰处的虚无

当那个完美的打字机效果上线,并获得老板和用户的一致好评时,我却陷入了最深的思辨。

我的效率确实提速了500%,但我感觉自己变虚了。以前解决一个Bug,我会兴奋地在技术群里复盘;现在,我只是淡淡地对AI说一句:“这里性能有问题,优化一下”,然后再次按下Tab

我们真的在进化吗?

在2025年,前端工程师的定义正在发生不可逆的偏移。我们正在从“代码生产者”转型为“代码审核员”。我们不再关心如何实现一个打字机,我们关心的是如何定义这个打字机的“意图”。

但这种转型背后的代价是:我们正在失去“犯错”的权利,而犯错恰恰是人类习得直觉的唯一途径。 当一切都被预设得如此完美,我们是否还会去深究一个SSE连接为什么处理丢包?我们是否还会去在乎一个Vue组件卸载时为什么要手动清除定时器?

这篇文章写到最后,我想说的不再是打字机,而是关于我们在这个时代的定位。AI给了我们通往终点的捷径,但它也顺手抹掉了沿途的风景。2025年,我的工作不在于我能写出多么华丽的代码,而在于我还能在这个行业待多久?

Electron 瘦身记:我是如何把安装后 900MB 的"巨无霸"砍到 466MB 的?

作者 mCell
2025年12月29日 17:46

同步至个人站点:Electron 瘦身记:我是如何把安装后 900MB 的"巨无霸"砍到 466MB 的?

089.webp

最近在参与一个 Electron 桌面端项目开发。作为 Electron 萌新,我原本以为“桌面端 = 写写前端 + 套个壳”,结果真正折磨人的,反而是工程化那一坨:打包构建、签名、公证、发布更新、更新测试……坑点密度高到离谱。

先吐槽两句:

  • macOS 打包要走签名 + 公证(Notarization)流程,第一次搞的时候你会怀疑自己是不是在给苹果写论文。
  • 自动更新测试更难绷:为了测 Windows 的更新链路,你甚至得在 macOS 上开个 Windows 虚拟机……(是的,我真的干过)

但让我印象最深刻、也最有成就感的,是一次很“实在”的工作:构建产物体积优化

因为它直接影响三件事:

  1. 用户下载/安装体验(你不希望用户下载一个“3A 大作”)
  2. 启动速度(图标跳半天才开,真的很败好感)
  3. 发布流程成本(包越大,签名/公证/上传/分发越折磨)

这篇就记录一下:我怎么从 DMG 240MB、安装后 900MB+,做到 DMG 155MB、安装后 466MB

先看结果:体积变化

最初版本:

  • macOS dmg:240MB
  • 安装后:900MB+
  • 体验:启动慢,菜单栏图标疯狂弹跳,弹到我怀疑人生

优化后:

  • macOS-arm.dmg:155MB
  • 安装后:466MB
  • 体验:启动速度肉眼可见地正常了

第一性原则:别猜,先把包拆开看

我一开始犯的错就是“凭感觉优化”。后面发现这事必须回到最朴素的方法:

把构建产物展开/解压,看看到底是谁在占空间。

在 macOS 上你可以:

  • dmg 挂载后找到 .app
  • 右键 → 显示包内容 → Contents/Resources/
  • 常见结构里会有 app.asar / app.asar.unpacked

然后用最土但最有效的方式查体积:

du -sh "YourApp.app"
du -sh "YourApp.app/Contents/Resources"/*

当我第一次看到结果时,基本就破案了:

  • .map 有,但不是主犯
  • 真正离谱的是:node_modules(体积大得像是把整个开发环境一起打进去了)

第一刀:发布版本别带 SourceMap(别把源码线索塞给用户)

这是我第一次做 Electron 发布,发布第一个内部测试版时我居然没关 sourcemap。

同事一句话把我点醒:“你这包里怎么能看到源码痕迹?”

我去翻构建产物:好家伙,.map 真在里面。

虽然 sourcemap 通常不会占几百 MB,但它有两个问题:

  • 安全性 / 泄露风险
  • 它属于“你不该带”的东西(该清理就清理)

于是我先在构建侧关掉 sourcemap,并在打包规则里也顺手排除 .map(双保险)。

第二刀:依赖治理——“npm install xxx 一把梭”的历史债,要还

接下来就是 node_modules 瘦身。

我以前装依赖的习惯非常粗暴:npm install xxx,能跑就行;根本不在乎它应该在 dependencies 还是 devDependencies

这在 Web 项目里可能不致命,但在 Electron 打包里非常致命:你分错了依赖,构建产物就会帮你把一堆开发工具、类型、脚手架、lint、测试相关,全塞进最终 App。

我当时做了两件事:

  1. 清理 package.json 里压根没用的废弃包 (这种是纯收益,删就完事了)
  2. 重新整理 dependencies / devDependencies 我甚至请 Claude Code 帮我分了一遍(因为我当时真的没经验,靠自己很容易漏)

这一轮做完再构建:

  • 安装体积从 900MB → 600MB+

我当时很开心,但冷静想想:600MB 的桌面软件还是大得离谱。

所以继续拆包。

第三刀:node_modules 里全是“你根本不需要”的文件

当 node_modules 变小之后,我继续往里看,结果又发现一堆“脂肪”:

  • README.md / CHANGELOG.md / HISTORY.md / LICENSE
  • tests / __tests__ / examples / docs / coverage
  • .d.ts
  • .map
  • 甚至还有 *.ts / *.tsx 源码、配置文件(rollup/webpack/tsconfig)

这些对用户运行 App 来说几乎没价值。

形象点讲:这就像你买披萨,厨师把面粉袋子、烤箱说明书、甚至工作笔记也一起塞给你。

于是我遇到了关键问题:

怎么系统性剔除这些文件? 难道要打包前去 node_modules 手动删?(那也太原始了)

关键方案:electron-builder 的 files 规则,精准排除

我最终落在 electron-builderbuild.files 配置上:用 glob 模式明确告诉打包工具“哪些要、哪些不要”。

核心思路很简单:

  • 只打包你的 dist / dist-electron
  • 排除 .map
  • 排除所有 *.md / *.ts / *.tsx 等“开发者文件”
  • node_modules 里重点清理:文档、测试、类型、锁文件、工具脚本、隐藏文件等

这是我最终的配置:

"files": [
  "dist/**/*",
  "dist-electron/**/*",
  "!dist/**/*.map",
  "!dist-electron/**/*.map",

  "!**/*.{ts,tsx,md}",

  "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,HISTORY.md,CONTRIBUTING.md}",
  "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples,coverage,docs,doc}",
  "!**/node_modules/*.d.ts",
  "!**/node_modules/.bin",
  "!**/node_modules/*/*.md",
  "!**/node_modules/*/*.markdown",
  "!**/node_modules/*/LICENSE*",
  "!**/node_modules/*/{.github,.vscode,.idea}",
  "!**/node_modules/*/{rollup.config.js,webpack.config.js,tsconfig.json}",
  "!**/node_modules/*/.*",
  "!**/node_modules/**/*.map",
  "!**/node_modules/**/*.{spec,test}.{js,jsx,ts,tsx}",
  "!**/node_modules/*/{yarn.lock,package-lock.json,pnpm-lock.yaml}",

  "!**/node_modules/lucide-react/dist/*.{ts,tsx}",
  "!**/node_modules/@types"
]

配置完,我点击构建按钮。电脑开始发烫、风扇狂转,我开始祈祷不要出“运行时报错找不到某个文件”的地狱场景。

结果:

  • macos-arm.dmg: 155MB(原 240MB)
  • 安装后:466MB(原 900MB+)

体积几乎减半,而且功能没缺、启动也明显变快。

这类“删文件式瘦身”有风险吗?

有,但可控。

因为确实存在少数包会在运行时读取某些资源文件(甚至是你以为“文档/配置”的东西)。所以我的策略是:

  1. 先排除最通用的一批:md/tests/docs/types/map/lockfile
  2. 打完包之后做一轮完整回归(重点:启动、关键路径、更新链路)
  3. 如果某个依赖真的需要某类文件,再对它做“例外放行”(白名单)

这比“手动删 node_modules”靠谱太多了:可重复、可追踪、可回滚。

工程化不只是“能跑”,而是“交付可控”

回头看,我从“能打出来就行”的心态,变成了:

  • 我知道最终产物里有什么
  • 我知道哪些文件不该进去
  • 我知道体积怎么定位、怎么收敛、怎么验证

如果你也在做 Electron,建议你按顺序做这几件事(基本不会亏):

  1. 拆包看体积分布(别凭感觉)
  2. 关 sourcemap(安全 + 清爽)
  3. 清理无用依赖、分好 dev/prod(最稳最值)
  4. 用 files 精准剔除 node_modules 垃圾文件(体积大头就在这)
  5. 每次瘦身都配回归测试(尤其是更新链路)

附:我常用的排查命令

# 看体积分布
du -sh dist dist-electron node_modules
du -sh "YourApp.app/Contents/Resources"/*

# 查大体积 sourcemap
find . -name "*.map" -size +5M -print

(完)

❌
❌