普通视图

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

案例分析:从“慢”到“快”,一个后台管理页面的优化全记录

作者 wuhen_n
2026年3月28日 06:52

前言

想象我们是一个电商平台的运营人员,每天要处理几百个订单,需要在后台管理系统里查订单、看统计、导出数据。早上9点,我们打开订单管理页面:

  • 等了3秒,页面才显示
  • 输入搜索关键词,打字都卡
  • 切换标签页,又等2秒
  • 导出数据,页面直接假死

初始状态 - 一个典型的“慢”页面

业务背景

某电商平台的后台管理系统,订单管理页面。功能包括:

// 这个页面有这些功能
const orderPage = {
  // 订单列表 - 2000条数据,12列
  orderTable: {
    rows: 2000,
    columns: 12
  },
  
  // 统计图表 - 3个图表
  statsCharts: ['日订单趋势', '品类分布', '收入趋势'],
  
  // 筛选表单 - 15个筛选项
  filters: ['日期范围', '订单状态', '销售渠道', '地区', ...],
  
  // 多标签页 - 5个标签
  tabs: ['所有订单', '待处理', '已发货', '已完成', '已取消']
}

初始性能指标

指标 测量值 行业标准 评级
FCP(首次内容绘制) 3.2秒 < 1.8秒
LCP(最大内容绘制) 4.5秒 < 2.5秒
TTI(可交互时间) 5.8秒 < 3.8秒
CLS(布局偏移) 0.25 < 0.1

问题代码(简化版)

<!-- ❌ 问题代码:订单管理页面 -->
<template>
  <div class="order-management">
    <!-- 统计卡片 -->
    <div class="stats-cards">
      <div v-for="stat in stats" :key="stat.key">
        {{ stat.label }}: {{ stat.value }}
      </div>
    </div>
    
    <!-- 筛选表单(15个筛选项) -->
    <div class="filters">
      <el-form :model="filters" inline>
        <el-form-item label="日期范围">
          <el-date-picker v-model="filters.dateRange" />
        </el-form-item>
        <el-form-item label="订单状态">
          <el-select v-model="filters.status" multiple />
        </el-form-item>
        <!-- ... 还有13个筛选项 -->
        <el-button @click="search">搜索</el-button>
      </el-form>
    </div>
    
    <!-- 订单表格(2000行数据) -->
    <el-table :data="orders" border stripe>
      <el-table-column prop="id" label="订单号" />
      <el-table-column prop="date" label="日期" />
      <el-table-column prop="customer" label="客户" />
      <!-- ... 还有9列 -->
    </el-table>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'

const orders = ref([])      // 2000条数据
const filters = ref({})     // 15个筛选项

// 加载订单
async function loadOrders() {
  const res = await api.getOrders(filters.value)
  orders.value = res.data  // 2000条
}

// 搜索
function search() {
  loadOrders()
}

// 监听筛选变化(性能杀手!)
watch(filters, () => {
  search()  // 每次筛选变化都请求
}, { deep: true })  // 深度监听15个字段

onMounted(() => {
  loadOrders()
})
</script>

网络层优化 - 减少等待时间

问题:请求太多太慢

// 优化前:4个请求串行执行
async function loadPageData() {
  await loadOrders()   // 请求1,耗时500ms
  await loadStats()    // 请求2,耗时400ms
  await loadCharts()   // 请求3,耗时300ms
  await loadFilters()  // 请求4,耗时200ms
  // 总耗时:1.4秒
}

解决方案:并行请求

// ✅ 优化后:4个请求并行执行
async function loadPageData() {
  const [orders, stats, charts, filters] = await Promise.all([
    api.getOrders(params),
    api.getStats(params),
    api.getCharts(params),
    api.getFilters(params)
  ])
  // 总耗时:500ms(取最长的那个)
  
  updatePageData({ orders, stats, charts, filters })
}

缓存策略

// ✅ 添加缓存,避免重复请求
class APICache {
  constructor() {
    this.cache = new Map()
  }
  
  async get(key, fetcher, ttl = 300000) {  // 默认5分钟
    const cached = this.cache.get(key)
    if (cached && Date.now() - cached.time < ttl) {
      return cached.data  // 命中缓存,直接返回
    }
    
    const data = await fetcher()  // 请求新数据
    this.cache.set(key, { data, time: Date.now() })
    return data
  }
}

const cache = new APICache()

// 使用
async function getOrders(params) {
  const key = `orders:${JSON.stringify(params)}`
  return cache.get(key, () => fetch('/api/orders', { params }))
}

构建层优化 - 减少代码体积

问题:代码太大

优化前:打包体积:
index.js: 2.8MB  ← 太大了!
vendor.js: 1.2MB
total: 4.0MB

解决方案:路由懒加载

// ✅ 优化后:按需加载
const routes = [
  {
    path: '/orders',
    // 只有访问订单页面时才加载这个文件
    component: () => import('@/views/Orders.vue')
  }
]

// 打包结果
orders.js: 180KB  ← 只有订单页的代码
vendor.js: 800KB
total: 1.0MB

按需引入 UI 库

// ❌ 优化前:全量引入 Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)  // 增加 1.2MB

// ✅ 优化后:按需引入
import { ElButton, ElTable, ElSelect } from 'element-plus'
import 'element-plus/theme-chalk/el-button.css'
import 'element-plus/theme-chalk/el-table.css'
// 只引入用到的组件,体积减少 800KB

渲染层优化 - 让页面更流畅

问题:表格渲染2000行

// 优化前:一次性渲染2000行
<el-table :data="orders">  // orders有2000条
  <!-- 2000个DOM节点,页面卡顿 -->
</el-table>

解决方案:虚拟滚动

<!-- ✅ 优化后:只渲染可视区域 -->
<template>
  <RecycleScroller
    :items="orders"
    :item-size="50"
    class="table-body"
  >
    <template #default="{ item }">
      <div class="table-row">
        <div>{{ item.id }}</div>
        <div>{{ item.date }}</div>
        <div>{{ item.customer }}</div>
        <!-- ... -->
      </div>
    </template>
  </RecycleScroller>
</template>

keep-alive 缓存

<!-- App.vue -->
<template>
  <router-view v-slot="{ Component, route }">
    <!-- 缓存已访问的页面 -->
    <keep-alive :include="cachedViews">
      <component :is="Component" :key="route.fullPath" />
    </keep-alive>
  </router-view>
</template>

运行时优化 - 让交互更跟手

问题:深度监听导致频繁请求

// ❌ 优化前:每次打字都触发请求
watch(filters, () => {
  search()  // 用户输入一个字母就请求一次
}, { deep: true })  // 深度监听15个字段

解决方案:防抖

import { debounce } from 'lodash-es'

// ✅ 优化后:用户停止输入300ms后才请求
const search = debounce(async () => {
  const res = await api.getOrders(filters.value)
  orders.value = res.data
}, 300)

导出数据不卡顿

