普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月20日首页

预售 30.88 万元起,全新雷克萨斯 ES 登场,加长 165 mm,设计大变样

作者 芥末
2026年3月20日 18:30

过去几年,雷克萨斯在中国市场的表现,称得上是逆势突围。

2025 年,雷克萨斯终端销量达到 18.38 万辆,连续 3 年保持正增长,在进口豪华品牌中相当少见。

其中,ES 依旧是绝对主力,销量从 2024 年的 10.8 万辆增长至 11.86 万辆,占到品牌总销量的 65%。

但另一边,中国豪华车市场的竞争逻辑也在不断变化。新能源车型正在快速改写市场格局,消费者对智能化、电气化的期待,也早已不是几年前的水平。

在这样的背景下,第八代 ES 算是迈出了雷克萨斯在本土化和电动化上的关键一步。

3 月 20 日,雷克萨斯正式宣布全新一代 ES 开启预售,提供混合动力和纯电动两种动力版本,正式取消纯燃油车型。

其中,混动版 ES 300h 的预售价为 30.88 万元,并提供两款专属定制选装包,分别是售价 1.2 万元的「舒享副驾套装」和售价 3 万元的「多感纯享套装」。

新一代 ES 300h 最大的变化,首先来自外观。

新车改用了无边界一体化纺锤形车身设计。前脸线条从引擎盖自然延伸至保险杠四角,拉出更低趴的视觉姿态。

侧面则采用溜背式设计,雕刻感更强的贯穿式腰线与蚌式双翼后备箱盖衔接自然,整体线条兼顾了视觉张力与空气动力学表现。

细节上,新车配备全新的 Twin-L 造型日行灯,轮廓锐利,层次分明,车尾采用立体贯穿式菱纹尾灯,与前脸形成呼应,车身两侧配备半隐藏式门把手,并保留了机械结构。

新车共提供 7 种外观颜色和 4 款内饰配色,其中包括专属苍蓝色车漆,以及面向中国用户打造的「青竹」主题内饰。

全新一代 ES 的长宽高分别为 5140 × 1920 × 1555 毫米,轴距达到 2950 毫米。相比上一代,车长增加 165 毫米,轴距加长 80 毫米。

总体来讲,新车明显摆脱了上一代偏优雅、舒展的豪华取向,整体风格转向更激进、更强调运动感的表达。前脸、侧面到尾部,都在努力营造一种更先锋、更有冲击力的视觉效果。

但这种变化并不完全是加分项。

上一代 ES 那种克制、舒展的优雅感,在这一代身上被明显削弱了,取而代之的是更厚重、更复杂的造型语言。尤其是车身比例,因为需要为电气化系统和电池布局让位,整车姿态显得不够轻盈,甚至有些臃肿。

设计团队显然也意识到了这一点,因此用了大量锋利线条、起伏曲面和更强的型面雕刻,试图在视觉上把车身拉得更低、更修长一些。

效果不是没有,但也让整套设计多了几分堆叠感,少了过去那种从容和高级。

尺寸的增长,最直接的变化就是车内空间。后排腿部空间相比上一代明显提升。后备箱采用全平设计,内部规整宽敞,可容纳 4 个高尔夫球包或 2 个 86 L 大号行李箱。更大的开口也让装载和取放都更方便。中控台下部还预留了开放式储物空间,日常使用更顺手。

走进车内,新一代 ES 采用「时光即奢华」的设计理念,整体以淡雅绿色为主色调。

中控区域配备了两块 14 英寸高清触控屏,支持双屏联动,并内置了防窥功能。中央扶手区域换用了电子排挡杆。驾驶席配备 Slope-HUD 全彩抬头显示器,行驶信息的呈现更直接,驾驶者不用频繁低头查看。

在材质和氛围营造上,这一代 ES 也明显更下功夫。

新车车门饰板采用静山水革纹工艺,还可选装雷克萨斯首创的竹层透光饰板,通过漫反射营造柔和面光。车内首次引入了竹韵香氛系统,提供 5 款含竹香的定制香氛,香氛盒则采用锻竹可持续工艺。

舒适性配置依旧是 ES 的强项。

全新一代 ES 前排座椅标配加热和通风功能,并可与方向盘加热联动。高配车型提供副驾 Ottoman 舒享座椅,集成小腿支撑、多向腰部支撑、座椅记忆、副驾柔光化妆镜和专属娱乐屏等配置。

其他舒适性配置还包括静感自吸门,以及可实时监测 PM2.5 浓度的生态新风系统。该系统可与 nanoe™ X 纳米水离子发生器和 CN95 空气滤芯协同工作,持续净化座舱空气。

智能化方面,新车搭载专为中国市场开发的新一代 LEXUS Interface 系统。智能语音助手支持「可见即可说」、连续自然对话和免唤醒操作,人机交互不再依赖固定指令。

值得一提的是,新车还搭载了全球首创的响应式隐藏按键。物理按键被巧妙隐藏在内饰之中,手部靠近时才会点亮唤醒,既保留了实体操作的质感,也尽量维持了内饰的整体简洁。

动力系统上,全新一代 ES 提供 3 种版本。

ES 300h 搭载第 5 代 THS 智能电混动系统,由 2.0 L 自然吸气发动机与高功率电机组成。其中,发动机最大功率 112 千瓦,电机最大功率 83 千瓦,系统综合功率 145 千瓦,匹配 E-CVT 无级变速箱,官方公布的 WLTC 综合油耗为 4.39 L / 100 km。

ES 350e 和 ES 500e 则基于全新开发的 GA-K 平台打造,配备大容量电池,其中 ES 500e 还搭载 DIRECT4 智能全轮驱动系统。不过,这两款纯电车型的续航和充电参数,目前还没有正式公布。

新车底盘同样基于深度优化后的多路径技术平台打造,车头、底盘和车尾结构刚性都得到了进一步强化。后悬架升级为五连杆独立结构,并搭配摆动阀式阻尼减振器,在滤振表现和操控响应之间提供更细致的平衡。转向系统也支持随速调节,可根据车速自动调整助力比例。

整体来看,全新一代 ES 交出的是一份诚意与局限并存的答卷。

它拥有了更大胆的内外设计,空间和舒适性依旧很有说服力,混动系统成熟稳定,底盘调校也还是雷克萨斯最拿得出手的部分。

但问题也同样存在,除了纯电版本的细节迟迟未公布外,在 30 万元左右的价格区间,新车甚至没有最基础的自动泊车功能,这一点很难不被拿来吐槽。

真正决定这一代 ES 后续表现的,除了混动版的市场接受度,恐怕还要看看纯电版本能拿出多大诚意。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

Ant Design Vue 表格组件空数据统一处理 踩坑

作者 28256_
2026年3月20日 18:06

transformCellText

提供 transformCellText 这个表格属性来做数据的处理

transformCellText 数据渲染前可以再次改变,一般用于空数据的默认配置 Function({ text, column, record, index }) => any,此处的 text 是经过其它定义单元格 api 处理后的数据,有可能是 VNode/string/number 类型

数据处理时,都是用text这个属性

划重点

text会有两种情况,这个才是坑的地方

  • 非数组(直接就是要展示的数据)
  • 是个数组(要展示的数据被数组包裹了一层)

text非数组情况


<a-table :dataSource="dataSource" :columns="columns" />

直接简单使用,不使用table组件的插槽,这个时候返回的就是要展示的数据

image.png 可以从图上看出,打印的text的结果

text是个数组


<template>
  <a-table :dataSource="dataSource" :columns="columns" :transformCellText="ssss">
    <template #bodyCell="{ column, record }">
      <template v-if="column.key === 'avatar'">
        <a-avatar :src="record.avatar" :style="{ backgroundColor: '#1890ff' }">
          {{ record.name?.charAt(0) }}
        </a-avatar>
      </template>
    </template>
  </a-table>
</template>

使用了table组件的bodyCell插槽,这个时候要展示的数据被数组包裹了一层

image.png 可以从图上看出,打印的text被数组包裹了一层

实践方案

既然text会有两种情况,就可以从两种情况下手,完成我们的需求

// 当返回的类型是VNode时,不用特殊处理,因为VNode是自定义的dom 直接渲染
const handleTransform = ({ text }) => {

  const isEmpty = val => val === null || val === undefined || val === ''

  const target = Array.isArray(text) ? (text.length > 0 ? text[0] : undefined) : text

  return isEmpty(target) ? '--' : text
}

Cesium 海量点位不卡顿!图标动态聚合效果深度解析,看完直接抄代码!

作者 李剑一
2026年3月19日 09:45

接上文# 告别冗余代码!Cesium点位图标模糊、重叠?自适应参数调优攻略,一次封装终身复用!,在地图上创建图标是基础操作,但是当地图上的图标过多的时候展示效果其实并不好。

毕竟谁也不想看到密密麻麻的图标,所以部分距离相近的图标应该聚合在一起,形成一个聚合图标展示出来。

image.png

在Cesium开发中,图标聚合能够解决海量图标重叠、界面杂乱、性能卡顿等问题。

尤其在智慧安防、智慧园区、设备监控等场景,几十个甚至上百个摄像头/设备图标挤在一块,不仅看不清,还会严重影响地图流畅度。

解决方案

通过监听相机高度,高度超过阈值,自动开启聚合。

根据计算屏幕像素距离,把三维坐标转成屏幕坐标,算两点多远,距离小于设定值,归为一组。

image.png

这时候隐藏原始图标,只显示聚合图标。

生成聚合点:显示图标+数量,拉近后自动散开。

实现代码

计算屏幕距离 + 判断是否在屏幕内。是聚合的核心基础:把三维坐标转屏幕坐标,再算距离。

/**
 * 计算两点在屏幕上的像素距离
 */
const calculateScreenDistance = (pos1, pos2) => {
    if (!viewer.value || !viewer.value.scene) return Infinity
    
    const scene = viewer.value.scene
    try {
        // 世界坐标 → 屏幕坐标
        const screenPos1 = Cesium.SceneTransforms.worldToWindowCoordinates(scene, pos1)
        const screenPos2 = Cesium.SceneTransforms.worldToWindowCoordinates(scene, pos2)
        
        if (!screenPos1 || !screenPos2) return Infinity
        
        // 勾股定理算像素距离
        const dx = screenPos1.x - screenPos2.x
        const dy = screenPos1.y - screenPos2.y
        return Math.sqrt(dx * dx + dy * dy)
    } catch (error) {
        return Infinity
    }
}

/**
 * 检查点是否在屏幕上可见
 */
const isPositionOnScreen = (position) => {
    if (!viewer.value || !viewer.value.scene) return false
    try {
        const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(viewer.value.scene, position)
        return screenPos != null
    } catch (error) {
        return false
    }
}

生成聚合点,图标更大、创建label显示当前标签数量更明显。

/**
 * 创建聚合图标
 */
const createClusterIcon = (clusterData) => {
    if (!viewer.value) return null
    const { icons, type, center } = clusterData
    const count = icons.length

    // 坐标转换
    const cartographic = Cesium.Cartographic.fromCartesian(center)
    const longitude = Cesium.Math.toDegrees(cartographic.longitude)
    const latitude = Cesium.Math.toDegrees(cartographic.latitude)

    // 创建聚合实体
    const clusterId = `cluster_${type}_${Date.now()}`
    const entity = viewer.value.entities.add({
        id: clusterId,
        position: center,
        billboard: {
            image: getClusterIconUrl(type),
            scale: 1.2,
            width: 40,
            height: 40,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            disableDepthTestDistance: Number.POSITIVE_INFINITY
        }
    })

    // 聚合数量标签
    const typeName = getTypeDisplayName(type)
    entity.label = {
        text: `${typeName} ${count}个`,
        font: '14px sans-serif',
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 2,
        pixelOffset: new Cesium.Cartesian2(0, -50),
        showBackground: true,
        disableDepthTestDistance: Number.POSITIVE_INFINITY
    }

    // 存入聚合列表
    clusterEntities.set(clusterId, { entity, icons, type, center })
    return entity
}

动态计算聚合阈值,通过遍历图标 → 分组 → 合并/显示,自动隐藏原始图标,显示聚合点。