// ❌ 优化前:导出时页面假死
async function exportOrders() {
  const data = await api.getOrders({ pageSize: 10000 })
  const excel = convertToExcel(data)  // 处理1万条数据,阻塞UI 3秒
  download(excel)
}

// ✅ 优化后:使用 Web Worker
// worker.js
self.addEventListener('message', (e) => {
  const excel = convertToExcel(e.data)  // 在另一个线程处理
  self.postMessage(excel)
})

// 主线程
async function exportOrders() {
  const data = await api.getOrders({ pageSize: 10000 })
  worker.postMessage(data)  // 发送到 Worker
  worker.onmessage = (e) => {
    download(e.data)  // 收到结果,下载文件
  }
}

优化检查清单

网络层

  • 请求合并(Promise.all)
  • API 数据缓存
  • 静态资源缓存

构建层

  • 路由懒加载
  • UI库按需引入
  • 图片压缩(WebP/AVIF)

渲染层

  • 虚拟滚动(长列表)
  • keep-alive 缓存页面
  • v-memo / v-once

运行时

  • 防抖节流
  • Web Worker 处理复杂计算
  • computed 缓存计算结果

优先级排序

高收益/低成本(立即做):
├─ 路由懒加载(30分钟,收益60%)
├─ 图片压缩(15分钟,收益75%)
├─ 防抖节流(10分钟,收益50%)
└─ 按需引入UI库(1小时,收益40%)

中收益/中成本(计划做):
├─ 虚拟滚动(2小时,收益50%)
├─ 数据缓存(1.5小时,收益35%)
└─ Web Worker(3小时,收益25%)

低收益/高成本(谨慎做):
├─ 完全重写组件(2天,收益10%)
└─ 替换UI框架(3天,收益5%)

核心原则

  • 先测量后优化:用数据说话
  • 渐进式优化:先做收益高的
  • 持续监控:防止性能回退
  • 用户体验优先:用户觉得快才是真的快

结语

当我们看到一个页面从 5秒加载变成 1秒,用户从抱怨变成点赞,我们就会知道这些优化值了!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

SwiftUI 如何实现 Infinite Scroll?

作者 RickeyBoy
2026年3月28日 00:56

欢迎点个 star:github.com/RickeyBoy/R…

面试题:用 SwiftUI 实现一个无限滚动列表,支持分页加载。

这道题我在面试中遇到过好几次,说实话第一次答的时候以为随便写个 LazyVStack + onAppear 就完事了。后来才发现,面试官真正想考的不是你会不会用 API,而是你对状态管理、性能优化、Task 生命周期这些东西到底理解多深。

我的思路是从最简方案出发,一步步暴露问题、一步步优化。在开始写代码之前,先聊一下架构选型。

为什么选 MVVM?

先说一下 SwiftUI 里常见的架构选择。MVC 就不聊了,那是 UIKit 时代的标配,Controller 跟 UIKit 强耦合,到了 SwiftUI 里根本没有 UIViewController 这个角色,MVC 自然也就退出舞台了。

SwiftUI 里最常见的架构,从简单到复杂大概是这么几个:

架构 特点 适合场景
MV(Model-View) 没有 ViewModel,状态直接放 View 里,Apple 官方示例的典型写法 逻辑简单的页面
MVVM 抽出 ViewModel 管理状态和逻辑,SwiftUI 里最主流的选择 中等复杂度,需要可测试性
TCA 单向数据流,State + Action + Reducer + Effect,强约束 大型项目,需要严格的状态管理

其中 MV 是最基础的,逻辑简单的页面,@State 往 View 里一放就完事了,Apple 自己的 WWDC 示例大量都是这么写的。但 infinite scroll 涉及分页状态、加载状态、错误处理、Task 生命周期管理这些东西,全塞 View 里会很乱。抽一个 ViewModel 出来专门管理这些状态,View 只负责渲染和转发用户操作,职责就清晰多了。

所以这道题用 MVVM 是最合适的,不是因为 MVVM 最好,而是这个场景的复杂度刚好适合。并且采用 MVVM 结构规整,可拓展性也强,从面试回答的角度来讲也是正好的。

而 SwiftUI 天然就鼓励这种模式,@Observable 本身就是 binding 机制,ViewModel 状态一变,View 自动更新,不需要手动同步。我们后面的代码就是按这个思路来的。

一、最小可用版本

先写一个能跑的最简版本。

核心思路很简单:LazyVStack 只在 item 即将可见时才实例化 View,我们利用 onAppear 检测"最后一个 item 出现了",然后触发下一页请求。

Model

struct Item: Identifiable, Equatable {
    let id: String
    let title: String
}

struct PageInfo {
    let endCursor: String?
    let hasNextPage: Bool
}

ViewModel

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private var pageInfo: PageInfo?

    func loadNextPage() async {
        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
        guard let response else { return }
        items.append(contentsOf: response.items)
        pageInfo = response.pageInfo
    }
}

View

struct ItemListView: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(viewModel.items) { item in
                    ItemRow(item: item)
                        .onAppear {
                            if item == viewModel.items.last {
                                Task { await viewModel.loadNextPage() }
                            }
                        }
                }
            }
        }
        .task { await viewModel.loadNextPage() }
    }
}

代码很短,逻辑也直白:

  1. 每当最后一个 item 出现在屏幕上,就触发 loadNextPage()
  2. loadNextPage() 请求后台去 fetch,拿到数据然后塞进 items 中
  3. View 检测到有更新,自动刷新页面

一句话总结:最后一个 item onAppear 的时候,就进行请求。

1.1 分页方式:cursor vs offset

可能有同学会问:为什么 fetchItems(after: cursor) 用的是 cursor,而不是传统的 pageoffset

分页一般有两种方式:

  • Offset-basedfetchItems(page: 3, size: 20),按页码或偏移量取数据
  • Cursor-basedfetchItems(after: "abc123"),传上一页最后一条的标识,从那里往后取

对于 infinite scroll 这种场景,cursor-based 更合适。详细对比一下:

Cursor-based Offset-based
数据一致性 不受中间插入/删除影响 插入新数据会导致重复或遗漏
性能 数据库只需定位到 cursor 后续 大 offset 需要 skip N 行
适用场景 实时 feed、社交流 固定数据集、后台管理列表

简单来说,cursor-based 更适合"数据随时在变"的场景(比如社交 feed),offset-based 更适合"数据基本不变"的场景(比如后台管理列表)。infinite scroll 的数据通常是动态的,所以用 cursor-based。

1.2 LazyVStack vs List

可能有同学会问:为什么用 LazyVStack 而不是 List

先说浅显的回答:LazyVStack 布局更自由,没有 List 自带的分割线、背景色、cell 样式这些限制,适合高度自定义的 UI。而 List 开箱即用,自带滑动删除、拖拽排序这些交互,适合标准列表场景。

当然,如果想要深入回答,还有可以继续。二者还有一个关键区别其实是内存模型