/**
 * 更新图标聚合状态
 */
const updateClustering = () => {
    if (!viewer.value || iconEntities.size === 0) return
    clearClusters()

    // 关闭聚合 = 显示全部
    if (!isClusteringEnabled.value) {
        showAllIcons()
        return
    }

    // 动态阈值:相机越高,聚合越明显
    const cameraHeight = viewer.value.camera.positionCartographic.height
    const dynamicClusterDistance = Math.min(
        MAX_SCREEN_CLUSTER_DISTANCE,
        SCREEN_CLUSTER_DISTANCE + (cameraHeight - CLUSTER_THRESHOLD) / 50
    )

    // 收集所有图标
    const allIcons = []
    iconEntities.forEach((iconData, id) => {
        const position = iconData.entity.position.getValue(Cesium.JulianDate.now())
        allIcons.push({ id, entity: iconData.entity, position, type: iconData.type })
    })

    // 先隐藏所有图标
    allIcons.forEach(icon => icon.entity.show = false)

    // 聚类算法
    const clusters = []
    const visited = new Set()

    for (let i = 0; i < allIcons.length; i++) {
        if (visited.has(i)) continue
        const current = allIcons[i]
        if (!isPositionOnScreen(current.position)) continue

        const cluster = [current]
        visited.add(i)

        // 寻找附近图标
        for (let j = i + 1; j < allIcons.length; j++) {
            if (visited.has(j)) continue
            const other = allIcons[j]
            if (!isPositionOnScreen(other.position)) continue

            const dist = calculateScreenDistance(current.position, other.position)
            if (dist <= dynamicClusterDistance) {
                cluster.push(other)
                visited.add(j)
            }
        }
        clusters.push(cluster)
    }

    // 生成聚合点 / 显示单个图标
    clusters.forEach(cluster => {
        if (cluster.length === 1) {
            cluster[0].entity.show = true
        } else {
            // 计算中心点
            let centerX = 0, centerY = 0, centerZ = 0
            cluster.forEach(icon => {
                centerX += icon.position.x
                centerY += icon.position.y
                centerZ += icon.position.z
            })
            const center = new Cesium.Cartesian3(
                centerX / cluster.length,
                centerY / cluster.length,
                centerZ / cluster.length
            )

            createClusterIcon({
                icons: cluster.map(c => c.id),
                type: 'camera',
                center
            })
        }
    })
}

总结

Cesium 图标聚合原理上很简单:

算距离 → 分组 → 隐藏/显示 → 生成聚合点

在园区级别的模型上其实启不启用影响不大,但是在城市级别,或者是多地区复杂情况的模型上还是有必要的。

能够极大的提升加载的流畅度,减少操作的卡顿。

nestjs学习 - 拦截器(intercept)

作者 web_bee
2026年3月20日 11:04

拦截器是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。

img

一、它是什么

拦截器(Interceptor) 是一个基于 面向切面编程(AOP) 思想的强大功能。它允许你在请求到达控制器(Controller)之前或之后,以及响应返回给客户端之前,插入自定义逻辑。

白话:

从上图可以看出,拦截器就是可以在 到达请求前请求返回结果后 进行拦截,做一些你想做的事情;

前端开发同学可以结合 axios 的拦截器理解,几乎就是同一个模式;

简单说:拦截器就是请求和响应路上的“把关人”,能在不修改核心业务代码的情况下,统一处理一些公共逻辑。

在下文中主要关注它的使用场景;

在框架生命周期中,它的执行时机是:

请求进入 → 中间件 → 守卫 → 拦截器 → 管道 → 控制器 → 服务 → 拦截器 → 异常过滤器 → 服务器响应

二、使用方法

在使用拦截器之前,需了解 RxJS(响应式编程库) 的使用

它底层严重依赖 RxJS,因为 intercept() 方法返回的是一个 Observable(可观察对象)。这意味着你需要对 RxJS 的操作符(如 map, tap, catchError 等)有一定了解。

1. 创建

统一响应数据格式demo:

import { NestInterceptor, CallHandler, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';


interface Data<T> {
  code: number;
  message: string;
  data: T;
}

/**
 * 响应拦截器
 * 用于处理响应数据
 * 可以用于处理响应数据,如添加响应头,添加响应体等
 */
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Data<T>> {
    // ==========================
    // 【阶段 1:控制器执行之前】
    // ==========================
    // 这里的代码会立即同步执行。
    // 此时请求刚到达拦截器,还没进控制器。
    console.log('❤️ [之前] 请求已到达拦截器');
    const startTime = Date.now();
    
    // 可以在这里做:权限预检、记录开始时间、修改请求参数等。
    // 如果在这里直接 return 一个 Observable (例如 return of({error: 'blocked'})) 
    // 而不调用 next.handle(),控制器将永远不会执行(短路)。

    // 调用 next.handle() 启动控制器逻辑
    // 它返回一个 Observable,代表控制器未来的执行结果(流)
    const response$ = next.handle(); 
    
    // ==========================
    // 【阶段 2:控制器执行之后】
    // ==========================
    // 这里的代码不会立即执行!
    // 它们被注册为 RxJS 的“操作符”,只有当控制器执行完毕并产生数据时,流才会流动到这里。
    return response$.pipe(
      map(data => {
        return {
          code: 200,
          message: 'success',
          data,
        };
      }),
    );
  }
}

2. 注册

有三种注册方式,作用范围依次扩大:

方法级别:

仅针对某个特定路由

@Get('users')
@UseInterceptors(LoggingInterceptor)
findAll() {
  return this.userService.findAll();
}

控制器级别:

针对该控制器下的所有路由

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  // ...
}

全局级别

针对整个应用的所有路由,在 main.ts 中注册:

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);

三、使用场景:

  1. 统一响应格式格式化(正常数据、错误数据)

  2. 响应缓存

    对于不经常变动的数据(如配置信息、列表页),可以在拦截器中检查缓存。

    • 如果缓存命中,直接 return of(cachedData)不调用 next.handle(),从而跳过控制器逻辑,极大提升性能。
    • 如果未命中,正常执行并写入缓存。
  3. 超时处理

    如果某个请求处理时间过长,可以强制中断。

    import { timeout } from 'rxjs/operators';
    
    // 在 intercept 方法中
    return next.handle().pipe(
      timeout(5000), // 5秒无响应则抛出异常
    );
    
  4. 数据序列化/脱敏

    在返回给用户之前,动态修改敏感字段。

    • 例如:将用户列表中的 password 字段移除,或将手机号中间四位替换为 ****
    • 通过 map 操作符遍历返回数据并进行清洗。

四、总结:

  • 基于 AOP 思想,利用 RxJS 在请求/响应生命周期中插入逻辑的机制。
  • 它本质上是一个强大的“切面”工具,用于处理那些横跨整个应用程序的、与核心业务逻辑无关的公共关注点。

    它的精髓在于:你可以在不侵入、不修改任何一个现有控制器方法的情况下,为整个应用或特定接口批量添加上述各种功能。 这使得你的代码更加干净、可维护,并且这些横切关注点可以被轻松地复用和组合。

  • 统一返回格式、日志记录、性能监控、缓存、数据转换、超时控制。
  • 区别: 比中间件更灵活,能操作返回值;比守卫(Guard)更侧重于数据转换而非权限决策。

LeetCode 918. 环形子数组的最大和:两种解法详解

作者 Wect
2026年3月20日 08:50

刷题路上遇到环形数组的问题,总容易被“环形”这个条件绕晕——子数组不仅能是常规的连续片段,还能跨数组首尾连接。今天就来拆解 LeetCode 918 题「环形子数组的最大和」,分享两种高效解法,从原理到代码一步步讲透,帮你彻底搞懂这类环形数组问题。

先看题目核心:给定一个长度为 n 的环形整数数组 nums,返回非空子数组的最大可能和。这里要注意两个关键约束:一是环形意味着数组首尾相连,二是子数组不能重复使用元素(也就是说,跨首尾的子数组比如 nums[n-1], nums[0], nums[1] 是允许的,但不能包含 nums[0] 两次)。

题目核心难点

常规的子数组最大和(比如 LeetCode 53 题),用 Kadane 算法就能轻松解决,但环形数组多了“跨首尾”的情况,这就需要我们跳出常规思维:

  • 常规子数组:从 i 到 j(i ≤ j),连续且不跨首尾;

  • 环形子数组:从 j 到 n-1,再从 0 到 i(j > i),本质是“数组总和 - 中间一段最小子数组的和”。

基于这个思路,衍生出两种经典解法,下面分别详细讲解。

解法一:全局最大值 = max(常规最大和, 总和 - 常规最小和)

核心原理

这是最直观、最易理解的解法,核心逻辑分两种情况:

  1. 最大子数组不跨首尾:就是常规的子数组最大和,用 Kadane 算法直接求解;

  2. 最大子数组跨首尾:此时最大和 = 数组总和 - 最小子数组的和(因为总和减去中间一段最小的子数组,剩下的就是跨首尾的最大子数组)。

还有一个特殊情况:如果数组中所有元素都是负数,那么“总和 - 最小子数组和”会得到 0(因为总和 = 最小子数组和),但题目要求子数组非空,所以此时直接返回常规最大和(即数组中最大的那个负数)。

代码解析(TypeScript)

function maxSubarraySumCircular_1(nums: number[]): number {
  if (nums.length === 0) return 0;
  let curMax = nums[0], maxSum = nums[0]; // 常规最大和相关
  let curMin = nums[0], minSum = nums[0]; // 常规最小和相关
  let totalSum = nums[0]; // 数组总和

  for (let i = 1; i < nums.length; i++) {
    // 常规Kadane算法求最大子数组和
    curMax = Math.max(nums[i], curMax + nums[i]);
    maxSum = Math.max(maxSum, curMax);

    // 同理,求最小子数组和(Kadane算法变种)
    curMin = Math.min(nums[i], curMin + nums[i]);
    minSum = Math.min(minSum, curMin);

    // 累加计算数组总和
    totalSum += nums[i];
  }

  // 特殊情况:所有元素都是负数,直接返回最大和(非空)
  if (maxSum < 0) {
    return maxSum;
  }

  // 两种情况取最大值:常规最大和 vs 总和 - 最小子数组和
  return Math.max(maxSum, totalSum - minSum);
};

关键细节

  • curMax 和 curMin 分别记录“以当前元素结尾的最大子数组和”和“以当前元素结尾的最小子数组和”,每次迭代更新;

  • totalSum 必须在迭代中累加,避免二次遍历数组,保证时间复杂度 O(n);

  • 判断 maxSum < 0 是核心容错,避免所有元素为负时返回 0(不符合非空子数组要求)。

解法二:前缀和 + 后缀枚举(避免总和为负的判断)

核心原理

这种解法的思路是“拆分环形子数组”:跨首尾的子数组可以拆分为「前缀子数组」(从 0 开始)和「后缀子数组」(到 n-1 结束)。我们可以:

  1. 先计算常规的最大子数组和(不跨首尾);

  2. 再计算“后缀子数组 + 前缀子数组”的最大和:用 leftMax 数组记录「从 0 到 i 的最大前缀和」,再从右到左枚举后缀子数组,每次将后缀和与 leftMax[i-1](前 i-1 个元素的最大前缀和)相加,取最大值。

这种方法不需要判断数组是否全为负,因为枚举的后缀和 + 前缀和都是非空的,且常规最大和已经覆盖了全负的情况。

代码解析(TypeScript)

function maxSubarraySumCircular_2(nums: number[]): number {
  let n: number = nums.length;
  // leftMax[i]:从0开始,到i为止的最大前缀和(必须包含0,保证前缀非空)
  const leftMax = new Array(n).fill(0);
  leftMax[0] = nums[0]; // 初始值:只有第一个元素的前缀和
  let leftSum: number = nums[0]; // 累加前缀和
  let pre: number = nums[0]; // 常规最大子数组和的中间变量(Kadane)
  let res: number = nums[0]; // 最终结果,初始化为第一个元素

  // 第一次遍历:计算常规最大和 + leftMax数组
  for (let i = 1; i < n; i++) {
    // 常规Kadane算法求最大子数组和
    pre = Math.max(pre + nums[i], nums[i]);
    res = Math.max(res, pre);

    // 累加前缀和,更新leftMax(保证leftMax[i]是0到i的最大前缀和)
    leftSum += nums[i];
    leftMax[i] = Math.max(leftMax[i - 1], leftSum);
  }

  // 第二次遍历:从右到左枚举后缀子数组,计算后缀和 + 对应最大前缀和
  let rightSum = 0;
  for (let i = n - 1; i > 0; i--) {
    rightSum += nums[i]; // 后缀和:从i到n-1的和
    // 后缀和(i到n-1) + 前缀和(0到i-1的最大),更新结果
    res = Math.max(res, rightSum + leftMax[i - 1]);
  }

  return res;
};

关键细节

  • leftMax 数组的核心作用:记录“以 0 为起点,到 i 为止”的最大前缀和,确保后续枚举后缀时,能快速找到对应的最大前缀;

  • 第二次遍历从 n-1 到 1(不包含 0),因为当 i=0 时,leftMax[i-1] 越界,且此时后缀和就是整个数组,已经被常规最大和覆盖;

  • 时间复杂度依然是 O(n),空间复杂度 O(n)(leftMax 数组),相比解法一多了一点空间,但避免了总和为负的判断,逻辑更简洁。

两种解法对比

解法 时间复杂度 空间复杂度 核心优势 适用场景
解法一(总和 - 最小和) O(n) O(1) 空间最优,逻辑直观 追求空间效率,能记住“全负判断”的场景
解法二(前缀+后缀) O(n) O(n) 无需特殊判断,逻辑更简洁 不想处理边界条件,追求代码简洁

刷题总结

环形子数组的最大和,本质是“常规子数组”和“跨首尾子数组”的最大值求解。两种解法都基于 Kadane 算法的延伸,核心是找到“跨首尾子数组”的等价转换方式——要么用总和减去最小子数组和,要么拆分为前缀+后缀。

刷题时可以根据自己的习惯选择:如果喜欢空间最优,优先解法一;如果怕遗漏边界条件,解法二更友好。另外,建议多动手模拟几个测试用例(比如全负数组、全正数组、混合数组),就能彻底掌握两种解法的逻辑。

LeetCode 53. 最大子数组和:两种高效解法(动态规划+分治)

作者 Wect
2026年3月19日 21:44

LeetCode经典题目「53. 最大子数组和」,这道题是动态规划和分治思想的典型应用,也是面试中高频考察的基础题。题目难度不算高,但两种解法各有侧重,吃透能帮我们更好地理解两类算法的核心逻辑,话不多说,直接进入正题。

一、题目回顾

题目要求:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。注意,子数组是数组中的一个连续部分,和子序列(不要求连续)是完全不同的概念哦。

举个简单例子:输入 nums = [-2,1,-3,4,-1,2,1,-5,4],输出应该是 6。因为连续子数组 [4,-1,2,1] 的和最大,等于6。

这道题的核心难点在于「连续」和「最大和」,如果暴力枚举所有连续子数组,时间复杂度会达到 O(n²),对于大规模数组会超时,所以我们需要更高效的解法——今天重点讲两种 O(n) 和 O(nlogn) 复杂度的解法。

二、解法一:动态规划(DP)—— 最优时间复杂度 O(n)

1. 核心思路

动态规划的核心是「状态定义」和「状态转移方程」,这道题我们可以这样拆解:

定义状态 pre:表示以当前元素结尾的连续子数组的最大和。

对于每个元素 nums[i],我们有两个选择:

  • 将当前元素加入到之前的连续子数组中(即 pre + nums[i]);

  • 放弃之前的子数组,以当前元素为起点重新开始一个子数组(即 nums[i])。

所以状态转移方程就是:pre = Math.max(pre + x, x)(x 是当前遍历到的元素)。

同时,我们需要一个变量 maxAns 来记录遍历过程中出现的最大 pre 值,这个值就是最终的最大子数组和。

2. 代码解读

给出的代码非常简洁,我们逐行拆解:

function maxSubArray_1(nums: number[]): number {
  // pre:以当前元素结尾的连续子数组最大和;maxAns:全局最大和
  let pre: number = 0, maxAns: number = nums[0];
  // 遍历数组中的每个元素x
  nums.forEach((x) => {
    // 状态转移:选择加入前序子数组,或重新开始
    pre = Math.max(pre + x, x);
    // 更新全局最大和
    maxAns = Math.max(maxAns, pre);
  });
  return maxAns;
};

举个例子辅助理解(以 nums = [-2,1,-3,4,-1,2,1,-5,4] 为例):

  • 初始:pre=0,maxAns=-2(nums[0]);

  • 遍历x=-2:pre = max(0+(-2), -2) = -2,maxAns = max(-2, -2) = -2;

  • 遍历x=1:pre = max(-2+1, 1) = 1,maxAns = max(-2, 1) = 1;

  • 遍历x=-3:pre = max(1+(-3), -3) = -2,maxAns 仍为1;

  • 遍历x=4:pre = max(-2+4, 4) = 4,maxAns = 4;

  • 后续遍历依次更新,最终 maxAns 为6,和预期一致。

这种解法的优势的是:一次遍历完成,时间复杂度 O(n),空间复杂度 O(1)(只用到两个变量),是这道题的最优解法,面试中优先推荐写这种。

三、解法二:分治思想 —— 时间复杂度 O(nlogn)

分治思想的核心是「分而治之」:将数组分成左右两部分,最大子数组和要么在左半部分,要么在右半部分,要么横跨左右两部分。我们需要分别计算这三种情况的最大值,取三者中的最大者。

1. 核心思路

为了高效计算「横跨左右两部分」的最大和,我们需要定义一个 Status 类,存储每个区间的四个关键信息:

  • lSum:该区间的最大前缀和(从区间左端点开始,连续子数组的最大和);

  • rSum:该区间的最大后缀和(从区间右端点开始,连续子数组的最大和);

  • mSum:该区间的最大子数组和(就是我们需要的核心值);

  • iSum:该区间的所有元素和(用于计算横跨左右的最大和)。

然后通过「递归拆分」和「合并区间」(pushUp 函数),逐步计算出整个数组的 mSum,即为答案。

2. 代码解读

class Status {
  lSum: number; // 区间最大前缀和
  rSum: number; // 区间最大后缀和
  mSum: number; // 区间最大子数组和
  iSum: number; // 区间总元素和
  constructor(l: number, r: number, m: number, i: number) {
    this.lSum = l;
    this.rSum = r;
    this.mSum = m;
    this.iSum = i;
  }
}

function maxSubArray_2(nums: number[]): number {
  // 合并两个区间的Status,计算出父区间的四个关键值
  const pushUp = (l: Status, r: Status): Status => {
    const iSum = l.iSum + r.iSum; // 父区间总和 = 左区间总和 + 右区间总和
    // 父区间最大前缀和:要么是左区间的最大前缀和,要么是左区间总和+右区间最大前缀和
    const lSum = Math.max(l.lSum, l.iSum + r.lSum);
    // 父区间最大后缀和:要么是右区间的最大后缀和,要么是右区间总和+左区间最大后缀和
    const rSum = Math.max(r.rSum, r.iSum + l.rSum);
    // 父区间最大子数组和:三者取最大(左区间最大、右区间最大、横跨左右的最大)
    const mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
    return new Status(lSum, rSum, mSum, iSum);
  }

  // 递归获取区间 [l, r] 的Status
  const getInfo = (a: number[], l: number, r: number): Status => {
    if (l === r) { // 递归终止:区间只有一个元素时,四个值都等于该元素
      return new Status(a[l], a[l], a[l], a[l]);
    }
    const m = Math.floor((l + r) / 2); // 拆分区间为左右两部分
    const lSub = getInfo(a, l, m); // 左区间Status
    const rSub = getInfo(a, m + 1, r); // 右区间Status
    return pushUp(lSub, rSub); // 合并左右区间,返回父区间Status
  }

  // 整个数组的区间是 [0, nums.length-1],其mSum就是答案
  return getInfo(nums, 0, nums.length - 1).mSum;
};

3. 补充说明

分治解法的时间复杂度是 O(nlogn),空间复杂度是 O(logn)(递归调用栈的深度)。虽然效率不如动态规划,但这种思想很重要——在解决更复杂的区间问题(如最大子矩阵和)时,分治+区间信息合并的思路会非常有用。

四、两种解法对比总结

解法 时间复杂度 空间复杂度 核心优势 适用场景
动态规划 O(n) O(1) 高效、简洁,空间开销小 单独求解最大子数组和,面试首选
分治思想 O(nlogn) O(logn) 思路通用,可扩展到复杂区间问题 区间相关延伸题,理解分治思想

五、刷题思考

这道题虽然简单,但能帮我们理清两个重要算法思想的应用:

  1. 动态规划的核心是「抓住当前状态的最优选择」,不需要回溯,通过状态转移逐步推导全局最优;

  2. 分治思想的核心是「拆分+合并」,将大问题拆成小问题解决,再通过合并小问题的结果得到大问题的答案。

NativeScript iOS 平台开发技巧

作者 sp42_frank
2026年3月20日 09:35

升级到 NativeScript 8.7 后出现 APPLE is not defined 错误

出现了__APPLE__ is not defined 错误,是在你将 @nativescript/core, @nativescript/ios, @nativescript/android 升级到 ^8.7.0 版本后可能遇到的一个烦人错误。

官方推荐所有人都升级到 NativeScript 8.7,因为它包含了许多错误修复和改进,例如 devtool 以及恢复了从 8.4 版本开始中断的网络检查功能。然而有些人可能会遇到像下面这样的奇怪错误:

System.err: ReferenceError: __APPLE__ is not defined
System.err:
System.err: StackTrace:
System.err: ./node_modules/@nativescript/core/accessibility/font-scale-common.js(file: src/webpack:/FarmOps/node_modules/@nativescript/core/accessibility/font-scale-common.js:1:7)

原因

__APPLE__ is not defined 错误是由于 NativeScript 在他们的构建代码中引入了一些新的占位符。这些占位符依赖 Webpack 在构建时进行替换。而这个逻辑是在 @nativescript/webpack 5.0.19 中引入的。所以关键是确保你使用的 @nativescript/webpack 至少是 5.0.19 版本,才能成功使用 NativeScript 8.7 构建你的项目。

解决方案

所以基本上,解决 __APPLE__ is not defined 错误的方法是确保两件事:

  1. 首先,确保 @nativescript/webpack 的版本在你的 package.json 中没有被限制,像这样是最好的:^5.0.0
  2. 其次,确保你的 npm 已经知晓了 @nativescript/webpack 的最新可用版本,并且没有任何缓存。对我而言,我会执行 rm -rf node_modulesrm package-lock.json 然后再重新运行 npm i 来确保。或者更简单地,执行 ns clean 然后重新运行。

你总是可以尝试查看 package-lock.json找到 @nativescript/webpack 部分。如果它看起来像这样: 在这里插入图片描述

这表明实际安装的版本是 5.0.18,这是不行的。需要用我上面提到的任一种方法来解决。

在确保 @nativescript/webpack 版本没问题后,你现在可以再次运行 ns run 来继续你的 NativeScript 开发工作。

附言:如果你正在经历常见的 NativeScript 问题,并且需要一些快速修复或解决方法,请务必查看我们的“快速修复”部分。在这一部分,你会发现我在 NativeScript 之旅中收集的技巧和窍门,以及解决常见问题的解决方法。希望能帮助到许多像我一样的人。

NativeScript iOS: 无法启动模拟器

作为一名 iOS 开发者,最令人沮丧的事情莫过于 iOS 模拟器突然停止工作。这个工具对于在受控环境中测试和调试你的应用程序至关重要。当它失效时,你的工作流就会戛然而止,打乱工作效率并造成不必要的压力。

问题:无法启动模拟器

模拟器就是不工作,拒绝启动。并且一直说“无法启动模拟器”。 在这里插入图片描述

解决方案:

这个修复方法非常简单。

对于 Mac Ventura 13.0 及更高版本的操作系统 -> 点击 Mac 左上角的苹果图标 > 系统设置 > 搜索存储空间 > 等待加载,然后点击开发者 (Developer)。

在这里插入图片描述

在下一个屏幕中,选择删除 Xcode 缓存 (deleting Xcode Caches)。

删除完成后。尝试重新启动你的模拟器,现在它应该又能正常工作了。

如何正确修复:Info.plist 键 'BGTaskSchedulerPermittedIdentifiers' 必须包含一个标识符列表在这里插入图片描述

对于一个 NativeScript 应用,这个错误通常出现在 iOS 应用开发的上下文中,具体来说,当你或你安装的某些插件试图使用后台任务时,就会出现这个问题。

解决方法

  1. 打开你的应用的 App_Resources/iOS/Info.plist 文件。
  2. 如果尚不存在,添加键 BGTaskSchedulerPermittedIdentifiers
  3. 将其类型设置为 Array(数组)。
  4. 对于每个后台任务,在此数组中添加一项。每一项都应该是一个字符串,代表一个后台任务的唯一标识符。

使用示例

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>

如果你有任何自定义的后台任务,你也需要将其 ID 列入上面的数组中。除此之外,你可以直接使用上面这段代码。

$(PRODUCT_BUNDLE_IDENTIFIER) 将被解析为你在 nativescript.config.ts 中定义的应用 Bundle ID,例如:com.newbiescripter.myawesomeapp

请记住,在 iOS 中使用后台任务有一些限制和准则,因为苹果旨在优化电池续航和性能。请确保你使用后台任务的方式符合这些准则。

昨天以前首页

nestjs学习 - 守卫

作者 web_bee
2026年3月18日 16:40

NestJS 守卫是一个实现了 CanActivate 接口的类。

一、它是什么?

在 NestJS 里,「守卫(Guard)」是一种用来控制请求是否能进入路由处理器(Controller 方法) 的机制。

通俗点说:

守卫就是“门卫”——每次请求进来之前,它会先检查一下你有没有资格进去。

  • 通过进入下一步
  • 未通过❌,请求拒绝(比如返回 403 Forbidden)

核心职责:主要关注 授权(Authorization) 。虽然也可以做认证(Authentication),但通常认证由中间件或 Passport 策略处理,而守卫用于更细粒度的权限控制(如:只有管理员才能删除文章)。

在框架生命周期中,守卫的执行时机是:

请求进入 → 中间件 → 守卫 → 拦截器 → 管道 → 控制器 → 服务

二、怎么用?

创建守卫

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();

    // 简单示例:如果请求头中有 token,就放行
    const token = request.headers['authorization'];
    if (token) {
      return true; // 放行
    }

    // 否则拒绝
    return false;
  }
}

canActivate() 方法返回:

  • true → 允许进入控制器;
  • false → 阻止访问(会返回 403 Forbidden);
  • 也可以返回一个 Promise<boolean>Observable<boolean>(支持异步)。

应用守卫

你可以在三个层级使用守卫:

1. 方法级

import { UseGuards, Controller, Get } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('user')
export class UserController {
  @Get('profile')
  @UseGuards(AuthGuard)
  getProfile() {
    return { msg: '用户资料' };
  }
}

2. 控制器级

@UseGuards(AuthGuard)
@Controller('admin')
export class AdminController {
  @Get()
  getAdminData() {
    return '后台数据';
  }
}

3. 全局守卫

// main.ts
import { AppModule } from './app.module';
import { AuthGuard } from './auth.guard';
import { NestFactory } from '@nestjs/core';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}
bootstrap();

三、使用场景

守卫最常见的用途是:权限控制 / 身份验证

  1. 身份验证(Authentication) :检查用户是否已登录(例如验证 JWT Token 是否存在且有效)。
  2. 角色授权(Role-based Authorization) :检查当前用户是否拥有特定角色(如 admin, editor)。
  3. 权限控制(Permission-based Authorization) :检查用户是否有执行特定操作的权限(如:user:delete)。
  4. IP 地址过滤:只允许特定 IP 段的请求访问。
  5. 功能开关:根据配置动态开启或关闭某些接口。
  6. 请求时间限制(比如只允许工作时间访问)

你可以把它理解为:

在“进入接口之前”的最后一道防线。

四、中间件 vs 守卫

1. 中间件

中间件 是通用的“流水线工人”,负责处理请求的通用逻辑(如日志、解析数据),它不知道具体的业务逻辑是什么。

中间件的盲区: 当中间件运行时,NestJS 还没有确定最终由哪个 Controller 的哪个方法来处理请求。因此,中间件无法知道当前请求是否需要“管理员权限”。你无法在中间件里写:“如果这个路由用了 @Roles('admin') 装饰器,则检查角色”。

2. 守卫:

守卫 是专业的“安检员”,专门负责授权决策(能不能进),它完全知道即将执行哪个具体的控制器方法,并能根据元数据做判断。

守卫的全知视角

守卫接收 ExecutionContext 对象。通过这个对象,你可以拿到:

  • context.getClass(): 当前的 Controller 类。
  • context.getHandler(): 当前正在执行的方法。
  • 结合 Reflector,你可以读取该方法上所有的装饰器元数据(例如 @Roles('admin'))。
  • 结论:凡是需要根据路由元数据(装饰器)来做判断的逻辑,必须用守卫。

3. 核心区别对比表

特性 中间件 (Middleware) 守卫 (Guard)
主要职责 通用逻辑:日志、压缩、Cookie 解析、原始请求预处理。 授权 (Authorization) :决定请求是否允许执行特定的 Handler。
执行时机 最早。在守卫、拦截器、管道之前执行。 中间。在中间件之后,拦截器和管道之前执行。
上下文感知 。只知道 reqres不知道具体要调用哪个 Controller 或哪个方法。 。拥有 ExecutionContext,知道具体的 Class、Handler 方法、参数类型等。
访问元数据 无法直接访问路由装饰器(如 @Roles, @Get)定义的元数据。 可以访问。配合 Reflector 可以轻松读取路由上的自定义元数据。
返回值/控制流 必须调用 next() 才能继续,或者直接 res.end() 结束响应。 返回 boolean (或 Promise/Observable)。true 放行,false 拒绝(抛出异常)。
依赖注入 支持,但配置稍显繁琐(通常通过 forRoot 或模块配置)。 完美支持,像普通 Service 一样注入依赖。
适用场景 记录所有请求日志、解析 JSON/Cookie、设置 CORS、Gzip 压缩。 检查 JWT、验证用户角色、IP 白名单、基于权限的访问控制。

TS 入门:给 React 穿上“防弹衣”

作者 玉米Yvmi
2026年3月18日 14:50

前言
JavaScript 像是一位随性的艺术家,自由但易错;TypeScript 则是一位严谨的工程师,用类型系统为我们筑起防线。

很多新手觉得 TS 繁琐,那是还没掌握“正确姿势”。今天,我不讲枯燥理论,直接通过实战场景,带你把 TS 融入 React 的血脉。

一、组件的“身份证”:Props 精准定义

在 JS 中,Props 靠“口头约定”;在 TS 中,Props 必须有“身份证”。

传错参数?漏传必填项?运行时才报错?NO!

使用 interface 定义契约,利用 React.ReactNode 兼容所有内容。

// 第一步:定义契约
interface AaaProps {
  name: string;        // 必填:必须是字符串
  age?: number;        // 可选:注意那个问号 '?'
  content: React.ReactNode; // 万能容器:字符串/JSX/Fragment 都能装
}

// 第二步:应用契约 (推荐写法)
function Aaa({ name, content }: AaaProps) {
  return <div> Hi, {name} | {content}</div>;
}

// 第三步:安全使用
export default function App() {
  // TS 会立刻拦截:如果忘记传 name,或者 content 传了数字,直接标红!
  return <Aaa name="玉米🌽" content={<span>我是内容</span>} />;
}
  • ? 的作用:明确区分“可有可无”和“必须拥有”。
  • React.ReactNode:比 any 安全,比 string 灵活,它是 React 内容的“最大公约数”。
  • 解构赋值:直接在函数参数中解构 { name },代码更清爽,TS 依然能自动推断类型。

二、Hooks 的“导航仪”:泛型让状态不再模糊

useStateuseRef 是 React 的左右手,但在 TS 中,如果不加泛型 <T>,它们就像失去了导航的船。

场景 A:状态初始化

// 没给初始值时,TS 默认它是 undefined
const [num, setNum] = useState<number>(); 
// 类型推断:number | undefined

// 给了初始值,TS 就知道它永远是 number
const [count, setCount] = useState<number>(0); 
// 类型推断:number

场景 B:Ref 的双重身份

useRef 既能抓 DOM,也能存数据。怎么区分?看泛型!

// 身份 1:DOM 捕手
const inputRef = useRef<HTMLInputElement>(null);
// current 可能是 HTMLInputElement 或 null

// 身份 2:数据储物柜 (不触发重渲染)
const storeRef = useRef<{ num: number }>(null);

// 安全赋值
if (storeRef.current) {
  storeRef.current.num = 2; // TS 知道这里有 num 属性
}

泛型就像一个**“模具”**。

  • 倒入 HTMLInputElement,它就是抓 Input 的夹子。
  • 倒入 { num: number },它就变成了存数据的盒子。
  • 如果不指定模具,TS 就只能给你一团模糊的橡皮泥(any 或推断错误)。

三、打通任督二脉:ForwardRef 的类型接力

父组件想操作子组件的 DOM?forwardRef 是桥梁,但 TS 需要知道这座桥通向哪里。

核心三步走

// 定义子组件:明确 Ref 的目标是 input
const Child = forwardRef<HTMLInputElement>((props, ref) => {
  return <input ref={ref} placeholder="请聚焦我" />;
});

// 父组件声明:Ref 类型必须与子组件一致
const parentRef = useRef<HTMLInputElement>(null);

// 安全调用:使用可选链 '?.' 防止 null 报错
useEffect(() => {
  parentRef.current?.focus(); 
}, []);

如果子组件说“我要 Input”,父组件却传了个 div 的 ref,TS 编译器会直接亮红灯。这种端到端的类型检查,彻底杜绝了 Cannot read property 'focus' of null 的低级错误。

四、性能优化的“类型护航”

当项目变大,useReducermemo 登场。TS 能让你的优化逻辑无懈可击。

状态建模:Action 联合类型

这是 TS 最强大的特性之一:判别联合类型

// 定义状态
interface State { result: number; }

// 定义动作 (关键!限制 type 的取值)
type Action = 
  | { type: 'add'; num: number } 
  | { type: 'minus'; num: number };

// Reducer:TS 会自动根据 type 推断 action 的结构
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'add': 
      // 这里 TS 知道 action 一定有 num
      return { result: state.result + action.num };
    case 'minus':
      return { result: state.result - action.num };
    default:
      return state;
  }
}

配合 Memo 优化

// 缓存计算结果
const count = useMemo(() => res.result * 10, [res.result]);

// 缓存函数引用 (防止子组件无效渲染)
const cb = useCallback(() => 666, []);

// 子组件:只有 props 变了才渲染
const Child = memo(({ count }: { count: number }) => (
  <h2>计算结果:{count}</h2>
));

如果你手误写了 dispatch({ type: 'delete' ... }),TS 会立刻告诉你:“没有 delete 这个动作!”。这在 JS 中可能要等到用户点击按钮报错了才能发现。

结语

TypeScript 初看是束缚,实则是赋予你重构底气的“防弹衣”。它让你从“运行后报错”的被动救火,转向“编写时即知”的主动掌控,将潜在的隐患扼杀在编译阶段。

不必追求一步到位,渐进式地收窄每一个 any,都是在为代码大厦加固地基。当你习惯了类型系统带来的智能提示与安全边界,便真正完成了从“码农”到“工程师”的思维跃迁。

给 JS穿上铠甲:TypeScript 基础核心概念详解(类型/接口/泛型)

作者 玉米Yvmi
2026年3月18日 13:52

前言