LazyVStack List
View 回收 ❌ 不回收,创建后常驻内存 ✅ 内部回收机制
内存增长 随滚动距离线性增长 基本恒定
自定义布局 完全自由 受限于 List 样式
万级数据 可能有内存压力 表现更好

为什么会有这个区别?因为它们底层的实现不一样。List 底层是基于 UICollectionView(iOS 16 之前是 UITableView),天然有 cell 回收复用机制,滚出屏幕的 cell 会被回收,滚入时再复用,所以内存占用基本恒定。而 LazyVStack 底层只是一个普通的布局容器,"Lazy" 的意思是延迟创建,item 滚入可见区域时才创建 View,但创建之后就一直留在内存里,不会回收。

所以如果列表数据量很大(比如社交 feed 那种上万条的),List 在内存上更有优势。如果需要高度自定义的 UI,那就用 LazyVStack,但要心里有数:用户滚得越远,内存占用越大。

1.3 为什么加 @MainActor

上面的 ViewModel 代码加了 @MainActor,这个很容易被忽略但其实很关键。

@Observable 本身不会自动保证在主线程更新状态。而我们的 loadNextPage() 是在 Task 里通过 await 拿数据,await 之后的代码在哪个线程执行是不确定的。如果恰好在后台线程执行了 items.append(...),SwiftUI 收到状态变更通知后会在后台线程刷新 UI,这就会导致紫色警告("Publishing changes from background threads is not allowed")甚至崩溃。

加上 @MainActor 之后,这个类的所有属性访问和方法调用都会被隔离到主线程,从根源上避免线程安全问题。

另外补充一下:Swift 6.2(Xcode 26)引入了模块级别的 Default Actor Isolation 设置,可以把整个模块的默认隔离改为 MainActor,开启之后所有类型都默认跑在主线程,不用再手动加 @MainActor。但这是一个 opt-in 的设置,默认值还是 nonisolated,而且不是所有项目都会立刻升级。所以目前来说,显式写 @MainActor 仍然是更稳妥的做法。

1.4 几个小细节

有几个代码细节,不影响功能,但代码质量会好不少,属于面试加分项。

private(set) 控制可见性

itemsprivate(set) 修饰,外部只能读不能写。这样 View 就没法直接改 items,所有数据变更都必须经过 ViewModel 的方法,数据流向是单向的。这个习惯在 MVVM 里很重要,不然 View 和 ViewModel 的职责边界很容易模糊。

让 Item 遵循 Equatable

上面的代码里 Item 已经加了 Equatable,所以判断"是不是最后一个"可以直接写 item == viewModel.items.last,不用绕一圈去比 id。后面加叠加更多功能的时候也可以用,代码更简洁。

.task = .onAppear + Task

View 里首次加载用的是 .task { await viewModel.loadNextPage() },这其实等价于在 .onAppear 里手动创建一个 Task。但 .task 有个好处:当 View 消失时会自动 cancel 这个 Task。手动写 Task {} 的话你得自己管 cancel,容易漏掉,所以首次加载优先用 .task

二、防重复请求

上一个最基础的版本,有个明显的问题:快速滚动时 onAppear 有可能会被多次触发,导致同一页被重复请求了。

怎么解决?思路也很直接:加一个 isLoading 标记 + hasNextPage 判断,双重 guard,然后通过这些属性来判断是否需要发送请求。

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private var pageInfo: PageInfo?

    var canLoadMore: Bool {
        guard let pageInfo else { return items.isEmpty } // 首次加载
        return pageInfo.hasNextPage && !isLoading
    }

    func loadNextPage() async {
        guard canLoadMore else { return }
        isLoading = true
        defer { isLoading = false }

        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
        guard let response else { return }
        items.append(contentsOf: response.items)
        pageInfo = response.pageInfo
    }
}

canLoadMore 这个 computed property 干了两件事:

  • 没有下一页时不请求(通过后端返回的 pageInfo.hasNextPage 来判断)
  • 正在加载时不重复请求(通过 isLoading 来判断)

2.1 小细节

defer 管理状态翻转

注意 isLoading 的写法:开头设为 true,然后紧接着 defer { isLoading = false }。这样不管后面是正常返回还是提前 returnisLoading 都会被重置回 false

如果不用 defer,你就得在每个 return 之前手动加一句 isLoading = false,路径一多很容易漏掉,漏掉的后果就是列表永远卡在 loading 状态,再也加载不了下一页。

canLoadMore 作为 computed property

把"能不能加载"的判断收到一个 computed property 里,而不是在 loadNextPage() 里写一堆 if。好处是逻辑集中,后面要加新条件(比如错误状态下不加载)直接改这一个地方就行,调用方不用动。

三、提前预加载:Threshold Prefetch

目前的逻辑是"最后一个 item 出现了才开始加载",那么用户的感受就是:滚到底 → 停顿 → 等数据 → 新数据出现。那个停顿虽然可能只有几百毫秒,但体感上还是挺明显的。

怎么办?提前触发。 不等最后一个 item,而是在还剩 N 个 item 时就开始加载下一页。

// View
ForEach(viewModel.items) { item in
    ItemRow(item: item)
        .onAppear { viewModel.onItemAppear(item) } // View 层仅透传,将逻辑交给 ViewModel
}

// ViewModel,新增 prefetch threshold
private let prefetchThreshold = 5

func onItemAppear(_ item: Item) {
    guard let index = items.firstIndex(of: item),
          index >= items.count - prefetchThreshold else { return } // 判断是否该加载下一页了
    Task { await loadNextPage() }
}

这样一来,用户还剩 5 个 item 可以滚的时候,网络请求就已经在跑了,等滚到底部时,数据大概率已经回来了,体验上就是"无缝衔接"。

那 threshold 到底设多少合适?这个纯属经验值,根据具体的数据量、UI 复杂度都相关,5 只是一个经验值。总的来讲就是一个 trade-off:

  • threshold 太小:快速滚动还是会看到停顿
  • threshold 太大:用户可能只看前几条就走了,白白浪费请求

四、Task 取消 + 错误处理

到这里基本功能已经没问题了。接下来聊聊 Task 生命周期管理和错误恢复,这部分在面试里属于加分项。

4.1 Task 取消

为什么需要管理 Task 取消?我们目前的例子中,单一列表的情况可能不需要考虑。但是如果是搜索页面的列表,或者叠加筛选功能,问题就复杂了。

举个具体的例子:

  1. 用户在搜索页搜"咖啡",然后在列表页向下滑动,触发了一个 loadNextPage 的请求 A
  2. 还没等数据回来,用户改成搜"奶茶",请求 B 又发出去了
  3. 这时候网络上同时有两个请求在跑。如果请求 B 先于 A 回来,那么等请求 A 回来的时候,用户就会发现明明搜索的是“奶茶”,但是却又展示了不少“咖啡”内容。