曾经,我沉醉于 JavaScript 的灵活与自由。变量可以随意赋值,函数参数无需声明,一切看起来都那么随心所欲。直到有一天,一个看似简单的 undefined is not a function 错误在生产环境爆发,我才惊觉:这种“自由”有时更像是在没有护栏的悬崖边跳舞。

在深入探索 TypeScript 的过程中,我深刻体会到了它带来的秩序之美。今天,我想结合实战代码,和大家聊聊 TypeScript 的基础,希望能帮同样在转型的你,穿上铠甲,从容前行。

一、混沌与秩序:为什么我们需要类型?

在学习 TypeScript 之前,我们先回看一下 JavaScript 的世界。

在 JavaScript 中,变量就像是一个个没有标签的盒子。你可以把数字放进去,下一秒又可以把它拿出来,换成一个字符串。这种“弱类型”特性虽然开发速度快,但也埋下了隐患。编译器直到代码运行的最后一刻,才知道盒子里装的是什么。如果此时盒子里的东西不是我们预期的,程序就会崩溃。

// JavaScript 的动态类型陷阱
function add(a, b) {
  // 运行时才能发现类型问题
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b
  }
  return undefined; 
}

// 调用时传入了字符串,编译器不会报错,但逻辑可能非预期
const result = add(1, '2'); 
console.log(result); // 输出 undefined,而非报错

相比之下,C 语言等“强类型”语言则要求我们在定义变量时就必须声明类型,一旦类型不匹配,编译直接失败。

TypeScript 正是为了解决 JavaScript 的痛点而生。它给 JavaScript 加上了静态类型的“护栏”。它不改变 JS 的运行机制,而是在代码运行前(编译阶段)就帮我们检查类型是否正确。

看,这是迈向 TypeScript 的第一步:

// TypeScript 的类型注解
let a: number = 1;
// a = '2';  // ❌ 报错!TypeScript 会大声告诉你:'2' 不能赋值给 number 类型的变量
console.log(a);

这就好比给变量贴上了标签。一旦贴上 number 的标签,这个盒子就只能装数字。如果你试图塞进字符串,TS 编译器会立即拦截,将错误扼杀在摇篮里。

二、基础数据类型:构建类型的基石

有了类型注解的概念,我们就可以开始构建更复杂的数据结构了。TS 提供的一系列基础类型,是我们搭建程序的砖瓦。

1. 布尔值与数字

最基础的类型,对应 JS 中的 booleannumber

let isDone: boolean = false;
let count: number = 123;

2. 字符串与字面量类型

除了普通的字符串,TS 还允许我们定义“字面量类型”,即变量只能是某个特定的字符串值。这在做状态管理时非常有用,就像给变量限定了唯一的“身份证号”。

const hello = 'hello';
const a: 'hello' = 'hello'; // ✅ 正确
// const b: 'hello' = 'world'; // ❌ 错误,只能是 'hello'

3. 数组与元组

数组用来存储相同类型的列表,而元组(Tuple)则像是固定长度的“混合容器”,可以存储不同类型的值,但顺序和类型必须严格对应。

// 普通数组:只能装数字
let list: number[] = [1, 2, 3];

// 元组:第一个必须是 number,第二个必须是 string
let tuple: [number, string] = [1, 'hello']; 
// let errorTuple: [number, string] = ['hello', 1]; // ❌ 类型错位,编译器直接红牌罚下

4. 枚举(Enum)

枚举让我们可以定义一组命名的常量,让代码可读性更强。就像给方向定义了名字,而不是使用晦涩的数字。

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST
}
let dir: Direction = Direction.NORTH; // 比直接写 0 更易读,代码自文档化

5. Any 与 Unknown:双刃剑与保险丝

在迁移旧代码时,我们难免会遇到类型不确定的情况。JS 开发者习惯用 any,它意味着“关闭类型检查”。

let notSure: any = 100;
notSure = '123'; // ✅ 随便改,TS 不管了,这里失去了保护

any 用多了,TS 就退化成 JS 了,失去了保护意义。

TS 提供了更安全的 unknown。它和 any 一样可以接收任何类型,但在你使用它之前,必须进行类型判断或断言。这就像是一个带保险丝的电路,虽然通电,但必须先确认安全才能使用。

let value: unknown = 123;
value = '123';

// let abc: string = value; // ❌ 报错!不能直接把 unknown 赋给 string
// 必须先收窄类型,确认安全
if (typeof value === 'string') {
  let abc: string = value; // ✅ 安全了,TS 知道此时 value 一定是 string
}

6. Void, Null, Undefined 与 Symbol

这些类型分别对应无返回值、空值、未定义以及唯一的标识符。特别是 void,常用于没有返回值的函数,明确告诉调用者“别指望我有返回值”。

function warnUser(): void {
  console.log("This is my warning message");
  // 这里不需要 return 任何值,甚至 return undefined 也是允许的
}

三、对象与接口:描绘数据的形状

在实际开发中,我们处理最多的往往是对象。如何描述一个对象的“形状”?TS 提供了 接口类型别名

接口就像是建筑的蓝图,规定了对象必须拥有哪些属性,哪些是可选的。

interface Person {
  name: string;
  age: number;
  sex?: string; // ? 表示可选属性,就像装修时的“预留接口”
}

const p: Person = {
  name: '探长',
  age: 20
  // sex 属性可选,不写也不会报错,系统依然认为它是合法的 Person
};

除了接口,TS 还提供了强大的类型运算。我们可以像搭积木一样组合类型。 使用了交叉类型(&)来合并两个类型,创造出新的形态:

type PartialX = { x: number };

// Point 类型既要有 x,也要有 y,通过 & 将两个类型“焊接”在一起
type Point = PartialX & { y: number };

const p: Point = {
  x: 1,
  y: 2
};

这就像是将两块拼图完美地拼在一起,形成了一个新的、更完整的形状。这种组合能力让 TS 在处理复杂数据结构时游刃有余,避免了重复定义。

四、泛型:类型的“模具”

如果说接口是描述具体对象的蓝图,那么泛型(Generics)就是制造蓝图的模具

想象一下,你要写一个函数,它的功能是“原样返回传入的参数”。

  • 如果传入数字,返回数字;
  • 如果传入字符串,返回字符串。

在没有泛型之前,我们可能要用 any,但这会丢失类型信息,导致调用者不知道返回的是什么。泛型允许我们将类型作为一个参数传递进去,让函数具有“多态”的能力,且保持类型安全。

// T 是一个类型占位符,调用时确定具体是什么类型
// 就像是一个通用的容器,里面装什么,倒出来就是什么
function identity<T>(value: T): T {
  return value;
}

// 调用时指定 T 为 number
const num = identity<number>(100); 
// num 的类型被推断为 number

// 调用时指定 T 为 string
const str = identity<string>('hello');
// str 的类型被推断为 string

泛型还可以同时接受多个类型参数,甚至用于约束数组等复杂结构,极大地提高了代码的复用性:

// 定义一个既可以存 number 也可以存 string 的数组
let arr: Array<number | string> = [1, 2, 3, '1'];

泛型让代码变得更加灵活且安全,它是 TS 进阶的必经之路,也是区分新手与老手的关键标志。

五、类型断言与守卫:掌控不确定性

有时候,我们比编译器更清楚某个变量的类型。比如在处理 DOM 元素或者第三方库返回的数据时。这时,我们可以使用类型断言,告诉编译器:“相信我,我知道我在做什么。”

TS 提供了两种断言方式,推荐使用的是 as 语法:

let someValue: any = 'this is a apple';

// 方式一:as 语法(推荐,兼容性好)
let strLength = (someValue as string).length;

// 方式二:尖括号语法(不能在 JSX/TSX 中使用,容易与 HTML 标签混淆)
// let strLength = (<string>someValue).length;

但断言并非万能,盲目断言可能导致运行时错误。更优雅的方式是使用类型守卫。通过 typeofinstanceof 或自定义判断函数,在代码块内部收窄类型范围。这就像是在迷雾中点亮一盏灯,只有走进灯光范围(if 语句块内),变量的真实面目才会被看清,TS 也会随之放宽限制,允许你访问特定类型的方法。

function printId(id: number | string) {
  if (typeof id === "string") {
    // 在这里,id 的类型被收窄为 string
    console.log(id.toUpperCase());
  } else {
    // 在这里,id 的类型被收窄为 number
    console.log(id);
  }
}

结语:从束缚到自由

回顾这段旅程,我们经历了从“随意赋值”的混乱,到“严格定义”的束缚,最后达到了“类型安全下的自由”。TypeScript 并不是要给 JavaScript 戴上沉重的枷锁,而是为我们提供了一套精密的导航系统。

学习之路漫长,这些基础只是探索 TS 世界的起点。希望这篇文章能帮你理清 TS 的脉络,让你在写代码时多一份底气,少一份 undefined 的惊吓。

让 AI 用自然语言操控三维地球 -- Cesium MCP 开源实践

作者 laogao
2026年3月15日 16:53

让 AI 用自然语言操控三维地球 -- Cesium MCP 开源实践

一句"飞到埃菲尔铁塔,加个红色标记",Claude/Copilot/Cursor 就能帮你在 CesiumJS 里完成操作。这是怎么做到的?

演示效果

先看效果,了解 cesium-mcp 能做什么:

demo-full.gif

演示中通过 AI 对话完成了相机飞行、添加标记、样式修改等操作。

背景:当 GIS 遇上 AI Agent

CesiumJS 是 WebGL 三维地球可视化的事实标准。但凡涉及地理信息系统(GIS)的 Web 项目——智慧城市、数字孪生、无人机航线规划——几乎绑定 CesiumJS。

问题是:Cesium API 体量庞大,光 Viewer 就有几十个配置项,Entity 系统更是嵌套层层。新人写个"在地图上加个点"都要翻半天文档。

2024 年底 Anthropic 推出了 MCP(Model Context Protocol),让 AI 智能体能以标准化方式调用外部工具。我们顺着这条路做了一件事:

把 CesiumJS 的能力通过 MCP 协议暴露出来,让任何 AI 智能体都能用自然语言操控三维地球。

这就是 cesium-mcp

它能做什么

整体架构

graph LR
    subgraph AI["AI 智能体"]
        A1["Claude Desktop"]
        A2["VS Code Copilot"]
        A3["Cursor"]
    end

    subgraph MCP_Server["cesium-mcp-runtime<br/>(Node.js MCP Server)"]
        R1["MCP stdio 接口"]
        R2["WebSocket Server"]
    end

    subgraph Browser["浏览器"]
        B1["cesium-mcp-bridge<br/>(SDK)"]
        C1["CesiumJS Viewer<br/>三维地球"]
    end

    A1 -->|"MCP 协议<br/>(stdio)"| R1
    A2 -->|"MCP 协议<br/>(stdio)"| R1
    A3 -->|"MCP 协议<br/>(stdio)"| R1
    R1 <--> R2
    R2 <-->|"WebSocket<br/>JSON-RPC"| B1
    B1 -->|"命令执行"| C1

    style AI fill:#e8f4f8,stroke:#2196F3,stroke-width:2px
    style MCP_Server fill:#fff3e0,stroke:#FF9800,stroke-width:2px
    style Browser fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px

简单说就是三层:

  1. cesium-mcp-bridge(浏览器 SDK):嵌入你的 CesiumJS 应用,通过 WebSocket 接收命令并执行
  2. cesium-mcp-runtime(MCP Server):连接 AI 智能体和浏览器,暴露 19 个标准化工具
  3. cesium-mcp-dev(开发辅助 MCP Server):在 IDE 里让 AI 助手更懂 Cesium API

19 个工具,覆盖 GIS 核心场景

类别 工具 说明
相机 flyTo setView getView zoomToExtent 飞行定位、视角切换
图层 addGeoJsonLayer addHeatmap addMarker addLabel 数据叠加、热力图
图层管理 removeLayer setLayerVisibility listLayers updateLayerStyle 增删改查
三维数据 load3dTiles loadTerrain loadImageryService 3D Tiles、地形、影像服务
底图 setBasemap 天地图、ArcGIS、OSM 一键切换
交互 highlight screenshot 要素高亮、截图
动画 playTrajectory 沿路径播放轨迹动画