这种 bug 不是每次都能复现(取决于网络时序),但一旦出现用户会很困惑,所以解决方式就是:发新请求前先 cancel 旧的,被 cancel 的任务即便返回了 response 也不处理

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private(set) var error: Error?
    private var pageInfo: PageInfo?
    private var loadTask: Task<Void, Never>? // 💾 持有当前请求的引用

    func loadNextPage() {
        guard canLoadMore else { return }
        loadTask?.cancel() // ❌ 发新请求前,先 cancel 旧的
        isLoading = true

        loadTask = Task { [weak self] in // 🔒 weak self 防止循环引用
            guard let self else { return }
            defer { self.isLoading = false }

            do {
                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
                guard !Task.isCancelled else { return } // 🛡️ 被 cancel 了就不写入
                self.items.append(contentsOf: response.items)
                self.pageInfo = response.pageInfo
                self.error = nil
            } catch {
                guard !Task.isCancelled else { return } // 🛡️ 同上
                self.error = error
            }
        }
    }

    func reset() {
        loadTask?.cancel() // ❌ 先 cancel,再清空
        items = []
        pageInfo = nil
        isLoading = false
        error = nil
    }

    // ...
}

4.2 错误重试

错误处理其实是一个很容易被忽略,同时也非常复杂的事情。这里我们的方案是当出现错误的时候,展现一个重试按钮。从 UI 的角度来讲不好看,但实际上面试阶段时间有限,能够展示出有错误处理的思维就可以了。

@MainActor @Observable
final class ItemListViewModel {
    // ...
    private(set) var error: Error?

    func retry() {
        error = nil
        loadNextPage()
    }
}
// View — 列表底部
if viewModel.error != nil {
    RetryButton { viewModel.retry() }
} else if viewModel.isLoading {
    ProgressView()
}

4.3 空状态处理

还有一个容易忽略的边界情况:首次加载完成后,后端返回了 0 条数据。

当前的代码里,items 为空有两种可能:一种是"还在加载第一页",另一种是"加载完了但确实没数据"。如果不区分这两种状态,用户看到的就是一片空白,不知道是在等数据还是真的没有内容。

处理方式也很简单,加一个 computed property 判断一下:

var isEmpty: Bool {
    !isLoading && items.isEmpty && error == nil && pageInfo != nil
}

这里的关键是 pageInfo != nil,说明至少请求过一次了(首次加载前 pageInfonil)。四个条件同时满足,才说明"确实没数据"。

View 里对应的处理:

if viewModel.isEmpty {
    ContentUnavailableView("暂无数据", systemImage: "tray")
} else if viewModel.isLoading && viewModel.items.isEmpty {
    ProgressView() // 首次加载中
} else {
    // 正常的列表内容
}

这样用户就能清楚地区分"加载中"和"没有数据"这两种状态了。

4.4 用 enum 收敛 View 状态

到这里你会发现,View 层需要处理的状态越来越多:首次加载中、有数据、空数据、出错。如果全用 if/else if 判断,条件一多很容易写乱,漏掉某个分支也不会有编译器提醒。

可以定义一个 enum 来收敛这些状态:

enum ViewState {
    case initialLoading    // 首次加载中
    case loaded            // 有数据,正常展示列表
    case empty             // 加载完了但没数据
    case error(String)     // 出错了
}

然后在 ViewModel 里加一个 computed property,从现有属性推导出当前的 View 状态:

var viewState: ViewState {
    if let error, items.isEmpty {
        return .error(error.localizedDescription)
    }
    if isLoading && items.isEmpty {
        return .initialLoading
    }
    if isEmpty {
        return .empty
    }
    return .loaded
}

注意这里的关键:ViewState 是 computed property,不是存储属性。底层的数据源还是 isLoadingitemserrorpageInfo 这些独立属性,viewState 只是把它们组合成 View 更容易消费的形式。这样既不会出现之前 LoadingState enum 耦合状态的问题,又让 View 的代码变得很干净:

var body: some View {
    Group {
        switch viewModel.viewState {
        case .initialLoading:
            ProgressView()
        case .empty:
            ContentUnavailableView("暂无数据", systemImage: "tray")
        case .error(let message):
            ErrorView(message: message) { viewModel.retry() }
        case .loaded:
            ScrollView {
                LazyVStack(spacing: 0) {
                    ForEach(viewModel.items) { item in
                        ItemRow(item: item)
                            .onAppear { viewModel.onItemAppear(item) }
                    }
                    loadingFooter
                }
            }
        }
    }
    .task { viewModel.loadNextPage() }
}

switch 替代 if/else if,每个分支对应一种状态,漏掉任何一个编译器都会报错。用 Group 包裹 switch 是为了能在外层挂 .task 触发首次加载。

五、完整代码

前面一步步拆解完了,最后把所有东西整合到一起。先看一下整体架构:

graph LR
    View -->|用户操作| ViewModel
    ViewModel -->|状态更新| View
    ViewModel -->|网络请求| APIService
    APIService -->|响应数据| ViewModel

    style View fill:#E8F5E9,stroke:#4CAF50
    style ViewModel fill:#E3F2FD,stroke:#2196F3
    style APIService fill:#FFF3E0,stroke:#FF9800

View 只管渲染和转发用户操作,ViewModel 管状态和请求编排,APIService 做实际的网络调用。数据流向是单向的:用户操作 → ViewModel 处理 → APIService 请求 → 数据回来更新状态 → View 自动刷新。

Model

struct Item: Identifiable, Equatable {
    let id: String
    let title: String
}

struct PageInfo: Equatable {
    let endCursor: String?
    let hasNextPage: Bool
}

struct PagedResponse {
    let items: [Item]
    let pageInfo: PageInfo
}

ViewState

enum ViewState {
    case initialLoading
    case loaded
    case empty
    case error(String)
}

ViewModel

@MainActor @Observable
final class ItemListViewModel {
    // MARK: - State

    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private(set) var error: Error?

    // MARK: - Private

    private let prefetchThreshold = 5
    private var pageInfo: PageInfo?
    private var loadTask: Task<Void, Never>?

    // MARK: - Computed

    var canLoadMore: Bool {
        guard !isLoading else { return false }
        guard let pageInfo else { return items.isEmpty }
        return pageInfo.hasNextPage
    }

    var isEmpty: Bool {
        !isLoading && items.isEmpty && error == nil && pageInfo != nil
    }

    var viewState: ViewState {
        if let error, items.isEmpty {
            return .error(error.localizedDescription)
        }
        if isLoading && items.isEmpty {
            return .initialLoading
        }
        if isEmpty {
            return .empty
        }
        return .loaded
    }

    // MARK: - Trigger

    func onItemAppear(_ item: Item) {
        guard let index = items.firstIndex(of: item),
              index >= items.count - prefetchThreshold else { return }
        loadNextPage()
    }

    // MARK: - Actions

    func loadNextPage() {
        guard canLoadMore else { return }
        loadTask?.cancel()
        isLoading = true

        loadTask = Task { [weak self] in
            guard let self else { return }
            defer { self.isLoading = false }

            do {
                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
                guard !Task.isCancelled else { return }
                self.items.append(contentsOf: response.items)
                self.pageInfo = response.pageInfo
                self.error = nil
            } catch is CancellationError {
                // Task was cancelled, do nothing
            } catch {
                guard !Task.isCancelled else { return }
                self.error = error
            }
        }
    }