你对 AI 说"加载这个 GeoJSON,用渐变色渲染人口密度",它会自动调用 addGeoJsonLayer 并传入样式参数。

三分钟跑起来

第一步:浏览器嵌入 bridge

npm install cesium-mcp-bridge
import { CesiumMcpBridge } from 'cesium-mcp-bridge';

// viewer 是你已有的 Cesium.Viewer 实例
const bridge = new CesiumMcpBridge(viewer, { port: 9100 });
bridge.connect();

第二步:启动 MCP 运行时

npx cesium-mcp-runtime

第三步:接入 AI 智能体

以 Claude Desktop 为例,在配置文件中添加:

{
  "mcpServers": {
    "cesium": {
      "command": "npx",
      "args": ["-y", "cesium-mcp-runtime"]
    }
  }
}

VS Code Copilot 用户在 .vscode/mcp.json 中配置:

{
  "servers": {
    "cesium": {
      "command": "npx",
      "args": ["cesium-mcp-runtime"]
    }
  }
}

然后直接用自然语言下指令:

  • "飞到北京天安门,高度 1000 米"
  • "加载这个 3D Tiles 模型"
  • "画一条从上海到纽约的折线"
  • "截张图发我"

开发时也有 AI 加持

除了运行时操控,我们还做了 cesium-mcp-dev——专为 IDE AI 助手设计的 MCP 服务器:

graph LR
    subgraph IDE["IDE 环境"]
        D1["GitHub Copilot"]
        D2["Cursor AI"]
        D3["Claude Code"]
    end

    subgraph DevServer["cesium-mcp-dev<br/>(MCP Server)"]
        T1["cesium_api_lookup<br/>API 文档查询"]
        T2["cesium_code_gen<br/>代码生成"]
        T3["cesium_entity_builder<br/>Entity 构建器"]
    end

    subgraph Output["输出"]
        O1["API 签名 & 示例"]
        O2["TypeScript 代码片段"]
        O3["Entity 配置 JSON"]
    end

    D1 -->|"MCP stdio"| DevServer
    D2 -->|"MCP stdio"| DevServer
    D3 -->|"MCP stdio"| DevServer
    T1 --> O1
    T2 --> O2
    T3 --> O3

    style IDE fill:#f3e5f5,stroke:#9C27B0,stroke-width:2px
    style DevServer fill:#fff3e0,stroke:#FF9800,stroke-width:2px
    style Output fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px

提供 3 个工具:

工具 功能
cesium_api_lookup 按类名/方法查 Cesium API 文档,覆盖 Viewer、Entity、Camera 等 12 个核心类
cesium_code_gen 自然语言生 Cesium 代码,内置 11 个常见场景模板
cesium_entity_builder 交互式构建 Entity 配置,支持 8 种类型(point/polygon/model 等)

配置方式和 runtime 完全一致:

{
  "servers": {
    "cesium-dev": {
      "command": "npx",
      "args": ["cesium-mcp-dev"]
    }
  }
}

这意味着你在 VS Code 里写 Cesium 代码时,Copilot 可以直接查 API、生成代码片段、构建 Entity 配置——再也不用频繁切到文档网站。

一次操控的完整流程

以"飞到北京天安门,加个红色标记"为例,看看数据是怎么流转的:

sequenceDiagram
    participant User as 用户
    participant AI as AI 智能体
    participant RT as cesium-mcp-runtime
    participant BR as cesium-mcp-bridge
    participant CS as CesiumJS

    User->>AI: "飞到北京天安门,加个红色标记"
    AI->>AI: 理解意图,拆解为两步

    rect rgb(232, 244, 248)
    Note over AI,CS: 第一步:飞行定位
    AI->>RT: MCP tool_call: flyTo({lon:116.39, lat:39.91, h:1000})
    RT->>BR: WebSocket JSON-RPC
    BR->>CS: viewer.camera.flyTo(...)
    CS-->>BR: 飞行完成
    BR-->>RT: result: success
    RT-->>AI: tool_result: "已飞行到目标位置"
    end

    rect rgb(232, 245, 233)
    Note over AI,CS: 第二步:添加标记
    AI->>RT: MCP tool_call: addMarker({lon:116.39, lat:39.91, color:"red"})
    RT->>BR: WebSocket JSON-RPC
    BR->>CS: viewer.entities.add(...)
    CS-->>BR: entity created
    BR-->>RT: result: {id: "marker-1"}
    RT-->>AI: tool_result: "已添加红色标记"
    end

    AI-->>User: "已飞到天安门并添加了红色标记"

AI 自动将自然语言拆解为多个工具调用,每个工具走完 MCP -> WebSocket -> CesiumJS 的完整链路,结果逐级回传。用户只需要说一句话。

技术实现要点

Bridge:命令注册与执行

cesium-mcp-bridge 的核心是一个命令注册表。每个 MCP 工具对应一个命令处理器,通过 CesiumBridge.execute() 分发:

const bridge = new CesiumBridge(viewer);
// 收到 WebSocket 消息后
const result = await bridge.execute({
  action: 'flyTo',
  params: { longitude: 116.4, latitude: 39.9, height: 1000 }
});

Bridge 不关心命令从哪来——WebSocket、HTTP、甚至手动调用都行。这种解耦使得 Bridge SDK 可以独立于 MCP 使用。

Runtime:双向通信

Runtime 同时充当 MCP stdio 服务器和 WebSocket 服务器。AI 智能体通过 MCP 协议发送工具调用,Runtime 把它翻译成 JSON-RPC 命令通过 WebSocket 推给浏览器,等待执行结果后回传给 AI。

支持多会话(session),同一个 Runtime 可以连接多个浏览器页面。

版本策略

主版本号.次版本号跟踪 CesiumJS(1.139.x 对应 Cesium ~1.139.0),补丁版本独立迭代 MCP 功能。这样用户一看版本号就知道兼容哪个 Cesium。

已上架平台

平台 状态
npm Registry cesium-mcp-bridge / cesium-mcp-runtime / cesium-mcp-dev v1.139.2
MCP Official Registry io.github.gaopengbin/cesium-mcp-runtime / cesium-mcp-dev
Smithery runtime(19 tools)/ dev(3 tools)
awesome-mcp-servers PR 已提交

适用场景

  • 快速原型:用自然语言几分钟搭出 GIS 可视化 demo
  • 非开发人员:分析师、项目经理可以直接对 AI 说需求,AI 在 Cesium 上渲染结果
  • 教学演示:课堂上让学生用自然语言探索地理数据
  • 自动化流水线:CI/CD 中自动截图、自动验证地图渲染
  • 智慧城市/数字孪生:AI Agent 作为交互层,终端用户通过对话操控三维场景

参与贡献

项目完全开源(MIT),欢迎参与:

git clone https://github.com/gaopengbin/cesium-mcp.git
cd cesium-mcp
npm install
npm run build
npm test

GitHub: github.com/gaopengbin/… 官方文档: gaopengbin.github.io/cesium-mcp


如果你也在做 GIS + AI 的事情,欢迎交流。有问题直接在 GitHub Issues 提,我们会及时回复。

数字孪生大屏必看:Cesium 3D 模型选中交互,3 种高亮效果拿来就用!

作者 李剑一
2026年3月15日 16:23

接前文,3D模型加载到页面以后肯定要执行各种各样的操作,模型在大屏上的主要作用是执行相应的建筑物交互。

问题

这里需要注意3D模型的加载仍然存在一些问题。

首先是如果你使用的GLTF文件,在某些特殊情况下可能导致模型的网格加载异常,会出现无法选中的情况。

这一点我目前没发现有什么太好的解决方案,所以这里采用GLB文件规避掉了这个问题。

image.png

如果你手里只有GLTF文件,可以考虑使用 Blender 进行一下转换。

另外模型本身离地高度都是 0 的话可能存在无法选中的问题,所以这里建议背景设置离地高度为 -0.5,普通模型正常为 0

还有就是模型选中以后的显示问题。

解决方案

模型无法选中和选择错误的问题通过两个方案进行规避,一个是height离地高度,一个是pickable拾取

首先设置背景的离地高度为 -0.5,普通可以选中的模型离地高度为 0

image.png

另外设置一下 pickPriority拾取优先级pickable是否可拾取

最后就是设置模型的选中效果,这里我简单写了三种效果给大家选择,可以自行决定。

实际代码

模型添加的时候增加拾取优先级参数:

const buildingEntity = viewer.entities.add({
    id: options.id, // 唯一ID,点击交互时识别核心
    name: options.properties.name || options.id, // 建筑名称(可选)
    position: position,
    orientation: orientation, // 控制模型朝向
    pickPriority: options.pickPriority, // (核心)添加拾取优先级
    pickable: options.pickable,  // (核心)允许拾取
    model: {
        uri: options.modelUrl, // glTF/glb 模型路径
        scale: options.scale || 1.0, // 保证模型真实比例(建模时单位为米)
        minimumPixelSize: 0, // 取消最小像素限制,模型随地图缩放正常变化
        maximumScale: 20000, // 最大缩放限制
        runAnimations: false, // 静态建筑关闭动画(节省性能)
        clampToGround: true, // 贴地(自动适配地形高度,可选)
    },
    properties: options.properties || {}, // 绑定自定义属性(如状态接口)
});

这里的参数其实在模型选择那里可以再增加一层判断。

模型选中方法主要有三种:

// 1. 轮廓线方案
/**
 * 选中指定模型
 * @param {Cesium.Entity} entity 要选中的模型实体
 * @param {Function} onSelect 选中回调(如展示状态弹窗)
 * @param {Function} onUnselect 取消选中回调(用于先取消之前的选中)
 */
const selectModel = (entity, onSelect, onUnselect) => {
    // 取消之前的选中状态(包括回调执行)
    if (selectedEntity) {
        unselectModel(onUnselect);
    }

    // 校验是否为模型实体
    if (!entity || !entity.model) {
        console.warn('❌ 选中的不是模型实体');
        return;
    }

    // 标记为当前选中实体
    selectedEntity = entity;

    // 轮廓线高亮(更醒目,性能略高,需 Cesium 1.90+)
    entity.model.outlineColor = Cesium.Color.RED;
    entity.model.outlineWidth = 2;
    entity.model.outline = true;

    // 执行选中回调(绑定业务逻辑)
    if (typeof onSelect === 'function') {
        onSelect(entity);
    }
};

如果没有特殊要求,轮廓线方案其实非常简单实用。

// 2. 颜色高亮,修改模型材质
originalModelMaterial = entity.model.color || Cesium.Color.WHITE.clone();
entity.model.color = Cesium.Color.fromCssColorString('#fb0528').withAlpha(0.8);
// 强制刷新
viewer.scene.requestRender();

这里需要注意,修改模型材质一定要执行强制刷新

// 3. 模型遮罩效果
viewer.entities.add({
    position: entity.position,
    orientation: entity.orientation,
    model: {
        uri: entity.model.uri, // 复用同一个模型文件
        scale: 0.35, // 稍微大一点
        color: Cesium.Color.fromCssColorString('#409EFF').withAlpha(0.5), // 半透明蓝
        silhouetteColor: Cesium.Color.BLUE, // 可选:配合轮廓
        silhouetteSize: 2.0
    }
});

这种效果也非常不错,复用模型文件稍大一号,让他完美的遮住原始模型,给出一个透明色作为材质,显得很有科技感。

总结

模型设计的时候推荐大家优先使用 GLB 格式替代 GLTF 规避网格加载异常问题,

另外通过pickPriority(拾取优先级)和pickable(是否可拾取)参数,从逻辑层面控制模型的交互规则,彻底解决 点错模型、点不到模型 的问题。

后续增加相关图标的单击和操作,实现小型设备的交互。

对话蔚来李斌:2026 年押注大五座 SUV,存储成本大涨但蔚来不涨价

作者 刘学文
2026年3月13日 19:24

蔚来的首个季度盈利财报在 2025 年 3 月 10 日如约而至,即便从去年开始蔚来就宣布了季度盈利目标,但对于一家持续亏损,经常陷入质疑需要一再自证的新造车企业而言,2025 年四季度的 12.5 亿元经营利润是一个崭新的开始,或许它经受的质疑会少一些,大家对它的信心也会多一些。

3 月 11 日,在蔚来总部,蔚来创始人,董事长兼 CEO 李斌和蔚来总裁秦力洪接受了爱范儿和董车会在内的媒体采访,重点聊了聊 2026 年的规划和变化,以及为什么蔚来实现了盈利,并且会持续盈利。

蔚来进入发展的第三阶段,2026 年重点发力大五座

2025 年最终能够实现盈利的功勋车型非全新 ES8 莫属,这款三排六座旗舰 SUV 在去年 9 月上市之后,连续多月斩获大型 SUV 和 40 万元以上车型销量冠军,即便是今年 2 月中间插入了一个春节淡季,这款车型也卖出了 11260 辆。

在全新 ES8 之前,蔚来旗下的乐道品牌推出的乐道 L90 也是一款三排六座 SUV,并且多月销量过万。

可以说,2025 年蔚来力推的产品是大六座产品,而在采访中,李斌透露 2026 年蔚来将会重点发力大五座。

2025 年蔚来的主力新车是一款五座 SUV 乐道 L60,两款六座 SUV 蔚来 ES8 和 乐道 L90,今天的产品规划也已经明牌:首先是比 ES8 更高端更豪华的行政级大三排 SUV ES9;然后是「双舱超级大五座」乐道 L80,接着是基于 ES8 同平台的大五座(也就是之前盛传的 ES7,但最终命名可能不是 ES7)。

李斌说:

今年我们这三款全新车型,能让我们覆盖更大的市场,我们还是有信心延续去年 L90 和 ES8 的成功。从用户基数的角度来讲,这几款车覆盖的用户范围目标市场能还更大一些,应该能大两到三倍,所以我们其实是很有信心的。

爱范儿在去年年初也表示,三排六座 SUV 是 2025 年汽车市场的版本答案,主流车企都在这个领域投入海量资源进行竞争以至于这个版本答案里也存在这诸多炮灰选手。

▲ 蔚来 ES8 同平台大五座产品谍照

这里蔚来和李斌对 SUV 市场有一个基本的判断:

家庭大三排 SUV 市场的增量我认为比较小,甚至于会往下走。但是大五座 SUV 的这个增量(会爆发),真正好用的大五座和不好用的区别还是挺大的。

李斌做这个判断的一个底层逻辑是人口结构的变化,小家庭是主流家庭结构,如果说六座 SUV 是为了满足过往没有满足的巨大需求的话,那么大五座 SUV 则是主流市场的重新洗牌机会。李斌还透露,乐道 L80 和基于全新 ES8 平台的新大五座,在产品定义的时候洞察了小家庭这部分用户的需求,会有非常有意思的,并且足够多的适配场景。

他甚至还说,如果不是创业的话,他和秦力洪早就各自开着大五座到处游山玩水去了。

在蔚来的设想中,两款大五座产品去覆盖比去年大六座产品目标市场大两三倍的市场,这就是蔚来在 2026 年获得继续增长的动力。

并且,基于 2025 年最后一个季度获得盈利的事实,蔚来也正式表示,进入了发展的第三阶段。

对于一家年销量三四十万辆,销售均价 30 万以上的品牌来说,想要再获得年年翻倍的高速增长已经几乎不可能,但是作为一家需要在国内市场比肩 BBA 的品牌来说,蔚来需要在比较短的时间里站上 50 到 60 万年销量的门槛,这意味着即便 2025 年蔚来获得了快速增长,但目前 326028 的年销量,离 BBA 还有数个身位的距离。

李斌说:

我们今天要做的事情就是一天一天把事情做好,这也是为什么这两年内部统一思想,强调日拱一卒,久久为功,我们不太去提太宏大的东西了,很少说 10 年后要做到多少量。现在提的就是在第三个增长周期里保持 40%–50% 的(平均年)增长。

在 3 月 10 日的财报披露上,蔚来对 2026 年第一季度的交付指引是 80000 台-83000 台,同比增长90.1%-97.2%。营收指引 244.8 亿元-251.8 亿元,同比增长 103.4% 至 109.2%,这还是建立在年初行业寒冬的基础上。

营收指引增长高于销量增长,这意味着大概率蔚来的平均成交单价继续增长。李斌在采访中透露:

我们去年三个品牌的平均成交价总体来讲是增加了一万多,然后蔚来品牌应该是去年全行业唯一的一家,平均成交价往上走的品牌。

ES8 能卖好,以及蔚来对 ES9、乐道 L80 以及蔚来待命名旗舰大五座 SUV 的信心意味着,蔚来在保持车型矩阵的合理价格带分布上,其实看得还挺长远,并非销量不行就推低价车拉量的短视思路。

整个公司爱上了算账,存储价格上涨,但不会把成本转嫁给消费者

「算账」是整场采访的高频词汇。

如果没有精打细算的话,蔚来很可能没法在 2025 年最后一个季度获得盈利,毕竟 12.5 亿的经营利润对于过往蔚来的投入和亏损来说,显得渺小。

但,算账省钱成为蔚来的一种企业文化,甚至是一种乐趣之后,变化比想象中更大。

李斌举了一个小例子:作为公司的创始人和董事长,他出差的住宿标准是 400 元一天,去海拉尔还有牙克石出差都是住全季酒店,回合肥还是住全季酒店,甚至合肥全季还超标准了。

另外一个省钱的例子是,蔚来公司有一个研发项目,按照行业标准是 3000 万的项目花费,内部申报立项的时候预算是 2000 万,当时内部评估还挺省,不过还是被李斌否定了立项,要求省了还要省,最终这个研发项目的花费是 200 万,是原来预期的十分之一。

甚至还有以「元」为单位的成本减少:原先蔚来门店的矿泉水是依云,后来和中石化合作,换成了中石化搞副业的高原雪山矿泉水,一瓶也能省几块钱。

李斌说:

我们现在整个公司变成了这么一群人,就是以算账为乐趣。

越来越多的以百元,以万元的算账之后,就能够撬动上亿和上十亿的杠杆了。

在全新 ES8 发布的时候,李斌曾表示,全新 ES8 的成本和售价之所以能够大幅度降低,是因为之前在研发上的投入到了回报期,时间和规模摊薄了成本。在回答新阶段将会有怎样的研发投入节奏时,李斌说:

我们现在保持每个季度 20-25 亿的研发投入,但是现在我们 100 亿的研发支出,应该相当于以前的 140 亿到 150 亿,也就是我们至少提了 1/3。这里面 60% 以上的投入还是在基座能力的建设,40% 不到的部分会放到一些应用层,比如新车型新产品上去,这是蔚来研发的很大的变化,所以我们还是会保持长期的竞争力。

 

我们对做太多的新产品这件事会克制一些,不是做得越多越好,重要的是把产品做对。

2026 年对于消费电子行业,尤其是 PC 和手机行业来说会是一个极为艰难的年份,因为 AI 基建对内存和存储的需求极为巨大,供远远小于求的情况下,内存和存储的价格会大幅上涨。汽车行业的出货量虽然比 PC 和手机要小一两个数量级,是次要的受冲击行业,但也会面临成本上涨的风险。

▲ 乐道 L80 谍照

李斌也表示,对于行业来讲,内存存储芯片的涨价,是今年比较大的一个成本压力。不仅仅是内存存储和芯片的成本在涨,包括铜和锂的原材料成本也在涨,李斌估算,两者涨价分别会对高端智能电动汽车造成 3000–5000 元的成本增加,总计就是 6000–10000 元的成本上涨。不过,蔚来乐道和萤火虫三个品牌的价格体系能够支撑蔚来应对成本上涨。

李斌说:

反过来讲,就是如果内存存储芯片不涨这个价,我们今年应该能多赚一些钱,等于在经营目标里面,我们已经把这部分涨价的因素考虑进去了。

相比于手机和 PC 行业的涨声一片,蔚来目前能够做的就是不把成本上涨转嫁给消费者,保障价格体系的稳定。

一定程度上,神玑芯片的成功量产上车也可以规避一些成本上涨风险,对核心供应链的把控越深,被供应链波动影响的概率就越小。

并且,神玑的第二款芯片在前几个月已经流片成功,正在进入量产阶段,并且成本相比于第一款神玑 NX9031 要降低 1/3 到 1/2 左右。

有意思的是,过往类似采访里大家会重点问到换电路线的合理性,以及换电体系的必要性,经过李斌和蔚来多年的基建投入以及反复解释,起码在媒体采访中,已经少了很多对换电路线的质疑。

唯一的一两次提及还是跟前不久比亚迪推出的闪充技术,比亚迪表示,最新的闪充技术在常温下从 10% 充至 70% 仅需 5 分钟,充至 97% 也不过 9 分钟;即便是处于零下 30℃的极寒环境,也仅仅比常温多花 3 分钟。

对于比亚迪闪充和蔚来换电的对比,李斌表示:

我觉得充电和换电就不要对立起来,它们俩解决的问题是不一样的。我们蔚来也布了 28000 多根充电桩了,超充桩和目的地桩我们的车都是能充电的,不是不能充电的。

 

比亚迪闪充出来之后又说蔚来换电(要完)的,每一次有个电池技术,或者充电技术出来,标题就是什么《固态电池来了,蔚来换电完蛋》,这次是《比亚迪闪充来了,蔚来换电完蛋》。我们才卖 30 多万辆车,盯着我们那点车干嘛,我也不知道为啥,就是经常躺枪。

 

但是我这两天也看到了,非常感谢比亚迪的闪充,把我们换电的热度也搞高了。充电的技术进步,对蔚来,对整个行业都是有好处的。

 

换电能够解决充电解决不了的问题,比如车电不同寿问题,比如能源效率的问题,运营安全性的问题。

稳中向好。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

1000 块/年的输入法,我用它习惯了「口喷」,再也回不去打字了 | AI 器物志

作者 苏伟鸿
2026年2月21日 18:34

编者按:
当 AI 开始寻找自己的形状,有些选择出人意料。
AI 在智能手机上生出了一颗独立按键,似乎让智能手机找回了久违的进化动力。眼镜凭借着视觉和听觉的天然入口,隐隐有了下一代个人终端的影子。一些小而专注的设备,在某些瞬间似乎比 All in one 的设备更为可靠。与此同时,那些寄望一次性替代手机的激进尝试,却遭遇了现实的冷遇。
技术的落地,从来不只是功能的堆叠,更关乎人的习惯、场景的契合,以及对「好用」的重新定义。
爱范儿推出「AI 器物志」栏目,想和你一起观察:AI 如何改变硬件设计,如何重塑人机交互,以及更重要的——AI 将以怎样的形态进入我们的日常生活?

我很难用熟悉的软件分类去安放 Typeless。

它跟传统输入法格格不入——界面里几乎看不到键盘,最显眼的是一个语音按钮。它也跟那些自称「AI 加持」的输入法不太像,那些产品总喜欢把功能铺满首页,Typeless 的功能反而少得可怜,像是故意把选择题删成了一道填空题。

这份不合群带来一个关键词:越界。

输入法原本服务人与人沟通,目标清晰——打字更快,选词更准。Typeless 把边界往外推了一步,它更在意把自然语言说出的需求梳理得井井有条。它把语言提炼成想法,或者说,它从一段话里捞出真正的意图,再把意图写成一段能直接用的文字。

输入的对象变了。不只是写给人,更多是写给模型。

一款会思考的输入法

我第一次意识到它「会思考」,是在最普通的口述里。

说话时会绕,会补充,会重复,也会用很多填充词。Typeless 的输出更像想清楚之后才落笔的版本——句子更短,信息更集中,语气更收敛。它不执着把说过的每一个音节都留下来,更在意到底想表达什么。

▲ 口述内容被 Typeless 转写后

临时改主意时,差异更明显。传统听写会把自我修正一股脑堆在屏幕上,留下许多中间态。Typeless 更像把中间态折起来,只把最后那个「定稿」留下。屏幕上出现的不是过程,是结果。