    func retry() {
        error = nil
        loadNextPage()
    }

    func reset() {
        loadTask?.cancel()
        items = []
        pageInfo = nil
        isLoading = false
        error = nil
    }
}

View

struct ItemListView: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        Group {
            switch viewModel.viewState {
            case .initialLoading:
                ProgressView()
            case .empty:
                ContentUnavailableView("暂无数据", systemImage: "tray")
            case .error(let message):
                ErrorView(message: message) { viewModel.retry() }
            case .loaded:
                ScrollView {
                    LazyVStack(spacing: 0) {
                        ForEach(viewModel.items) { item in
                            ItemRow(item: item)
                                .onAppear { viewModel.onItemAppear(item) }
                        }
                        loadingFooter
                    }
                }
            }
        }
        .task { viewModel.loadNextPage() }
    }

    @ViewBuilder
    private var loadingFooter: some View {
        if viewModel.error != nil {
            VStack(spacing: 8) {
                Text("加载失败")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Button("Retry") { viewModel.retry() }
                    .buttonStyle(.bordered)
            }
            .frame(maxWidth: .infinity)
            .padding()
        } else if viewModel.isLoading {
            ProgressView()
                .frame(maxWidth: .infinity)
                .padding()
        }
    }
}

总结

回顾一下整个思路:

  1. 从简单方案说起LazyVStack + onAppear last item,先把原理讲清楚
  2. 暴露问题并优化 — 重复请求 → guard;体验停顿 → threshold prefetch
  3. 展示工程素养 — Task 取消、error handling、retry
  4. 完整架构 — View 只渲染 + 转发,ViewModel 管状态 + 编排

厌倦了那些看着像一个模版复刻出来的抓包工具,我开发了一款iOS端HTTPS抓包调试工具

作者 吴就业
2026年3月27日 22:29

最近的一份工作,因为对业务不熟悉,产品经理出的需求又不考虑历史兼容性,问同事同事也不清楚,作为一个后端开发,我也拿不到客户端的代码,于是我就想到了抓包,通过安装app,抓取某块功能使用了哪些接口。

因为我手机是iPhone, 我因此试用了很多款在app store下载的HTTPS抓包工具,包括免费的Stream、ProxyPin、付费了一款螃蟹抓包。但这些工具感觉都是出自于同一个模版,体验雷同,因为没有别得选择,当时只好忍受。

当时没被满足的一些需求:

1、发现一些图片无法抓取到(我想知道图片用的域名和路径,知道是直接访问云存储,还是用的哪个文件系统服务,这在后端项目中看不出来,因为这个项目的后端也没提供文件上传功能)。

2、JSON无高亮、无搜索功能,也无法对比某个业务参数(比如当商品类型是电子钥匙时、以及商品类型是摄像头时,实际传的参数以及响应的Body有哪些不同的字段)。

3、除体验外,我当时还希望能满足我这个需求:我想把这些接口导入到Apifox,并且基于当前接口和新的迭代需求在此基础上去修改接口,并在团队中共享这份接口。 而当时我只能基于抓取的响应结构,自己在Apifox里面写接口,这耗费了我整整一天时间。

经过那次之后,我决定自己研究写一个,这个HTTPS抓包工具一定把用户体验做好,一定支持抓图片、支持JSON高亮和搜索(甚至是JSON Diff),以及支持自动生成API文档,可以一键导出到Apifox。

2026年1月我开发出来了,这款APP就叫ApiCatcher(因为一开始的目的就是抓API的,所以取名ApiCatcher),所有产品功能皆为原创设计。

能做出来要感谢那些开源项目的,比如ProxyPin,或许是因为开源项目没有盈利,所以体验没做好吧。我似乎也能理解为什么大多数抓包工具长得那么相似了。

我研究了他们的核心抓包功能是如何实现,用了哪些技术,然后自己花两周时间在Claude辅助下用Swift造了一份轮子(就是核心的NIO代理服务器以及SSL握手),在此基础又花两周时间做了优化性能,降低CPU和内存的占用,同时支持抓取大文件请求,避免进程被系统kill掉。我使用SwiftData和文件来存储抓包数据,将请求和响应Body存文件,其它字符串存SwiftData,然后通过边读边写文件来降低对内存的占用,而SwitData则提供更强大的搜索能力,这为产品做查询过滤功能提供了支持,所以ApiCatcher支持非常多的过滤条件。

以下是产品最初几个核心功能的产品设计:

1、极简风格的抓包页面。(我还加了个小创意:正在抓包中的背景是一张蜘蛛网,有一只蜘蛛在上面爬) ApiCatcher | HTTPS抓包工具

2、请求详情内容聚合,便于在手机这种小设备上更好的查看数据,同时减少操作步骤。请求响应的每个部分都是一个卡片,卡片可展开收起。Body可导出和一键复制。Body可展开全屏预览。Body目前支持渲染图片、svg、html、xml和json。 ApiCatcher |请求详情页

3、JSON格式化、高亮、搜索、Diff支持: ApiCatcher | JSON格式化、高亮、搜索、Diff支持

4、接口文档自动生成,以及导出接口文档到Apifox等API调试工具,因为海外用户不用Apifox,所以也支持了Postman和Bruno: ApiCatcher | api导出到Apifox、Postman、Bruno

5、可以抓文件,其实任何HTTP请求都支持,不仅仅是图片,而且没有限制图片大小,多大都能抓,这些图片还可以导出来拿来测试用(一些需要上传特定图片测试的接口):

在这里插入图片描述

经过两个月时间,加上有不少用户给我提需求,于是慢慢功能都完善了。基本app store上的https抓包工具有的功能ApiCatcher都支持了,并且体验更好,像一些正则表达式、脚本都集成AI生成功能提升效率,让用户自己填API Key 。

工具本就是为开发者提升工作效率而开发,所以我们做了支持导入企业内部使用的受信的自签私钥和证书,也可以自己开发一个接收器实时接收抓包流量,实现API扫描分析需求。

这款工具不支持iOS17以下系统,因为用了SwiftData,SwiftData需要17.0以上才支持。整个项目纯SwiftUI开发,核心功能代码用swift-nio等apple官网库。代码高亮则用了WebView+CodeMirror+Highlight.js以及一些插件。这些在app关于我们->开源组件许可都有声明。

ApiCatcherChatTCP这两款网络数据包抓包分析工具都是我自己原创设计、开发的作品,目前两款产品在海外还是不少用户喜欢的,我知道国内大家都喜欢用免费的,比如Stream、ProxyPin、Reqable,但我还是要在各个平台上分享一下的,避免后面被人借鉴反被别人说是我们抄袭,赚不赚钱是次要的,得先证明自己是原创的。

昨天 — 2026年3月27日首页

全球首个多模态创意营销 Claw 来了,好创意比以前更值钱了

作者 Shawn Rain
2026年3月27日 21:45

最近有支叫《霍去病》的 AI 短片让我印象深刻,播放量轻松破亿,逼真得让人以为是重工业大制作。

真相是:3 个人,48 小时,从立项到成片。 核心创作者还不是影视科班出身,人家之前只是一名内容运营。

同样正在大火的《雪山救狐狸》 AI 小短片,已经衍生出了一个「复仇宇宙」,让全民都开始了这场「抽象狂欢」,甚至连不少大 V 都一起玩梗。

如果要用一句话来形容这件事,我只想说:传统影视工业那套「排期三个月、经费三百万」的秩序,正在被悄悄地颠覆着。

现象级的热门特效视频,很可能就藏在你的下一个 AI 工具里。

但别以为小白就能做到这样的程度,一旦你想试试,就会发现,总是被迫在各种网页和软件之间反复横跳:找这个大模型写脚本文案,切到那个工具生成图像,再换一个平台去把图转成视频。它们就像是公司里各自为战的员工,彼此之间毫无默契,记忆不互通。

一旦某个环节的产出偏离了预期,整条流水线就得推倒重来。

最近 OpenClaw 爆火后,已经有网友尝试指挥龙虾来完成影视创作全流程 —— 用 AI 生成素材,导入 Premiere Pro,再让 Agent 自行完成剪辑。从策划到最终成片,除了疯狂消耗 Tokens 之外,没什么问题。

前期策划、脚本写作、素材生成、剪辑、裁切、配音、字幕,AI 全包了。

这的确让人兴奋,但大多人还会茫然,这……是一种新工作流,但具体怎么跑起来?用什么工具?接口在哪?指令怎么写?中间还隔着一条不小的鸿沟。

恰好最近,我留意到常用的一个视频生成工具 Vidu,上线了 ViduClaw 「V 龙」——全球首个多模态创意营销 Claw。虽然此前已有不少 AI 厂商推出了自家的「Claw」,但作为视频模型厂商,而且做得这么完整的,Vidu 是我见到的业内头一个。

APPSO 体验后,我认为尤其是两类人 —— 广告营销和电商从业者,终于能吃上「真香」的龙虾了。

因为目前体验下来,Vidu Claw 完全不是那种需要你不停折腾养虾的「小众自嗨」,而是目前头一个能实际创意落地、直接把最终成片拍在你桌上的「营销全链路」龙虾。

🎁 APSPO 给大家也准备了福利,登录 vidu.cn,输入邀请码:APPSON1,注册即送 500 积分,快一起来体验吧。

单枪匹马,也能「团队级」交付提案

先说说它是什么。

市面上多数的 AI 视频生成器只是单纯的工具,而 Vidu Claw 更像是一个「全能员工」。

它把 Marketing 领域里不同岗位的能力全部打包成一个 Skill,集中赋能给了你的操作终端。

不过,过去那些厉害的 Skill,总是要研究各种复杂的代码部署,一步跑错,整个龙虾可能都嘎了。而 Vidu Claw 开箱即用。直接进入 https://abc.vidu.cn/vidu-claw,登录账号,输入邀请码 XXXX,就完事了。

当然,如果你已经部署了自己的龙虾,只要在龙虾的对话框里输入一行简单的指令:「帮我安装 Skills https://github.com/Saerdna/vidu-skills」,你也能让它即刻入职。

那么,它到底能做什么?

就拿广告行业里最常见的工作来举例:给客户提案。在从业的几年时间里,一个 Campaign 从 Brief 解读、市场与受众分析,到头脑风暴,再到最终的方案包装,每一步都在疯狂消耗人力和时间。尤其在提案阶段,最让人无力的就是「光靠嘴说」。

比如,我曾在广告公司负责过一个护肤品的案子,当时我们定的主题是「自信转身,高光一刻」。我脑子里有着极其清晰的画面:女主角转身时的光影变化、发丝的飘动、配合着鼓点的运镜。

但在现实中,受限于预算和排期,我们不可能为了一个还没中标的提案去实拍一条 Demo,最后只能拿着几张粗糙的分镜草图,指望客户凭空「脑补」出那种高级感。

现在,Vidu Claw 不仅能精准理解这些抽象的创意,还能直接跑出一整套极具质感的视觉方案。

我可以拍好产品后,直接用 Vidu Claw 生成视频静帧图,然告诉它我想要的效果,它就开始干活了。

它不是毛头小子上来就哐哧干活,而是先理解我的需求,写出对应的视频画面脚本,再调用合适的镜头,才正式「开工」。

开工之后,它还会随时给我同步状态。有新进展了再来敲我,不用我干等着。

如果它在「摸鱼」没回你消息,你还可以直接问它进展怎么样了。

很快,一个简短的演示短片就搞定了。

相比于过去需要精雕细琢提示词、在多个平台间来回倒腾的折磨,Vidu Claw 带来的绝不仅仅是生产力的解放。它打破了过去需要策划、美术、导演、后期一整个团队通力合作的壁垒,让所有环节的交付能力浓缩于你一人之手。

现在,哪怕只是我一个人坐在屏幕前,也能端出完整团队级别的专业提案 —— 我一个人,就是一间五脏俱全的创意广告公司。

让每一个独立创作者,都有专业级的底气

如果说 Vidu 过去的能力更专注于电商和广告,那么 Vidu Claw 则是它的一次关键进化:它成为了独立创作者的「超级助理」

还在广告公司摸爬滚打时,遇到瓶颈至少还能拉上几个同事头脑风暴,拼凑出一个相对完美的方案。但自从转做自由职业后,个人的能力再全面,也难免会遇到「双拳难敌四手」的时刻。

不仅仅是我这样的文字工作者,很多单打独斗的自媒体人都有过同样的无力感。

但 Vidu Claw 的到来改变了这种单兵作战的劣势。想做一支降噪耳机的创意短片?现在只需要扔给它几张产品图,加上一句话的描述,它就能包揽爆款脚本、镜头调度以及背景音乐的匹配。

这甚至让我回想起了职业生涯里的一次「意难平」。

曾经,我为一个新锐茶饮品牌构思过一套充满夏日清新感的视频创意。想法很好,但因为没能拿出足够抓人的动态视觉演示,方案显得单薄,最终遗憾落选。

如果把当时的场景放到今天,结果也许会完全不同。

哪怕只有我一个人,只需要把茶饮的物料图和参考人物发给 Vidu Claw,它就会自动梳理出符合夏日清爽氛围的调性,替我写好每一幕的分镜,安排好运镜走向,铺上配乐,直接生成一支可以放在大屏幕上向客户展示的成片。

我想,如果当时有 Vidu Claw 的帮忙,我们也许就不会错失这个机会了。

从「好玩的玩具」,到「好用的生产力」

AI 工具的焦虑,本质上来自两件事:一是「学不完」,二是「用完还是啥活没干」。