需要把一段想法拆成条目时,用普通输入法得先说完再自己排版。Typeless 往往会主动把结构摆出来,逻辑顺序更清楚,段落边界更干净。它像是随手把笔记整理了一遍。

「边说边改」是另一种用法。说完一段话,接着补一句改写要求——更克制、更正式、更短,或者把语气改成邮件——它会在原文上直接调整。不需要停下来选字、删句、重写开头,只要继续说出修改意图。

翻译也是高频场景。需要中英来回切换时,它把翻译变成输入动作的一部分。更省心的是语气处理,它不会把句子翻得像说明书,整体更接近日常沟通。

在办公室或通勤场景里不方便大声说话?它提供了小声输入一类的模式。语音输入过去常被「场合」限制,这类适配决定了它能不能真的用起来,而不是只在安静房间里表现良好。

常用表达也能做成快捷方式——一段固定格式的确认信息,一段常用的工作回复。Typeless 更像把这些东西做成可调用的块,减少重复劳动。输入法从「敲字」变成「调度」。

这些体验汇总到一个点上:Typeless 一直在 Thinking。它把杂乱的口语消化掉,再把更有条理的文字吐出来。它不追求完整复刻说话的全过程,它在整理真正的想法。

这是它最不一样的地方。

AI 器物的新物种

在讨论 AI 产品时,我们更习惯看到的是软硬结合的新尝试——智能眼镜、AI 耳机、豆包手机,它们在新场景里重新定义硬件的形态和交互方式。Typeless 走的是另一条路。

它是纯软件工具,但本质上仍然是硬件的延伸。

从打字机到键盘,再到输入法,这条线索一直存在。打字机把手写变成了机械敲击,键盘把机械敲击变成了电信号,输入法把电信号变成了字符选择。每一次演进,都是在人与文字之间增加一层更高效的转译机制。

Typeless 延续了这个逻辑,但加入了一个新元素——AI 不再只是辅助选字或纠错,它成为输入链路的核心。

传统输入法关心的是「把字打出来」,效率体现在敲击次数、选词准确率、响应速度。到了模型时代,真正消耗时间的往往不是第一次把需求说清楚,而是后续的反复修改。一次改动里夹着大量细节——语气、结构、删改尺度、信息顺序,每一项都需要来回拉扯。人工沟通的成本会在这一步迅速膨胀。

Typeless 解决的就是这段拉扯。

它让「说一句—改一下—再说一句—再改一下」变得顺滑,五到十分钟内把十轮调整连续做完。每一轮都能直接看到结果,马上继续下一轮。输入不再以「把字符敲完」为终点,而是以「文本进入可继续加工的状态」为终点。

这里出现了一个新的「精准输入」。

打字机和键盘诞生时,精准指向的是某个字、某句话。AI 时代的输入变长了,上下文变厚了,沟通频次也变高了。现在的精准更像针对一段超长上下文的控制:按想要的方式分段,或者连写;把某一句压短,或者把某一段扩写;要求它不要分点,或者把逻辑拆成几条。

控制对象变了,输入法的职责也随之变化。

这也是「给 AI 用的输入法」的含义。

▲ Prompt 由 Typeless 转写而成

Typeless 的重点不在社交表达的情绪张力,它更适合把需求交给模型,再把模型产出收拢成能用的文本。它强化的是人与 AI 的沟通效率。商业模式也很符合这种取向——界面极简,没有广告位,付费方式更像「为结果付费」。订阅用户不限量,非订阅用户每周有固定额度。产品用得越多,价值越容易被衡量。

把它放回国内输入法的语境,对比会更清晰。

老派输入法以搜狗为代表,今天也能加上「AI」二字,也能提供一堆 AI 功能。但它依旧像原来的产品——键盘还在,广告和功能标签也还在。输入法被迫承担太多与输入无关的任务,效率容易被稀释。

▲ 搜狗 AI 输入法

另一类是 AI 工具的延伸,比如豆包或微信输入法,它们更像把既有的 AI 能力塞进键盘里,做成一个入口。入口当然有用,但入口并不等于工具。入口解决的是「去哪里用 AI」,Typeless 更关心「怎样把 AI 用得更精确」。

▲ 左边为豆包输入法听写,右边为 Typeless 听写

真正的 AI 输入法,服务的对象变了。它主要服务与模型的高频沟通,服务长上下文里的精确控制,服务反复修改直到结果落地。它不需要把自己做成一个热闹的广场,它只要把那条最难的链路打通。

它也有副作用。用它跟同事沟通时,偶尔会显得过于干净,像把语气里的缓冲都删掉了。对方会觉得不够有人味。会在这种场景里切回普通输入法,手动敲几句更口语的句子,补一个表情,或者加一段无意义的笑声。这不是 Typeless 的问题,而是它的真实位置——它最自然的场景是与 AI 沟通,不是与人闲聊。

▲ 给同事发显得有点「人机」感

输入法向来是残酷的赛道。到处都能用,也意味着到处都会被挑剔。每一次卡顿、每一次误判、每一次隐私疑虑,都会直接影响它能否留下来。Typeless 要证明的不是「模型有多强」,而是「日常输入是否真的变快、变准、变省心」。

当人与 AI 的沟通变得日常,输入法可能会成为最隐蔽、也最核心的接口。它要做的不是替用户写完一切,而是把说出的信息整理成更可控、更可迭代的文本,让「多轮修改」从一种负担变成一种自然动作。

这类产品最终能不能站住脚,取决于两件事:一是它能否在所有细碎场景里保持稳定,二是它能否让「为结果付费」变得理所当然。

输入层向来没有中间地带——要么融入习惯,要么被迅速替换。Typeless 作为 AI 产品演进史上的一个新节点,把自己定位在了那条更窄、也更陡的路上。

One more thing: 我们是怎么用嘴「喷」出一篇文章的

上面的这些文字,以及下面的部分文字,我们全程只动了嘴皮子,指挥 Typeless、ChatGPT、Claude 等工具完成,没有手打一个字。

按照以往,要写一篇这样的文章,最少也得花上 2 个小时,现在只用了 30 分钟。

先介绍一下这个产品的具体细节。Typeless App 支持手机端的 iOS 和 Android,以及电脑端的 Windows 和 Mac。

免费方案提供每周 4000 字转写;而付费没有字数的限制,每个月 30 美元,每个季度 60 美元,一年 144 美元。

这个价格并不便宜,但它很符合 AI 时代「付费交货」的结果导向模式,即使是免费用户,也不会遇到广告和太多限制,最主要的差距仅限转写字数。

其实 Typeless 不太像一个「输入法」,它完全没有传统的键盘,只有少数几个按键,更不用提什么 AI 斗图、表情包的功能,只做好「语音转文字」的本职工作。

我很喜欢 Typeless 的在设备上全局的集成形式——手机上是输入法,电脑上是热键,让它可以像 AI 助手一般跨应用使用,这是 ChatGPT 无法给出的细节体悟。

整个过程还挺有意思,一开始我们只是想测试用 Typeless 和 ChatGPT 进行写稿的过程,但随着一轮一轮的对话深入,稿子不断打磨,最终出来了一篇观点明确的文章,不仅行文流畅,AI 味也很少。

一开始,我们先抛出了一些初步的想法,关于 Typeless 这个产品的一些观点,以及资料收集和写作注意事项,这些「意识流」的口述被 Typeless 整理成条理清晰的文字,直接用作 ChatGPT 的提示词。

ChatGPT 给出的第一版稿件没啥信息量,结构也不正确,语言平铺直叙还很有 AI 味,距离一篇好看的文章还有不小距离。换做平时,想要给细致的修改建议,不免得要花大量的笔墨给出新的提示词。

▲ Prompt 由 Typeless 转写

但现在我们有 Typeless,只要把听写打开,我们可以从头到尾一句一句提修改意见,并根据文段补充相应的观点和叙述。

▲ Prompt 由 Typeless 转写

我们需要尽可能给出细节,比如对比 Typeless 和搜狗、豆包、微信输入法区别的部分,就需要强调这几种产品的差异,AI 在写作时才能凸显 Typeless 的优势。

▲ Prompt 由 Typeless 转写

经过几轮的修改,ChatGPT 生成的内容已经相对完善,这时候我们可以换用 Claude 进行润色。

我们首先给 Claude 喂了几篇爱范儿写过的 AI 新硬件文章,让它充分学习我们行文的风格,据此来修改 ChatGPT 的草稿。

Claude 的初稿也还有提升空间,这时候我们可以继续用 Typeless 帮我们转述一些相对更细节的修改建议,直到满意为止。

▲ Prompt 由 Typeless 转写

其实我们对着 Typeless 侃侃而谈的文本量,累计可能已经比最终的成稿还要大,但出稿的效率大大提升,并且过程要比单纯写作更加轻松。

AI 时代,Typeless 应该「无处不在」

一开始试用 Typeless 的时候,作为一个不太习惯用语言来梳理想法和表达自己,也不需要长篇大论去表达想法的人,我会觉得它不适合我,更适合天天需要给出大量反馈的领导、Mentor、甲方人群。

但进一步探索使用之后,我觉得我还是狭隘了。在这个 AI 时代下,Typeless 不应该只是一个独立的 App,更应该成为一种「标配」无处不在。

从小处说,「语音转文字」,远远不能停留在「准」,在 AI 时代下更应该追求「精」。以后发语音转文字就全是精炼的信息,而不是满屏的「呃」「那个」以及口误。

▲ 42 秒的语音有用信息只有 10 个字

比起给爸妈手机装一个 Typeless,我更希望类似的功能直接集成到微信中——或者说,所有应用内置的「语音转文字」功能,都值得以 Typeless 的方式重做一遍。

更大的价值,在于 Typeless 给 AI 交互提供了一种新的可能。

哪怕是每天都在写稿,我的表达能力经常追不上自己的想法。甚至不是写稿,只是用键盘和 ChatGPT 对话,很多时候火花在敲击字母的时候,就已经熄灭。

改成开口说话,事情会轻松很多。我不必先想好结构,也不用马上挑最精确的词,语言会先把材料「拽」出来,观点和洞察会更自然而然流淌。

这就像在现场指导一个实习生做修改,指令可以很细,细到每一句话怎么落地——是的,我们每个人都有了 AI 作为「乙方」。

指望「一句话」让 AI 生成一切,基本不现实,信息密度太低,AI 很容易离题,素材又撑不起来,于是成品常常空、泛、虚,表面上写完了,读起来却像没落过笔。

对于 AI 来说,「上下文」很大程度决定了生成的质量,我们必须要给模型「喂」大量的想法、观点和语料,才能得到更符合预期的结果。为什么这两年内存价格大涨?要运行和训练 AI,超大的上下文必不可少,于是 AI 行业产生了对内存的巨大需求。

用 Typeless 的体验,更像是在给 AI 喂一份更丰富的语料,生成的内容有据可依,观点也够牢靠,AI 更多只是负责把这些碎片变成更好读的文章。

所以,不仅微信可以集成类似 Typeless 的功能,所有的 AI 公司,完全可以把这种「AI 翻译层」集成在聊天机器人之中,引导用户把提示词往多了说。

而只要用户给 AI 注入的内容够多,AI 模型能力的差距,也会被进一步缩小。

▲ 用 Typeless 转写的超长 Prompt

或许有人会对 Typeless-ChatGPT 这套解决方案有点悲观,这岂不是意味着,人类创作真的会彻底在 AI 时代消亡?

是,但又不全是,Typeless 只能消除「写作」这件事的成本和门槛,但却进一步凸显出「思想」的重要,让人类的感悟、观点、洞察变成了写作真正的核心。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


macOS 录屏软件开发实录:从像素抓取到元数据重现

作者 Fatbobman
2026年2月4日 22:12

视频正在取代文字成为主流的表达方式,而好工具是创作的加速器。macOS 录屏软件 ScreenSage Pro 的独立开发者 Sintone 分享了从像素抓取到元数据重现的全过程。从屏幕录制、元数据捕获,到高性能视频合成,他详述了开发中的挑战与解决方案。

❌
❌