各种 Agent 涌现,确实突破了聊天框的桎梏,但高昂的学习成本和依然存在的平台割裂感,并没有真正给打工人减负,反倒催生了一种新型焦虑——我花了三个小时学了这个工具,但我原来要干的活还在那里等着我。

这是把工具当目的,而不是把工具当手段的结果

但在使用 Vidu Claw 时,我完全感受不到这种焦虑。相反,我有一种踏实感——那种真正能帮人把活干完、把事落地的技术,终于来了。

回想在广告行业的那段日子,每一天都在和 Deadline 赛跑,为憋不出好点子抓狂,或是因为团队交付延迟而急得落泪。但如今重新用 Vidu Claw 操刀这些业务,曾经那种压迫感就逐渐消散了。

我唯一的感慨就是「相见恨晚」:如果当年我们拥有这个永远 24 小时在线、能瞬间响应热点和需求的神队友,是不是就能留下更多令人惊叹的爆款案例?

我想,一款真正优秀的「Claw」,就应该像 Vidu Claw 这样。

它既不是纯粹只能提供情绪价值的 Chatbot,也不是激进到企图取代人类的审美和创意。相反,它只会默默在后台扫清一切技术执行上的障碍,让我能用最快的速度、最低的成本,去验证那些天马行空的灵感。

所以,如果你也苦于脑海里光怪陆离的创意无法成真,不如也试试 Vidu Claw 吧。目前,Vidu Claw 已开放内测,输入 AppSo 为大家准备的邀请码 APPSON1,注册即送 500 积分,快来 Vidu Claw 施展你的创意吧。

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

4.48 万元起!全新五菱宏光 MINI EV 变为四门四座,既实用,也体面

作者 芥末
2026年3月27日 21:12

A0 级小车市场,绝对称得上「闷声发大财 」几个字。

在市场的目光被「高阶智驾」、「旗舰」、「大六座」等眼光缭乱的配置吸引住时,A0 级小车的销量却在节节攀升。

比如五菱宏光 MINI EV 就在过去 3 个月内,狂卖了 4.1 万辆。

但随着新能源市场的不断发展,市场对代步车的期待也在变得越来越高。不只要能跑起来,还要好看、好用、好玩,坐进去不寒碜,开出去有面子。

这也是今天上市的第五代宏光 MINI EV 的核心变化。

第五代宏光 MINI EV 的产品定位是「四门玩趣代步车」,覆盖带娃出行、日常通勤、城区练手和周边短途探索等场景。

对这款车来说,「四门」补上了过去微型车在实用性上的短板,「玩趣」则回应了年轻用户更看重的情绪价值。

第五代五菱宏光 MINI EV 共有 4 种配置,起售指导价为  4.48 万元起。

「四门」更实用

外观方面,第五代宏光 MINI EV 采用了名为「甜趣方糖」的全新设计语言。

整车延续了家族式的方正轮廓,同时通过更圆润的局部处理,弱化了传统方盒子造型的生硬感。

车身视觉重心按照 0.618 的黄金分割比例布局,前后观感更均衡,整体姿态也更轻巧。

前脸配有一条贯穿式镀铬饰条,形成略微上扬的弧线。保险杠两侧加入了类似「酒窝」的装饰,表面纹理在光线下层次更明显。

灯组部分,前后大灯采用圆环加贯穿灯带组成的「同心」式 LED 结构。近光覆盖宽度达到 84 度,官方称可照亮 7 条车道;远光照射距离为 165 米。全系的位置灯、转向灯、大灯和刹车灯均采用 LED 光源。

轮毂饰盖采用了旋转四叶草的造型,并经过空气动力学优化,用于降低风阻和整车重量。

新车推出了绿悠悠、白绒绒、灰萌萌三款偏复古取向的幻彩珠光车色。绿悠悠采用蓝绿双色珠光粒子,车漆会随视角和光线变化呈现不同层次;灰萌萌采用高定双涂色漆,在光影变化下更有流动感;白绒绒则偏向低饱和奶油色系,整体观感更柔和。车漆采用 8 层涂装工艺,并使用巴斯夫全涂层解决方案。

如果说外观变化更多体现在风格上,那么空间升级就是第五代宏光 MINI EV 最直观、也最有说服力的进化。

新车长宽高分别为 3268 × 1520 × 1575 mm,纵向空间利用率达到 89%,肩部空间利用率达到 79%。后排上方身体通过空间为 620 mm,下方腿部通过空间为 454 mm。

四门四座布局确实解决了过去许多微型车「后排能坐,但进出狼狈」的问题,此外新车后排独立车窗的可视面积约为 2016 cm²,还配备了同级唯一的电动升降功能,乘坐体验提升了很多。

车辆后备厢常规容积为 170 L,可以放下一个 23 英寸行李箱;后排座椅按 5:5 比例放倒后,容积可扩展至 838 L,能够同时容纳 1 个 32 英寸、1 个 28 英寸和 2 个 24 英寸行李箱。

全车共规划了 20 处储物空间,包括专属手机位、杯托、副驾双挂钩、门板储物槽和网兜等。

走进车内,第五代宏光 MINI EV 采用了「环趣美学」设计语言。环形元素贯穿仪表台、出风口、门饰板和储物格,整体视觉比上一代更完整,也更有主题感。配色采用焦糖色和奶油白组成的「焦糖奶油」撞色方案,再配合复古棕白格纹座椅面料,整体风格较为年轻化。

五菱还在仪表台区域设置了一个「百变造景空间」,既能储物,也可用于个性化展示,并采用了同级少见的波点镭雕工艺,质感比普通注塑或喷漆更细腻。副仪表台则采用「灵动岛」式一体化环形设计,将杯架、手机位和储物格整合在一起。

并且变速机构升级为怀挡设计,换挡时不用低头,手也无需离开方向盘,中央区域因此腾出了更多储物空间。

便宜但不将就

智能配置上,第五代宏光 MINI EV 这次同样升级了不少地方。

新车配备 10.1 英寸中控屏,是同级中尺寸最大的方案之一。下拉式控制中心集成了 17 项高频功能,UI 提供 5 种壁纸可选,还支持日夜模式智能切换。

手机互联支持 Apple CarPlay、华为 HiCar 和 Carlink 三大主流系统,同时支持有线与无线连接。

驾驶模式则提供经济、标准和运动 3 种选择,能量回收模式则有舒适、标准和强 3 档可调,支持靠近自动解锁、远离自动闭锁,踩下刹车即可启动,无需插钥匙;开门自动上电,关门闭锁后自动下电。

手机 App 远程控制功能也比较齐全,支持远程启动、导航寻车、双闪鸣笛定位、远程解闭锁、远程预调空调、查看实时电量与充电状态、预约错峰充电,以及预约空调定时开启。

主动安全层面,新车标配同级少见的 ESC 车身稳定系统,并针对微型车使用场景进行了专项调校,集成 ABS、EBD 等功能,在转向过度、转向不足和湿滑路面等工况下可自动介入。

电子手刹和自动驻车同样是标配,对坡道起步和拥堵工况更友好。高清倒车影像与后倒车雷达,则承担了停车时最基础、也最实用的安全辅助功能。

被动安全方面,车身采用环状笼式结构,高强钢占比达到 60%,热成型钢占比为 5.84%,关键防护区域使用 1500 MPa 超高强度钢材。

五菱表示,整车共进行了 14 种碰撞工况、累计 30 余次试验,覆盖从低速到高速、从正面到侧后碰的多种极端场景,模拟强度超过法规要求。模块化车身设计还能在轻微碰撞后显著降低维修成本,节约幅度超过 90%。

新车搭载了「神炼电池」,电池箱体采用 2 横 6 纵结构设计,底部设置了碰撞吸能结构,并 MUST 件将结构梁、侧板和热管理系统集成为一体,结构强度较传统方案提升超过 60%,维护成本降低 90%。

车云双 BMS 智能协同系统可在云端实现超过 240 项安全监管和超过 80 项故障预警,电池还具备加热和智能保温功能,在充电枪连接电源的状态下,可以在 24 小时内将电池温度维持在 20 ℃ 至 35 ℃ 的工作区间。

动力系统上,新车搭载 30 kW 三合一集成电驱,将电控、电机和减速器集成为一体,0 至 50 km/h 加速时间为 4.56 秒。

新车有 205 km 和 301 km 两个续航版本,其中 205 km 版本的度电续航里程超过 12.65 km/kWh,全系百公里综合电耗为 8.9 kWh,折算下来每公里使用成本约 0.05 元。

第五代五菱宏光 MINI EV 同时支持直流快充、交流慢充和家用 220 V / 10 A 插座。直流快充模式下,电量从 30% 充到 80% 需要 35 分钟;交流慢充从 20% 充到 100% 最快 4.8 小时。

随车充电枪内部电路板防护等级达到 IP67,充电枪与插座连接后的整体防水等级为 IP55,支持雨天户外充电。

除此之外,当低压蓄电池电压低于 11.9 V 且持续 14 秒、整车 SOC 不低于 10% 等 5 项条件同时满足时,车辆会自动启动 3 小时补电功能,用来防止小电瓶亏电。

底盘结构采用前麦弗逊式独立悬架和后三连杆式非独立悬架,制动系统标配前后盘式刹车。

如果把第五代车型放回整个宏光 MINI EV 家族的演进脉络里,会看到一条很清晰的演进过程。

第一代主打小巧便利,第二代强调时尚好看,第三代开始突出好玩有趣,第四代则往从容舒适上走。

而到了第五代,五菱想把前几代积累下来的产品价值重新整合起来,把四门布局正式固化为家族标配,再在设计语言、智能体验和续航上做一次系统升级。

宏光 MIN IEV 在 2020 年进入市场时,几乎带着一种开荒者姿态。那时,中国的微型纯电代步车市场还没有真正成熟,五菱抓住了「路面堵、停车难、油费高」这些非常现实的痛点,用一款价格低、好停车、使用成本也低的小车,迅速切入了城市通勤人群。

但现在 A0 级的市场风向变了。

早期比的是「有没有」「够不够用」,现在更看车辆外观是否漂亮,智能体验是否优秀以及是否有能被记住的差异点。

过去,宏光 MINI EV 最强的地方,是用极低门槛满足了城市短途出行的刚需。

现在它想做的,是在这个基础上加入更好的空间、设计、智能和安全性,让宏光 MINI EV 从「便宜的小车」,慢慢变成一台真正更好用、也更像样的代步车。

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

比亚迪:2025年全年营收8039.65亿元,同比增长3.46%

2026年3月27日 20:57
36氪获悉,比亚迪发布2025年业绩报告。报告显示,2025年全年营收8039.65亿元,同比增长3.46%;手机部件、组装及其他产品业务的收入约1552.37亿元,同比减少2.74%;占本集团总收入的比例分别为80.68%和19.31%。全年净利润326.2亿元,同比下降19%。

传音控股:2025年净利润25.81亿元,同比下降53.49%

2026年3月27日 20:53
36氪获悉,传音控股发布2025年业绩报。报告显示,公司2025年营业收入655.91亿元,同比下降4.55%;归属于上市公司股东的净利润25.81亿元,同比下降53.49%。公司拟以2025年度实施权益分派股权登记日登记的总股本为基数,向全体股东每10股派发现金红利人民币9.00元(含税)。

金帝股份再融资定于4月3日上会

2026年3月27日 20:40
36氪获悉,上海证券交易所上市审核委员会定于2026年4月3日召开2026年第15次上市审核委员会审议会议,审议山东金帝精密机械科技股份有限公司(再融资)。

胜宏科技:香港联交所审议公司发行境外上市外资股(H股)

2026年3月27日 20:33
36氪获悉,胜宏科技公告,公司正在进行申请发行境外上市外资股(H股)并在香港联合交易所有限公司(以下简称“香港联交所”)主板挂牌上市(以下简称“本次发行上市”)的相关工作,香港联交所上市委员会于2026年3月26日举行上市聆讯,审议公司本次发行上市的申请。

钉钉CLI以Apache-2.0协议开源

2026年3月27日 20:33
36氪获悉,3月27日,钉钉CLI开源项目上架Github社区,项目以Apache-2.0协议开源,首批开放AI表格、日历、日志、待办、机器人、通讯录、DING消息、考勤、开放平台文档、工作台共10项核心产品能力,原生支持Claude Code、Cursor等主流AI编程与Agent执行环境。

潮宏基:2025年净利润4.97亿元,同比增长156.66%

2026年3月27日 20:30
36氪获悉,潮宏基发布2025年业绩报告。报告显示,公司2025年营业收入93.18亿元,同比增长42.96%;归属于上市公司股东的净利润4.97亿元,同比增长156.66%。公司拟以8.89亿股为基数,向全体股东每10股派发现金红利3.5元(含税)。

伯特利:筹划发行H股股票并申请在香港联合交易所有限公司上市

2026年3月27日 20:21
36氪获悉,伯特利公告,为深入推进公司全球化战略布局,打造国际化资本运作平台,加快构建国内国际双循环格局,进一步助推公司产业全球化布局升维,公司拟发行境外上市外资股(H股)股票并申请于香港联合交易所有限公司(以下简称“香港联交所”)主板挂牌上市(以下简称“本次发行”、“本次发行上市”或“本次发行并上市”)。

江铃汽车:2025年归属于上市公司股东的净利润11.87亿元,同比下降22.75%

2026年3月27日 20:19
36氪获悉,江铃汽车发布2025年业绩报告。报告显示,2025年实现营业收入391.7亿元,同比增长2.07%;归属于上市公司股东的净利润11.87亿元,同比下降22.75%;基本每股收益1.38元。公司拟向全体股东每10股派发现金红利5.5581元(含税),送红股0股(含税),不以公积金转增股本。
❌
❌