普通视图

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

ps, ai, ae插件都可以用html和js开发了

作者 nujnewnehc
2026年4月16日 02:31

Adobe ScriptUI 学习之旅:从标记语言到跨应用交互

1. 缘起:对 Duik 插件的探索

作为 After Effects 用户,我一直对 Duik 这个强大的角色动画插件库充满好奇。Duik 提供了丰富的骨骼动画工具,极大地简化了角色动画的制作流程。然而,当我尝试深入学习它的代码结构时,却遇到了一些挑战:

  • 文档收费:完整的 Duik 文档需要付费购买
  • 代码结构问题:Duik 没有采用模块化设计,而是大量使用全局变量作为功能实现方式,导致代码可读性差,难以维护和扩展
  • 学习曲线陡峭:对于初学者来说,理解其代码逻辑较为困难

2. 寻找官方文档的历程

为了更好地理解 Adobe 脚本开发,我开始从 After Effects 的帮助菜单中寻找相关文档。有趣的是,Adobe 官方并没有直接提供完整的 ScriptUI 文档,而是在帮助菜单的链接中指向了第三方资源:

通过这些文档,我了解到 ScriptUI 不仅适用于 After Effects,还可以在其他 Adobe 应用中使用,并且支持跨应用交互,这为我的开发思路打开了新的大门。

3. 痛点:ScriptUI 构建的复杂性

在学习过程中,我发现 ScriptUI 的构建方式相对繁琐:

  • 需要通过脚本代码手动创建每个 UI 组件
  • 布局管理需要编写大量代码
  • 修改 UI 结构时需要调整多处代码

Duik 虽然抽象了一些方法来简化这个过程,但对于我来说仍然不够直观。我开始思考:是否可以使用更简洁的标记语言来描述 UI,然后编译成 ScriptUI 代码?

4. 解决方案:标记语言到 ScriptUI 的转换

基于这个想法,我创建了一个代码仓库,实现了从 HTML 标记语言到 Adobe ScriptUI JSX 代码的转换。在开发初期,我考虑过使用 Vue Compiler 来处理标记语言,但后来发现 Vue 支持的特性和 HTML 差别较大,而且我们并不需要 HTML 的全部功能,因此最终决定自己实现一个轻量级的转换引擎。

核心功能

  • HTML 标记语言支持:使用熟悉的 HTML 标签来描述 UI 结构
  • 组件映射:将 HTML 标签映射到对应的 ScriptUI 组件
  • 事件处理:支持内联 onClick 事件绑定
  • 实时编译:通过文件监视器实时将 HTML 变更编译为 JSX

项目结构

项目采用 monorepo 结构,包含三个核心包:

  1. adobe-scriptui-html-to-jsx:核心转换引擎,负责将 HTML 标记转换为 ScriptUI JSX 代码
  2. adobe-scriptui-watcher:文件监视器,监视 HTML 文件变化并自动触发编译
  3. adobe-scriptui-ae-helper:示例应用,展示了如何使用该工具构建实际的 After Effects 助手工具

这种拆包设计的目的是为了模块化和可扩展性,现在主要用于 After Effects,但未来可以轻松扩展到其他 Adobe 应用中。

5. 实际应用:AE 助手工具

通过这个工具,我构建了一个功能丰富的 After Effects 助手工具,包含:

  • 弹性表达式:为属性添加弹性动画效果
  • 常用表达式:快速应用 loop、wiggle、random 等常用表达式
  • AI 文件导入:支持从 Illustrator 导入文件到 After Effects
  • 用户友好的界面:通过 HTML 标记语言快速构建和修改 UI

6. 未来展望

这个项目只是我对 Adobe 脚本开发探索的开始。未来,我计划:

业务调研与痛点解决

  • 表达式管理系统:针对设计师使用表达式的痛点,开发一个更智能的表达式管理系统。目前设计师们通常需要从文本文件中复制粘贴表达式,修改参数困难,应用到其他属性时容易出错,最终只好放弃使用表达式而回到关键帧动画。
  • 可视化表达式编辑器:开发一个可视化的表达式编辑器,让设计师可以通过界面调整参数,而不需要直接编写代码。

功能抽象

  • 跨应用组件库:构建一套适用于所有 Adobe 应用的组件库
  • 模板系统:创建可复用的 UI 模板
  • 跨应用工作流:实现不同 Adobe 应用之间的无缝交互

开发流程优化

  • AI 辅助开发:利用 AI 工具生成初始代码,然后根据文档进行调整
  • 可视化编辑器:开发一个可视化 UI 编辑器,进一步简化开发流程
  • 社区贡献:鼓励社区贡献,共同完善这个工具

7. 结语

通过对 Adobe ScriptUI 的学习和探索,我不仅解决了 UI 构建的痛点,还为 Adobe 脚本开发提供了一种新的思路。标记语言到 ScriptUI 的转换,使得 UI 开发变得更加直观和高效。

虽然这个项目还在不断发展中,但它已经展示了如何通过创新的方法来简化复杂的开发流程。我相信,随着更多功能的加入和社区的参与,这个工具将成为 Adobe 脚本开发者的有力助手,同时也能帮助设计师们更轻松地使用和理解表达式,提升动画制作的效率和质量。


提示:如果你也对 Adobe 脚本开发感兴趣,欢迎访问我的 GitHub 仓库 adobe-script-ui-helper,一起探索 Adobe 脚本开发的无限可能!

每日一题-距离最小相等元素查询🟡

2026年4月16日 00:00

给你一个 环形 数组 nums 和一个数组 queries 。

对于每个查询 i ,你需要找到以下内容:

  • 数组 nums 中下标 queries[i] 处的元素与 任意 其他下标 j(满足 nums[j] == nums[queries[i]])之间的 最小 距离。如果不存在这样的下标 j,则该查询的结果为 -1

返回一个数组 answer,其大小与 queries 相同,其中 answer[i] 表示查询i的结果。

 

示例 1:

输入: nums = [1,3,1,4,1,3,2], queries = [0,3,5]

输出: [2,-1,3]

解释:

  • 查询 0:下标 queries[0] = 0 处的元素为 nums[0] = 1 。最近的相同值下标为 2,距离为 2。
  • 查询 1:下标 queries[1] = 3 处的元素为 nums[3] = 4 。不存在其他包含值 4 的下标,因此结果为 -1。
  • 查询 2:下标 queries[2] = 5 处的元素为 nums[5] = 3 。最近的相同值下标为 1,距离为 3(沿着循环路径:5 -> 6 -> 0 -> 1)。

示例 2:

输入: nums = [1,2,3,4], queries = [0,1,2,3]

输出: [-1,-1,-1,-1]

解释:

数组 nums 中的每个值都是唯一的,因此没有下标与查询的元素值相同。所有查询的结果均为 -1。

 

提示:

  • 1 <= queries.length <= nums.length <= 105
  • 1 <= nums[i] <= 106
  • 0 <= queries[i] < nums.length

整洁架构三连问:是什么,怎么做,为什么要用

作者 暖阳_
2026年4月15日 23:48

整洁架构问答

Q1:什么是整洁架构?

由 Robert C. Martin(Uncle Bob)提出,核心思想:业务逻辑独立于框架、UI、数据库等外部细节,依赖关系只能从外层指向内层。

四层结构

┌─────────────────────────────────┐
   Frameworks & Drivers             Web数据库UI(最易变)
   ┌─────────────────────────┐   
     Interface Adapters           控制器网关Presenter
     ┌───────────────────┐     
       Application              用例(Use Cases)
       Business Rules        
       ┌─────────────┐      
        Enterprise           实体(Entities)最稳定
        Business          
        Rules             
       └─────────────┘      
     └───────────────────┘     
   └─────────────────────────┘   
└─────────────────────────────────┘
  1. 实体层:核心业务规则,零依赖
  2. 用例层:编排实体完成应用逻辑
  3. 接口适配层:数据转换,连接内外
  4. 框架与驱动层:具体技术实现

关键收益

  • 可测试:业务逻辑不依赖外部,独立单元测试
  • 可替换:换数据库/框架不影响核心逻辑
  • 可维护:修改外层不影响内层

一句话:让业务逻辑成为核心,让数据库、框架等成为可插拔的细节。


Q2:整洁架构和普通架构有什么区别?

普通架构按技术类型分层,整洁架构按依赖方向分层。

直观对比

普通架构(传统分层)
src/
├── components/    ← UI 组件
├── api/           ← 接口调用
├── store/         ← 状态管理
├── utils/         ← 工具函数
└── views/         ← 页面

依赖方向:随意交叉

组件 ←→ store ←→ api
 ↕        ↕       ↕
utils ←─ views ←─ components
整洁架构
src/
├── domain/           ← 纯业务(零依赖)
├── application/      ← 用例编排
├── infrastructure/   ← 技术实现
└── ui/               ← 界面渲染

依赖方向:只从外向内

ui → application → domain
infrastructure → application → domain
         ↘          ↓          ↗
          任何外层都可以依赖内层
          内层永远不知道外层存在

具体差异对比

维度 普通架构 整洁架构
分层依据 按技术类型(组件、API、工具) 按依赖方向(外层依赖内层)
业务逻辑位置 散落在组件、store、utils 集中在 domain 层
框架耦合 业务逻辑依赖 Vue/React domain 层零框架依赖
改数据库 改 api/ + store/ + 组件 只改 infrastructure/
换框架 几乎重写 只重写 ui/ 层
单元测试 需要 mock 框架、mount 组件 直接测纯 TS 函数
API 字段变了 改多处 只改 Repository 转换逻辑

代码对比:同一个需求

普通架构写法
// store/cart.ts
import axios from 'axios'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])

  async function addItem(productId: string) {
    // 业务规则散落在 store 里
    if (items.value.length >= 20) {
      throw new Error('购物车已满')
    }
    // 直接耦合 axios
    const res = await axios.post('/api/cart', { productId })
    // 直接操作原始 JSON
    items.value.push(res.data)
  }

  return { items, addItem }
})
<!-- CartPage.vue -->
<script setup>
import { useCartStore } from '@/store/cart'

const cartStore = useCartStore()

// 组件里也有业务逻辑
const canAdd = computed(() => cartStore.items.length < 20)
</script>

问题:业务规则在 store 和组件里都有,换框架全废,换 axios 要改 store。

整洁架构写法
// domain/aggregates/Cart.ts — 纯 TS,零依赖
export class Cart {
  private items: CartItem[] = []

  addItem(item: CartItem): void { /* 规则在这里 */ }
  canAddMore(): boolean { return this.items.length < 20 }
}
// application/usecases/AddToCartUseCase.ts — 编排
export class AddToCartUseCase {
  constructor(private repo: CartRepository) {}
  async execute(id: string) {
    const cart = await this.repo.getCart()
    cart.addItem(item)           // 调领域方法
    await this.repo.save(cart)   // 持久化
  }
}
// infrastructure/repositories/ApiCartRepository.ts — 适配
export class ApiCartRepository implements CartRepository {
  async getCart() { /* axios → 领域对象 */ }
  async save(cart) { /* 领域对象 → axios */ }
}
<!-- ui/pages/CartPage.vue — 只渲染 -->
<script setup>
const { addItem } = useCart()  // composable 调用用例
</script>

好处:业务规则只在 Cart.ts,换框架/换 axios 都不影响它。

什么时候用哪个

场景 建议
简单 CRUD、几个页面 普通架构够用,别过度设计
中等复杂度、有业务规则 提取 domain 层,用 composable 做应用层
复杂业务(交易、审批、计费) 完整整洁架构 + DDD

一句话总结:普通架构按技术分文件夹,整洁架构按依赖方向分层。前者简单但业务逻辑散落各处,后者前期成本高但业务逻辑内聚、可替换、可测试。


Q3:目前 Vue CLI 是不是都是整洁架构?

不是。 Vue CLI 生成的项目默认不符合整洁架构

Vue CLI 默认结构

src/
├── components/    ← 组件(UI + 业务逻辑混在一起)
├── views/         ← 页面(UI + 业务逻辑混在一起)
├── router/        ← 路由
├── store/         ← Vuex/Pinia(状态管理)
├── api/           ← API 调用
├── utils/         ← 工具函数
└── App.vue

为什么不是整洁架构

整洁架构要求 Vue CLI 默认 问题
依赖从外指向内 各层互相 import,方向混乱 无依赖规则约束
领域层纯 TS,零框架依赖 业务逻辑写在 .vue 领域层耦合了 Vue
用例层编排领域对象 没有 use case 层 业务逻辑散落在组件和 store
Repository 接口在领域层定义 直接在组件里调 axios 数据获取和 UI 耦合

典型 Vue 组件的问题

<script setup>
// ❌ UI、业务规则、API 调用全混在一起
import axios from 'axios'

const cart = ref([])

async function addItem(id) {
  if (cart.value.length >= 20) {       // 业务规则在组件里
    alert('购物车已满')
    return
  }
  const res = await axios.post('/api/cart', { id })  // 直接调 API
  cart.value.push(res.data)
}
</script>

Vue CLI 能改成整洁架构吗

能,但需要手动改造,Vue CLI 不会自动帮你分层:

src/
├── domain/           ← 手动添加:纯 TS 领域层
│   ├── value-objects/
│   ├── entities/
│   ├── aggregates/
│   └── repositories/    ← 接口定义
├── application/      ← 手动添加:用例层
│   ├── usecases/
│   └── composables/
├── infrastructure/   ← 手动添加:适配层
│   ├── repositories/    ← Repository 实现
│   └── api/
├── ui/               ← 原 components/views,只负责渲染
│   ├── components/
│   └── pages/
├── router/
└── App.vue

一句话总结

Vue CLI 是脚手架工具,只管项目初始化和构建配置,不管架构分层。默认生成的项目是传统分层(按技术类型分),不是整洁架构(按依赖方向分)。整洁架构需要开发者自己设计和实施。

前端整洁架构详解

作者 暖阳_
2026年4月15日 22:52

前端整洁架构详解

以电商购物车系统为例,逐层讲解整洁架构在前端中的落地方式。


架构分层总览

┌──────────────────────────────────┐
│  UI 框架层(Vue/React 组件)      │  ← 最外层:页面、组件
│  ┌────────────────────────────┐  │
│  │  接口适配层                 │  │  ← Store/Composable、API 适配、Repository 实现
│  │  ┌────────────────────┐    │  │
│  │  │  应用层             │    │  │  ← 用例:编排领域逻辑
│  │  │  ┌────────────┐    │    │  │
│  │  │  │  领域层     │    │    │  │  ← 纯 JS/TS:实体、值对象、规则
│  │  │  └────────────┘    │    │  │
│  │  └────────────────────┘    │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

依赖规则:依赖关系只能从外层指向内层,内层完全不知道外层的存在。


1. 领域层(纯 JS/TS,零依赖)

最内层,不引入任何框架,不 import Vue/React,不 import axios,纯业务逻辑。

1.1 值对象 — 封装校验和不可变概念

// domain/value-objects/Money.ts
export class Money {
  constructor(private amount: number, private currency: string = 'CNY') {
    if (amount < 0) throw new Error('金额不能为负数');
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) throw new Error('币种不同');
    return new Money(this.amount + other.amount, this.currency);
  }

  multiply(factor: number): Money {
    return new Money(this.amount * factor, this.currency);
  }

  get value(): number { return this.amount; }
  toString(): string { return `${this.currency} ${this.amount.toFixed(2)}`; }
}
// domain/value-objects/Email.ts
export class Email {
  constructor(private value: string) {
    if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value)) {
      throw new Error('邮箱格式不合法');
    }
  }

  get domain(): string { return this.value.split('@')[1]; }
  toString(): string { return this.value; }
}

为什么用值对象? 校验逻辑内聚,不会散落在各个组件里。任何地方拿到一个 Money,就保证它是合法的。

1.2 实体 — 有标识,有生命周期

// domain/entities/CartItem.ts
import { Money } from '../value-objects/Money';

export class CartItem {
  constructor(
    public readonly productId: string,  // 唯一标识
    public readonly name: string,
    private unitPrice: Money,
    private quantity: number,
  ) {
    if (quantity <= 0) throw new Error('数量必须大于0');
  }

  get totalPrice(): Money {
    return this.unitPrice.multiply(this.quantity);
  }

  changeQuantity(newQty: number): CartItem {
    return new CartItem(this.productId, this.name, this.unitPrice, newQty);
  }
}

1.3 聚合根 — 保证一致性边界

// domain/aggregates/Cart.ts
import { Money } from '../value-objects/Money';
import { CartItem } from '../entities/CartItem';

export class Cart {
  private items: CartItem[] = [];

  addItem(item: CartItem): void {
    const existing = this.items.find(i => i.productId === item.productId);
    if (existing) {
      // 已存在则合并数量
      this.items = this.items.map(i =>
        i.productId === item.productId
          ? i.changeQuantity(i.quantity + item.quantity)
          : i
      );
    } else {
      this.items.push(item);
    }
  }

  removeItem(productId: string): void {
    this.items = this.items.filter(i => i.productId !== productId);
  }

  get total(): Money {
    return this.items.reduce(
      (sum, item) => sum.add(item.totalPrice),
      new Money(0),
    );
  }

  get itemCount(): number {
    return this.items.length;
  }

  // 业务规则:购物车最多 20 件商品
  canAddMore(): boolean {
    return this.items.length < 20;
  }
}

关键点:所有业务规则都在这里——最多 20 件、合并同类商品、金额计算。组件不需要知道这些规则。

1.4 Repository 接口(领域层定义)

// domain/repositories/CartRepository.ts
import { Cart } from '../aggregates/Cart';

export interface CartRepository {
  getCart(): Promise<Cart>;
  save(cart: Cart): Promise<void>;
  clear(): Promise<void>;
}

接口在领域层定义,实现在适配层——这是依赖反转的核心,让领域层不依赖具体的数据获取方式。


2. 应用层(用例 — 编排领域对象)

这一层只做编排,不包含业务规则本身。它协调领域对象完成一个完整操作。

2.1 添加商品到购物车

// application/usecases/AddToCartUseCase.ts
import { Cart } from '../../domain/aggregates/Cart';
import { CartItem } from '../../domain/entities/CartItem';
import { Money } from '../../domain/value-objects/Money';
import { CartRepository } from '../../domain/repositories/CartRepository';

export class AddToCartUseCase {
  constructor(private cartRepo: CartRepository) {}

  async execute(productId: string, name: string, price: number, qty: number): Promise<Cart> {
    // 1. 从仓库获取当前购物车
    const cart = await this.cartRepo.getCart();

    // 2. 业务规则检查(规则在领域对象里,用例只调用)
    if (!cart.canAddMore()) {
      throw new Error('购物车已满,最多 20 件商品');
    }

    // 3. 创建领域对象
    const item = new CartItem(productId, name, new Money(price), qty);

    // 4. 执行领域操作
    cart.addItem(item);

    // 5. 持久化
    await this.cartRepo.save(cart);

    return cart;
  }
}

2.2 结算下单

// application/usecases/CheckoutUseCase.ts
import { CartRepository } from '../../domain/repositories/CartRepository';
import { OrderRepository } from '../../domain/repositories/OrderRepository';
import { Order } from '../../domain/aggregates/Order';

export class CheckoutUseCase {
  constructor(
    private cartRepo: CartRepository,
    private orderRepo: OrderRepository,
  ) {}

  async execute(userId: string): Promise<Order> {
    const cart = await this.cartRepo.getCart();

    if (cart.itemCount === 0) {
      throw new Error('购物车为空,无法下单');
    }

    // 领域逻辑:创建订单
    const order = Order.createFromCart(cart, userId);

    await this.orderRepo.save(order);
    await this.cartRepo.clear();

    return order;
  }
}

用例层的特点:读数据 → 调领域方法 → 存数据。像导演一样编排,但不自己写业务规则。


3. 接口适配层(Store/Composable + API 适配)

3.1 Repository 实现(JSON ↔ 领域对象转换)

// infrastructure/repositories/ApiCartRepository.ts
import { CartRepository } from '../../domain/repositories/CartRepository';
import { Cart } from '../../domain/aggregates/Cart';
import { CartItem } from '../../domain/entities/CartItem';
import { Money } from '../../domain/value-objects/Money';
import { cartApi } from '../api/CartApi';

export class ApiCartRepository implements CartRepository {
  async getCart(): Promise<Cart> {
    // API 返回的是原始 JSON,需要转换成领域对象
    const raw = await cartApi.fetchCart();
    const cart = new Cart();
    for (const item of raw.items) {
      cart.addItem(new CartItem(item.productId, item.name, new Money(item.price), item.qty));
    }
    return cart;
  }

  async save(cart: Cart): Promise<void> {
    // 领域对象 → 原始 JSON,给 API
    const payload = {
      items: cart.items.map(i => ({
        productId: i.productId,
        name: i.name,
        price: i.unitPrice.value,
        qty: i.quantity,
      })),
    };
    await cartApi.updateCart(payload);
  }

  async clear(): Promise<void> {
    await cartApi.clearCart();
  }
}

关键:API 返回的 JSON → 领域对象的转换在这里完成。领域层永远不碰原始 JSON。

3.2 API 层(最外层细节)

// infrastructure/api/CartApi.ts
import axios from 'axios';

export const cartApi = {
  fetchCart: () => axios.get('/api/cart').then(r => r.data),
  updateCart: (data: any) => axios.put('/api/cart', data).then(r => r.data),
  clearCart: () => axios.delete('/api/cart').then(r => r.data),
};

3.3 Composable(连接用例和 UI)

// application/composables/useCart.ts
import { ref } from 'vue';
import { AddToCartUseCase } from '../usecases/AddToCartUseCase';
import { ApiCartRepository } from '../../infrastructure/repositories/ApiCartRepository';

const cartRepo = new ApiCartRepository();
const addToCartUseCase = new AddToCartUseCase(cartRepo);

export function useCart() {
  const cart = ref<Cart | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function addItem(productId: string, name: string, price: number, qty: number) {
    loading.value = true;
    error.value = null;
    try {
      cart.value = await addToCartUseCase.execute(productId, name, price, qty);
    } catch (e) {
      error.value = e.message;
    } finally {
      loading.value = false;
    }
  }

  return { cart, loading, error, addItem };
}

4. UI 框架层(组件 — 只负责渲染)

<!-- ui/components/ProductCard.vue -->
<script setup lang="ts">
import { useCart } from '@/application/composables/useCart';

const { addItem, loading, error } = useCart();

function handleAdd() {
  addItem(product.id, product.name, product.price, 1);
}
</script>

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>¥{{ product.price }}</p>
    <button :disabled="loading" @click="handleAdd">
      加入购物车
    </button>
    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

组件只做三件事:展示数据、捕获用户操作、调用用例。零业务逻辑。


依赖关系总览

ProductCard.vue          ← 只渲染,零业务逻辑
  ↓ 调用
useCart()                ← 状态管理 + 调用用例
  ↓ 调用
AddToCartUseCase         ← 编排领域对象
  ↓ 使用
Cart / CartItem / Money  ← 纯业务规则
  ↓ 通过接口
CartRepository (接口)    ← 领域层定义接口
  ↓ 实现
ApiCartRepository        ← 适配层实现,转换 JSON ↔ 领域对象
  ↓ 调用
cartApi (axios)          ← 最外层:HTTP 请求细节

箭头方向 = 依赖方向,全部从外指向内。内层完全不知道外层存在。


项目目录结构

src/
├── domain/                          # 领域层(纯 TS,零框架依赖)
│   ├── value-objects/
│   │   ├── Money.ts
│   │   └── Email.ts
│   ├── entities/
│   │   └── CartItem.ts
│   ├── aggregates/
│   │   ├── Cart.ts
│   │   └── Order.ts
│   └── repositories/
│       ├── CartRepository.ts        # 接口定义
│       └── OrderRepository.ts
│
├── application/                     # 应用层(用例编排)
│   ├── usecases/
│   │   ├── AddToCartUseCase.ts
│   │   └── CheckoutUseCase.ts
│   └── composables/
│       └── useCart.ts
│
├── infrastructure/                  # 接口适配层
│   ├── repositories/
│   │   └── ApiCartRepository.ts     # Repository 实现
│   └── api/
│       └── CartApi.ts               # axios 调用
│
├── ui/                              # UI 框架层
│   ├── components/
│   │   └── ProductCard.vue
│   └── pages/
│       └── CartPage.vue

这样做的好处

场景 传统做法 整洁架构
换框架 Vue→React 重写所有业务逻辑 只重写 UI 层,领域层直接复用
API 字段名变了 改几十个组件 只改 ApiCartRepository 的转换逻辑
加新业务规则 在组件里到处加 if 在领域对象里加,组件无感知
单元测试 mock axios、mount 组件 直接测 Cart.addItem(),纯函数测试
后端复用 不可能 领域层是纯 TS,Node 端可直接引用

实际项目中的权衡

不是所有前端项目都需要完整分层

项目规模 建议做法
简单页面/CRUD 分离 API 调用和 UI 即可,不必过度设计
中等复杂度 提取领域层(纯 TS),用 Composable 做应用层
复杂业务系统 完整分层 + DDD 建模(如交易系统、审批流)

核心原则

领域层必须是纯 JS/TS,不依赖任何框架。 这样你的业务逻辑可以:

  • 在 Vue 和 React 之间迁移
  • 在 Node 后端复用(同构)
  • 独立写单元测试,不需要 mock 组件

框架是细节,业务逻辑才是核心。

我花一天时间Vibe Coding的开源AI工具,一键检测你的电脑能跑哪些AI大模型

作者 徐小夕
2026年4月15日 22:43

最近一直在深耕 AI Agent 与大模型应用,比如 JitKnow AI 知识库、JitWord协同AI文档、Pxcharts 超级表格,同时也持续在给大家分享 GitHub 上真正能落地、能解决实际问题的优质AI开源项目。

图片

今天和大家分享一款我花了一天时间做的开源工具——ai-detector。

它是一款完全运行在浏览器端的免费硬件检测工具。能自动读取你电脑的内存、CPU、GPU 信息,智能匹配 21+ 主流开源 AI 大模型的兼容性,并给出本地运行速度预估,帮你在 5 秒内找到最适合自己电脑的 AI 模型。

无需安装、无需登录、无需上传任何数据,全程 100% 本地运行。

ps:最近小龙虾很火,但是又担心自己电脑配置不够的朋友,可以使用这款线上工具检测一下电脑适合哪些模型,告别AI恐惧啦~老规矩,先上开源地址。

github:github.com/MrXujiang/a…

演示地址:jitword.com/ai-detector

下面就和大家详细分享一下这款 AI Coding 出来的开源项目。

先上一个基础的功能演示比如我想在我的电脑里部署一个本地AI模型,但是又担心我的电脑配置不够,那么直接运行这个项目:

图片

点击开始检测, 不到5s,就会给出自己电脑的性能和适合运行哪些模型的详细报告:

图片

分析的非常准确,可能是我电脑年久失修,只给出了28分。。。ai-detector 还会为我们推荐基于当前电脑,适合运行的模型推荐:

图片

不仅如此,它还会对目前主流的数十个开源模型,对当前电脑进行分析评测,分析出部署这些大模型的性能,风险等信息,如下:

图片

对于比较吃电脑性能的模型,它会给我们全面的分析:

图片

最后会基于我们的硬件配置,估算各模型生成速度(tokens/秒),并输出可视化的分析报表:

图片

核心能力总结

下面和大家总结一下 ai-detector 的核心能力和亮点。

  1. 硬件自动检测
  • 系统内存通过 navigator.deviceMemory 读取 RAM 大小
  • CPU 核心数通过 navigator.hardwareConcurrency 读取逻辑核数
  • GPU 型号通过 WebGL WEBGL_debug_renderer_info 扩展识别显卡
  • 综合跑分基于内存与 CPU 计算 0-100 综合评分,直观了解你的 AI 能力等级

2. 21+ 大模型兼容性分析

覆盖当前最主流的开源大模型系列,一键发现哪些能跑、哪些跑不动:

系列 代表模型 参数规模
🦙 Llama TinyLlama、Llama 3.2、Llama 3.1 1.1B ~ 70B
🌐 Qwen Qwen2.5 3B/7B/14B/32B/72B 3B ~ 72B
💎 Phi Phi-3 Mini、Phi-3 Medium 3.8B ~ 14B
🌬️ Mistral Mistral 7B 7B
🧠 DeepSeek DeepSeek-R1、DeepSeek Coder 7B ~ 70B
👁️ 多模态 LLaVA 7B、MiniCPM-V 8B 7B ~ 8B
💫 Gemma Gemma 2 2B 2B
  1. 支持兼容性三级分类
  • 😊 流畅运行内存充裕,可稳定高速推理
  • ⚠️ 勉强运行:内存刚好满足,速度偏慢
  • ❌ 内存不足:当前配置无法加载该模型
  1. 运行速度排行

检测完成后自动生成可运行模型的速度排行榜,按 tokens/秒从高到低排列,帮你优先选出响应最快的模型。

  1. 量化模式切换

图片

  • Q4 量化内存占用更低,普通设备首选
  • Q8 量化精度更高,内存需求约为 Q4 的 2 倍
  • 一键切换,实时刷新所有模型兼容状态
  1. 个性化模型推荐基于我们的硬件配置,自动从模型库中精选 最均衡速度最快能力最强 三款推荐,省去选择烦恼。

  2. 一键复制检测报告

图片

生成包含硬件配置、综合评分、可运行模型的文本报告,方便分享或咨询。

完整使用流程总结

为了让大家轻松上手使用,我总结了一下7步使用法,大家可以参考一下:

  1. 打开页面 → 点击「开始硬件检测」按钮
  2. 等待扫描(约 3~5 秒)→ 自动完成内存、CPU、GPU 检测
  3. 查看结果 → 获得综合跑分与设备等级评价
  4. 浏览推荐 → 查看「为你推荐」区块,获取最适合你的 3 款模型
  5. 筛选模型 → 在「模型列表」中按兼容状态、类型筛选,支持关键词搜索
  6. 切换量化 → 尝试切换 Q4 / Q8 量化,查看内存需求变化
  7. 复制报告 → 一键复制检测结果,方便保存或分享

为什么要做这个开源项目这里分析一张图,大家就知道了:

特性 AI -Detector 其他工具
需要安装 ❌ 无需安装 ✅ 通常需要
需要登录 ❌ 无需注册 ✅ 通常需要
数据上传 ❌ 完全不上传 ⚠️ 部分上传
模型覆盖 ✅ 21+ 主流模型 ⚠️ 覆盖有限
速度预估 ✅ tokens/s 量化估算 ⚠️ 通常无此功能
升级建议 ✅ 智能提示内存升级方案 ❌ 无
量化切换 ✅ Q4 / Q8 实时切换 ❌ 无
开源免费 ✅ MIT 协议 ⚠️ 多数收费

主要是为了让任何没有技术基础的人,也能轻松拥有专业级AI模型选型能力,告别AI焦虑。

目前已开源,大家可以免费使用:

github:github.com/MrXujiang/a…

DEMO演示地址:jitword.com/ai-detector

贪心

作者 tsreaper
2025年3月16日 12:30

解法:贪心

如果不是环形序列,为了让距离差尽量小,每个元素只要考虑上一个和下一个相同元素即可。

环形序列怎么做呢?设序列长度为 $n$,我们把序列复制一遍加在原序列后面,然后按普通序列的方法计算这个长度为 $2n$ 的序列的答案。设新序列中下标为 $i$ 的元素答案为 $v_i$,则原序列中下标为 $i$ 的元素答案为 $\min(v_i, v_{i + n})$。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
        int n = nums.size();
        // 把序列复制一遍
        for (int i = 0; i < n; i++) nums.push_back(nums[i]);

        int ans[n * 2];
        for (int i = 0; i < n * 2; i++) ans[i] = 1e9;

        // 计算每个元素距离上一个相同元素多远
        unordered_map<int, vector<int>> mp;
        for (int i = 0; i < n * 2; i++) {
            auto &vec = mp[nums[i]];
            if (!vec.empty()) ans[i] = min(ans[i], i - vec.back());
            vec.push_back(i);
        }

        // 计算每个元素距离下一个相同元素多远
        mp.clear();
        for (int i = n * 2 - 1; i >= 0; i--) {
            auto &vec = mp[nums[i]];
            if (!vec.empty()) ans[i] = min(ans[i], vec.back() - i);
            vec.push_back(i);
        }

        vector<int> ret;
        for (int x : queries) {
            int t = min(ans[x], ans[x + n]);
            if (t < n) ret.push_back(t);
            else ret.push_back(-1);
        }
        return ret;
    }
};

两种方法:二分查找 / 预处理(Python/Java/C++/Go)

作者 endlesscheng
2025年3月16日 12:10

方法一:二分查找

看示例 1,所有 $1$ 的下标列表是 $p=[0,2,4]$。

我们在 $p$ 中二分查找下标 $i$,设二分返回值为 $j$,那么:

  • $p[j-1]$ 就是在 $i$ 左边的最近位置。
  • $p[j+1]$ 就是在 $i$ 右边的最近位置。

两个距离取最小值,答案为

$$
\min(i-p[j-1], p[j+1]-i)
$$

如果 $j-1$ 或者 $j+1$ 下标越界怎么办?需要写一堆 $\texttt{if-else}$ 吗?

在示例 1 中,由于 $\textit{nums}$ 是循环数组:

  • 在下标列表前面添加 $4-n=-3$,相当于认为在 $-3$ 下标处也有一个 $1$。
  • 在下标列表末尾添加 $0+n=7$,相当于认为在 $7$ 下标处也有一个 $1$。

修改后的下标列表为 $p=[-3,0,2,4,7]$。

特别地,如果 $\textit{nums}[i]$ 在 $\textit{nums}$ 中只出现了一次,那么答案为 $-1$。

代码实现时,可以直接把答案记录在 $\textit{queries}$ 数组中。

具体请看 视频讲解,欢迎点赞关注~

###py

class Solution:
    def solveQueries(self, nums: List[int], queries: List[int]) -> List[int]:
        indices = defaultdict(list)
        for i, x in enumerate(nums):
            indices[x].append(i)

        n = len(nums)
        for p in indices.values():
            # 前后各加一个哨兵
            i0 = p[0]
            p.insert(0, p[-1] - n)
            p.append(i0 + n)

        for qi, i in enumerate(queries):
            p = indices[nums[i]]
            if len(p) == 3:
                queries[qi] = -1
            else:
                j = bisect_left(p, i)
                queries[qi] = min(i - p[j - 1], p[j + 1] - i)
        return queries

###java

class Solution {
    public List<Integer> solveQueries(int[] nums, int[] queries) {
        Map<Integer, List<Integer>> indices = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            indices.computeIfAbsent(nums[i], k -> new ArrayList<>()).add(i);
        }

        int n = nums.length;
        for (List<Integer> p : indices.values()) {
            // 前后各加一个哨兵
            int i0 = p.get(0);
            p.add(0, p.get(p.size() - 1) - n);
            p.add(i0 + n);
        }

        List<Integer> ans = new ArrayList<>(queries.length); // 预分配空间
        for (int i : queries) {
            List<Integer> p = indices.get(nums[i]);
            if (p.size() == 3) {
                ans.add(-1);
            } else {
                int j = Collections.binarySearch(p, i);
                ans.add(Math.min(i - p.get(j - 1), p.get(j + 1) - i));
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
        unordered_map<int, vector<int>> indices;
        for (int i = 0; i < nums.size(); i++) {
            indices[nums[i]].push_back(i);
        }

        int n = nums.size();
        for (auto& [_, p] : indices) {
            // 前后各加一个哨兵
            int i0 = p[0];
            p.insert(p.begin(), p.back() - n);
            p.push_back(i0 + n);
        }

        for (int& i : queries) { // 注意这里是引用
            auto& p = indices[nums[i]];
            if (p.size() == 3) {
                i = -1;
            } else {
                int j = ranges::lower_bound(p, i) - p.begin();
                i = min(i - p[j - 1], p[j + 1] - i);
            }
        }
        return queries;
    }
};

###go

func solveQueries(nums []int, queries []int) []int {
indices := map[int][]int{}
for i, x := range nums {
indices[x] = append(indices[x], i)
}

n := len(nums)
for x, p := range indices {
// 前后各加一个哨兵
i0 := p[0]
p = slices.Insert(p, 0, p[len(p)-1]-n)
indices[x] = append(p, i0+n)
}

for qi, i := range queries {
p := indices[nums[i]]
if len(p) == 3 {
queries[qi] = -1
} else {
j := sort.SearchInts(p, i)
queries[qi] = min(i-p[j-1], p[j+1]-i)
}
}
return queries
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n + q\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度,$q$ 是 $\textit{queries}$ 的长度。每次二分需要 $\mathcal{O}(\log n)$ 的时间。
  • 空间复杂度:$\mathcal{O}(n)$。返回值不计入。

方法二:预处理左右最近相同元素的下标

定义 $\textit{left}[i]$ 表示在 $i$ 左边的等于 $\textit{nums}[i]$ 的最近元素下标。注意数组是循环数组,我们可以像方法一那样,用 $-1$ 表示最后一个数的下标,$-2$ 表示倒数第二个数的下标,依此类推。

定义 $\textit{right}[i]$ 表示在 $i$ 右边的等于 $\textit{nums}[i]$ 的最近元素下标。注意数组是循环数组,我们可以像方法一那样,用 $n$ 表示第一个数的下标,$n+1$ 表示第二个数的下标,依此类推。

计算方式:

  • 从 $-n$ 循环到 $n-1$,同时用一个哈希表记录每个数的最新位置。当 $i\ge 0$ 时,$\textit{left}[i]$ 就是哈希中记录的 $\textit{nums}[i]$ 的位置。注意先更新 $\textit{left}[i]$ 再更新哈希表。
  • 从 $2n-1$ 循环到 $0$,同时用一个哈希表记录每个数的最新位置。当 $i < n$ 时,$\textit{right}[i]$ 就是哈希中记录的 $\textit{nums}[i]$ 的位置。注意先更新 $\textit{right}[i]$ 再更新哈希表。

答案为:

$$
\min(i-\textit{left}[i], \textit{right}[i]-i)
$$

如果上式等于 $n$,说明只有一个 $\textit{nums}[i]$,答案为 $-1$。

优化前

###py

class Solution:
    def solveQueries(self, nums: List[int], queries: List[int]) -> List[int]:
        n = len(nums)
        left = [0] * n
        pos = {}
        for i in range(-n, n):
            if i >= 0:
                left[i] = pos[nums[i]]
            pos[nums[i]] = i

        right = [0] * n
        pos.clear()
        for i in range(n * 2 - 1, -1, -1):
            if i < n:
                right[i] = pos[nums[i]]
            pos[nums[i % n]] = i

        for qi, i in enumerate(queries):
            l = left[i]
            queries[qi] = -1 if i - l == n else min(i - l, right[i] - i)
        return queries

###java

class Solution {
    public List<Integer> solveQueries(int[] nums, int[] queries) {
        int n = nums.length;
        int[] left = new int[n];
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = -n; i < n; i++) {
            if (i >= 0) {
                left[i] = pos.get(nums[i]);
            }
            pos.put(nums[(i + n) % n], i);
        }

        int[] right = new int[n];
        pos.clear();
        for (int i = n * 2 - 1; i >= 0; i--) {
            if (i < n) {
                right[i] = pos.get(nums[i]);
            }
            pos.put(nums[i % n], i);
        }

        List<Integer> ans = new ArrayList<>(queries.length);
        for (int i : queries) {
            int l = left[i];
            ans.add(i - l == n ? -1 : Math.min(i - l, right[i] - i));
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
        int n = nums.size();
        vector<int> left(n), right(n);
        unordered_map<int, int> pos;
        for (int i = -n; i < n; i++) {
            if (i >= 0) {
                left[i] = pos[nums[i]];
            }
            pos[nums[(i + n) % n]] = i;
        }

        pos.clear();
        for (int i = n * 2 - 1; i >= 0; i--) {
            if (i < n) {
                right[i] = pos[nums[i]];
            }
            pos[nums[i % n]] = i;
        }

        for (int& i : queries) {
            int l = left[i];
            i = i - l == n ? -1 : min(i - l, right[i] - i);
        }
        return queries;
    }
};

###go

func solveQueries(nums []int, queries []int) []int {
n := len(nums)
left := make([]int, n)
pos := map[int]int{}
for i := -n; i < n; i++ {
if i >= 0 {
left[i] = pos[nums[i]]
}
pos[nums[(i+n)%n]] = i
}

right := make([]int, n)
clear(pos)
for i := n*2 - 1; i >= 0; i-- {
if i < n {
right[i] = pos[nums[i]]
}
pos[nums[i%n]] = i
}

for qi, i := range queries {
l := left[i]
if i-l == n {
queries[qi] = -1
} else {
queries[qi] = min(i-l, right[i]-i)
}
}
return queries
}

优化(一)

一次遍历同时计算 $\textit{left}$ 和 $\textit{right}$。(类似单调栈)

###py

class Solution:
    def solveQueries(self, nums: List[int], queries: List[int]) -> List[int]:
        n = len(nums)
        left = [0] * n
        right = [0] * n
        pos = {}
        for i in range(-n, n):
            x = nums[i]
            if i >= 0:
                j = pos[x]
                left[i] = j
                # 对于左边的 j 来说,它的 right 就是 i
                if j >= 0:
                    right[j] = i
                else:
                    right[j + n] = i + n
            pos[x] = i

        for qi, i in enumerate(queries):
            l = left[i]
            queries[qi] = -1 if i - l == n else min(i - l, right[i] - i)
        return queries

###java

class Solution {
    public List<Integer> solveQueries(int[] nums, int[] queries) {
        int n = nums.length;
        int[] left = new int[n];
        int[] right = new int[n];
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = -n; i < n; i++) {
            if (i >= 0) {
                int j = pos.get(nums[i]);
                left[i] = j;
                // 对于左边的 j 来说,它的 right 就是 i
                if (j >= 0) {
                    right[j] = i;
                } else {
                    right[j + n] = i + n;
                }
            }
            pos.put(nums[(i + n) % n], i);
        }

        List<Integer> ans = new ArrayList<>(queries.length);
        for (int i : queries) {
            int l = left[i];
            ans.add(i - l == n ? -1 : Math.min(i - l, right[i] - i));
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
        int n = nums.size();
        vector<int> left(n), right(n);
        unordered_map<int, int> pos;
        for (int i = -n; i < n; i++) {
            if (i >= 0) {
                int j = pos[nums[i]];
                left[i] = j;
                // 对于左边的 j 来说,它的 right 就是 i
                if (j >= 0) {
                    right[j] = i;
                } else {
                    right[j + n] = i + n;
                }
            }
            pos[nums[(i + n) % n]] = i;
        }

        for (int& i : queries) {
            int l = left[i];
            i = i - l == n ? -1 : min(i - l, right[i] - i);
        }
        return queries;
    }
};

###go

func solveQueries(nums []int, queries []int) []int {
n := len(nums)
left := make([]int, n)
right := make([]int, n)
pos := map[int]int{}
for i := -n; i < n; i++ {
if i >= 0 {
j := pos[nums[i]]
left[i] = j
// 对于左边的 j 来说,它的 right 就是 i
if j >= 0 {
right[j] = i
} else {
right[j+n] = i + n
}
}
pos[nums[(i+n)%n]] = i
}

for qi, i := range queries {
l := left[i]
if i-l == n {
queries[qi] = -1
} else {
queries[qi] = min(i-l, right[i]-i)
}
}
return queries
}

优化(二)

上面的写法并不是真正的一次遍历,因为遍历了 $\textit{nums}$ 两次。

要做到真正的一次遍历,需要记录每个元素首次出现的位置和最后一次出现的位置。

###py

class Solution:
    def solveQueries(self, nums: List[int], queries: List[int]) -> List[int]:
        n = len(nums)
        left = [0] * n
        right = [0] * n
        first = {}  # 记录首次出现的位置
        last = {}  # 记录最后一次出现的位置
        for i, x in enumerate(nums):
            left[i] = j = last.get(x, -1)
            if j >= 0:
                right[j] = i
            if x not in first:
                first[x] = i
            last[x] = i

        for qi, i in enumerate(queries):
            l = left[i] if left[i] >= 0 else last[nums[i]] - n
            if i - l == n:
                queries[qi] = -1
            else:
                r = right[i] or first[nums[i]] + n
                queries[qi] = min(i - l, r - i)
        return queries

###java

class Solution {
    public List<Integer> solveQueries(int[] nums, int[] queries) {
        int n = nums.length;
        int[] left = new int[n];
        int[] right = new int[n];
        Map<Integer, Integer> first = new HashMap<>(); // 记录首次出现的位置
        Map<Integer, Integer> last = new HashMap<>(); // 记录最后一次出现的位置
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            left[i] = last.getOrDefault(x, -1);
            if (left[i] >= 0) {
                right[left[i]] = i;
            }
            first.putIfAbsent(x, i);
            last.put(x, i);
        }

        List<Integer> ans = new ArrayList<>(queries.length);
        for (int i : queries) {
            int l = left[i] >= 0 ? left[i] : last.get(nums[i]) - n;
            if (i - l == n) {
                ans.add(-1);
            } else {
                int r = right[i] > 0 ? right[i] : first.get(nums[i]) + n;
                ans.add(Math.min(i - l, r - i));
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
        int n = nums.size();
        vector<int> left(n), right(n);
        unordered_map<int, int> first, last; // 记录首次出现和最后一次出现的位置
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            left[i] = last.contains(x) ? last[x] : -1;
            if (left[i] >= 0) {
                right[left[i]] = i;
            }
            if (!first.contains(x)) {
                first[x] = i;
            }
            last[x] = i;
        }

        for (int& i : queries) {
            int l = left[i] >= 0 ? left[i] : last[nums[i]] - n;
            if (i - l == n) {
                i = -1;
            } else {
                int r = right[i] ? right[i] : first[nums[i]] + n;
                i = min(i - l, r - i);
            }
        }
        return queries;
    }
};

###go

func solveQueries(nums []int, queries []int) []int {
n := len(nums)
left := make([]int, n)
right := make([]int, n)
first := map[int]int{} // 首次出现的位置
last := map[int]int{}  // 最后一次出现的位置
for i, x := range nums {
j, ok := last[nums[i]]
if ok {
left[i] = j
right[j] = i
} else {
left[i] = -1
}
if _, ok := first[x]; !ok {
first[x] = i
}
last[x] = i
}

for qi, i := range queries {
l := left[i]
if l < 0 {
l = last[nums[i]] - n
}
if i-l == n {
queries[qi] = -1
} else {
r := right[i]
if r == 0 {
r = first[nums[i]] + n
}
queries[qi] = min(i-l, r-i)
}
}
return queries
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n + q)$,其中 $n$ 是 $\textit{nums}$ 的长度,$q$ 是 $\textit{queries}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。返回值不计入。

相似题目

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 【本题相关】二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

昨天 — 2026年4月15日首页

39.98 万元起,小鹏最贵 SUV GX 登场,要豪华有技术,要设计有堆料

作者 芥末
2026年4月15日 22:53

在首款车型发布 8 年之后,小鹏终于迎来了一辆真正能对标业内「9」系的旗舰 SUV 。

何小鹏在今天发布会一开始就先解释了一个问题,为什么这辆车取名叫 GX?

 G 代表了伟大,强大,卓越的气场,以及它是一台真正的全尺寸的旗舰 SUV;而 X 代表一种探索,代表小鹏向上的探索科技无人区的一种勇气,以及领先一代的技术。

因此这辆车除了在用料、配置上追求豪华之外,还是小鹏近些年技术积累的集大成者,线控底盘、图灵芯片、接近 Robotaxi 级别的智驾能力等都搭载到了这台 GX 上。

当然,与此同时,小鹏 GX 的预售价也创下了有史以来的新高——39.98 万元。

设计不行,堆料来凑

GX 延续了小鹏一贯的简洁科技感,展现出了全尺寸 SUV 应有的磅礴体量,其车身长宽高分别为 5265 毫米、1999 毫米和 1800 毫米,轴距达到了 3115 毫米 。

为了削弱超大尺寸可能带来的笨重视觉感,设计团队从游艇造型中汲取了灵感,将修长的船舷线条融入了车身。

侧面车窗区域,该车采用了隐藏式水切设计,将传统的窗框水切条收纳于车门内部,使得侧窗玻璃表面保持纯平无遮挡的状态 。其侧向车窗总面积达到 2.682 平方米,这种无遮挡的设计不仅优化了侧向视野,其平整的表面结合侧扰流板,还使得整车风阻有所降低。

车辆的前后造型同样遵循了极致简化与一体化的思路。小鹏 GX 前部搭载了面积达 2.275 平方米的蚌式机舱盖,该机舱盖向侧面一直延伸至轮眉区域 。车尾部分同样采用了蚌式尾门设计,尾门向后侧围两侧延伸,与后翼子板结构相连接 。

此外,为了凸显旗舰定位,小鹏 GX 在车头首次应用了太空陶瓷 24K 金标,该车标采用航天级氧化锆陶瓷与 PVD 真空镀金工艺,经过 33 天共 15 道工序的微米级 CNC 精雕而成。

在外观色彩上,该车提供了珠峰白、天山黑、峡湾灰、丹霞红以及哑光质感的日照金山五种配色 。

其中,哑光漆面采用了成本颇高的巴斯夫顶级哑光清漆与多层纳米结构涂层技术,并结合了恒温无尘环境下的 AI 机器人喷涂与局部人工打磨,成功复刻了雪峰的绒感金光。

灯光系统的设计是小鹏 GX 外部的一大亮点。

小鹏 GX 的车头配备了贯穿式日间行车灯,内部集成了 340 颗 LED 光源,并采用了多层光路设计 。车尾则布置了一条长度为 1.64 米的贯穿式尾灯,内置 380 颗 LED 光源。

车辆的前大灯组搭载了华为 DLP 投影技术,系统能够实现 130 万像素的精准光线控制与遮蔽 。在夜间会车等驾驶场景下,大灯可以通过视觉算法远距离识别对向或同向车辆,并自动遮挡远光照射范围内的目标。在高速行驶状态下,这套投影大灯可以将实时的跟车距离以图像或数字形式投射在路面上作为安全预警 。

此外,大灯还能在车辆进行自动变道、转弯或礼让行人时,将车辆的行驶意图以高达 2000 流明的亮度投射于地面,同时支持在停车状态下进行娱乐投影 。

如果说小鹏 GX 此次的外观设计还称得上「中规中矩」甚至颇有亮点,那 GX 的内饰,尤其是金色版本,则显得有点匹配不上 40 万元的价格。

仪表台、座椅、门板、顶衬几乎都压在同一套颜色里,主色和辅色没有拉开,深浅层次也不够,结果就是整个座舱的视觉重量被平均摊开了,车内该有的前后层次和重点区域都不够清楚。

正常来讲,这个价位的车,内饰通常会通过颜色、材质,或者明暗变化,把「背景」和「前景」区分出来,哪些部分负责铺陈氛围,哪些部分负责形成视觉焦点,一般都会处理得更明确。

GX 在这方面做得一塌糊涂,完全没有体现出旗舰车型该有的质感。

▲ 小鹏 P7 的内饰设计就没有这个问题

从后排视角观察,会更容易发现问题。GX 整个仪表台的纵深显得有些单薄,高低起伏也不明显,导致前舱的体量感偏弱。17.3 英寸中控屏更像是被强行塞进了台面里。

放在十几万级别的车上,这样处理可以理解,但是放在 GX 这种旗舰定位的车型上,就显得有点敷衍。

还有一个细节,GX 门板上的氛围灯不仅布局很碎,边界处理得也不够干净,视觉观感非常松散,对比同价位一些做得更成熟的竞品,无论是线性灯带,还是边界清晰的轮廓式发光,都会更容易做出「见光不见灯」的高级感。

总之 GX 的内饰很多地方都停在了「够用」上,离这个价位该有的设计感和精细度还有不少差距。

不过,设计不行,还可以靠堆料来凑,小鹏在座舱配置上倒是毫不吝啬。

首先小鹏 GX 的车内空间相当宽裕,车内垂向高度达到了 1311 毫米,并在第二排座椅之间预留了 180 毫米宽度的中央通道,以便于乘客进出第三排 。

副驾驶位及第二排右侧均配备了零重力座椅,第二排左侧则为准零重力座椅 。

这些座椅表面采用 NAPPA 真皮包裹,内置 16 点按摩模块,支持 14 向电动调节,并具备 3 档加热与通风功能,其中加热区域覆盖至电动腿托 。在前后排均有乘客乘坐的状态下,这些零重力座椅也能够独立进行倾倒展开 。

第三排座椅具备电动调节功能,靠背和头枕可以实现 0 至 180 度的无极调节,直至完全放平状态,同时坐垫和靠背均配备了加热功能 。

此外全车座椅内部均集成了 ABTS 与 PLP 双预紧双限力式安全带,坐垫下方设有防下潜气囊,并且座椅骨架带有机械式溃缩吸能机构,用以在发生碰撞时减轻乘员受力 。

小鹏 GX 此次还在第二排和第三排的侧窗区域使用了总面积达 1.88 平方米的调光隐私玻璃 。该玻璃采用染料液晶(LC)技术,能够在 0.16 秒内完成明暗透光率的切换 。

系统可以根据外部光照条件自动进行无级调光,例如在车辆驶出地下车库遭遇强光时自动调暗玻璃透光度 。在非隐私模式下,玻璃的雾度低于 4%,而在最暗状态下可隔绝 99.9% 的紫外线及 99.5% 的可见光 。

其他舒适性配置上,小鹏 GX 搭载了一台容积为 12.5 升的车载冰箱,内置了美的品牌的 PST 杀菌模块;空调出风口支持风向的 AI 自动调节功能,可根据舱内温度变化、阳光强度以及乘员的座椅位置和靠背角度自动改变送风模式 ;车顶部分还设置了出风面积为 30990 平方毫米的环绕式微孔出风口,内部包含 2000 多个微孔,用于进行无感通风降温 。

安全冗余、线控底盘、图灵芯片,小鹏有的都给了

安全冗余也是小鹏此次在 GX 上化了大力气去改进的一个方面。

被动安全层面,GX 车身应用了 16000 吨前后一体压铸工艺,以减少零部件拼接并提升车身抗扭刚性。车体结构在车头、车顶、侧面及底部(特别是电池包的防刮底与涉水保护)进行了强化。针对侧翻工况,车顶配备了 1500MPa 的热成型横梁。

此外,全车共布置 11 个安全气囊,涵盖了座椅内部集成的气囊与延伸至第三排的贯穿式侧气帘 。

主动安全层面,该车搭载的 AI 线控智能底盘融合了 XVMC 运动控制系统 ,将驱动、转向与悬架进行一体化协同控制,以提升冰雪湿滑或爆胎等极端路况下的姿态稳定性。车辆还配备了最高工作时速为 150km/h 的 AEB(自动紧急制动)系统及 130km/h 的 AES(自动紧急转向)系统。

针对驾驶员突发疾病等失能场景,车辆引入了专属辅助系统;当监测到驾驶员失去驾驶能力时,车辆会自动接管操控、打灯变道并靠边平稳停车,随后自动解锁车门并呼叫 SOS 救援。

底层架构上,该车型引入了航空级六重安全冗余设计 。

其中,线控转向系统与制动系统(包含线控液压与电子制动双保险 )均具备四重备份;驱动层面设置了双回路与双电机冗余,单侧故障可在 1 毫秒内由备份系统接管;通信系统应用了双总线传输;两套低压供电电源被集中布置于底盘中心区域以降低碰撞损毁概率;车门解锁则配备了主副电源结合内外机械拉手的四重独立方案,确保在极端断电状态下车门仍能正常开启。

小鹏能把 GX 定到 40 万元的价格,其中最主要的技术支撑是一套原生 AI 线控智能底盘系统 。

GX 的底盘标配了前双叉臂与后 H 臂独立悬架、可变阻尼减震器、智能四驱系统以及 AI 智能空气悬架 ,并且全面引入了全线控四轮转向系统。

这种软硬件解耦的设计打破了传统机械转向柱的硬连接物理限制,旨在优化大型 SUV 的转弯半径与操控稳定性,降低驾驶员的操作门槛。

在制动层面,该底盘应用了线控液压制动系统(EHB) 。传统机械制动在驾驶员踩下踏板至车辆卡钳建立制动力的过程中,存在不可避免的物理传动延迟;而线控底盘通过电信号传输,大幅缩短了系统指令的响应时间。

得益于这种响应机制,小鹏 GX 在 100km/h 至静止的紧急制动测试中,刹停距离被标定在 34.4 米。在高速跟车等紧急制动场景下,更短的刹车距离能够为车辆多保留约半个车身的安全缓冲空间,从而降低追尾风险。

除了改善人工驾驶的操控体验,原生线控底盘也是面向高阶自动驾驶的关键技术闭环。GX 的智能底盘深度融合了运动控制系统(XVMC) ,将驱动、转向与悬架进行了一体化协同控制。

这种底层执行机构的线控化,使得车辆在接收智驾系统的电控指令时,能够获得比传统机械底盘更快速、精准的动态响应,为后续高阶辅助驾驶及自动驾驶能力的落地提供了物理层面的执行基础。

而在辅助驾驶系统上,小鹏 GX 是基于 Robotaxi(自动驾驶出租车)标准进行研发的量产车型,未来将获得 Robotaxi 级别的智驾能力 。

该车型搭载了四颗「图灵」芯片,系统总有效算力达到全球领先的 3000 TOPS 。为满足高阶辅助驾驶对硬件的冗余要求,这四颗芯片采用了「3+1」的阵列架构,即在常规运算外预留一颗作为安全备份,以保障系统在局部故障下的稳定运行。

在功能应用上,部分处于研发前沿的智驾能力在 GX 上得到了量产落地。

如新增的「园区漫游」功能,旨在摆脱对高精地图、固定记忆路线或规则算法的过度依赖。该系统能够在无明确道路标线的封闭园区或公共停车场内,进行自主的路径规划与寻位。

而在交互上,车辆本地部署了端侧 VLM(视觉语言)大模型。与依赖云端处理的模型不同,端侧大模型可在无网络覆盖的地下车库等环境中,提供指令理解与反馈,并在数据处理上兼顾了本地隐私安全。

此外,伴随智驾系统的后续升级,小鹏 GX 规划将逐步拓展高速服务区无接管自主泊车、基于端侧 VLM 的全量语音控车以及与车外智能灯语联动等进阶功能。

小鹏 GX 的推出,在很大程度上肩负着填补品牌「真旗舰 SUV」市场空白的战略重任。

小鹏 G9 虽然名为 9 系,但全系缺失「冰箱、彩电、大沙发」等配置,零重力座椅、车载冰箱、HUD 抬头显示等统统没有,无法满足消费者更高层的家用需求。

加之,小鹏在车系间的配置管理上做得相当糟糕。

比如原本想买 G9 的人转了一圈之后发现,几乎感知不到小鹏 G9 和 G7 之间的差距,而 G7 价格更便宜,也更适合家用。

这种内部定位的模糊直接反映在了终端数据上,使得 G9 目前的月销量仅徘徊在三位数,是目前小鹏车型中最差的那个。

▲ 小鹏各车型近半年销量,数据来源:汽车之家

这种「缺乏长线畅销车、销量高度依赖新车上市初期热度」的现象,在小鹏的其他车型上亦有体现,本质上反映了小鹏无论在产品定义,还是在用户口碑上,都有不少课要补。

将目光投向外部,小鹏 GX 所面临的市场竞争环境同样极度严苛。

在过去一年中,国内大六座 SUV 赛道呈现出极其拥挤的态势,从 15 万元直至 60 万元区间,各路竞品层出不穷,且在智能化、空间利用率以及续航表现上均展现出了极强的综合实力。

在竞争已经足够拥挤的 40 万元级市场里,小鹏 GX 面对的压力本来就不小。更关键的是,作为一台冲击高端定位的车型,它在内饰豪华感和层次营造上,还没有完全撑起这个价位应有的说服力。

内外压力同时摆在面前,GX 这一仗,会比想象中更加艰难。

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

第2章:声明式 UI 基础

作者 90后晨仔
2026年4月15日 22:22

2.1 声明式 vs 命令式 UI 对比

命令式 UI(UIKit)

命令式编程是一种传统的编程范式,开发者需要明确告诉计算机“如何”完成任务。在 UIKit 中,你需要:

  1. 创建视图对象
  2. 配置视图属性
  3. 添加视图到视图层级
  4. 设置布局约束
  5. 手动更新视图状态

示例代码(UIKit)

import UIKit

class ProfileViewController: UIViewController {
    private let nameLabel = UILabel()
    private let avatarImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 创建视图
        view.backgroundColor = .white
        
        // 2. 配置 nameLabel
        nameLabel.text = "张三"
        nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium)
        nameLabel.textColor = .black
        nameLabel.textAlignment = .center
        
        // 3. 配置 avatarImageView
        avatarImageView.image = UIImage(named: "avatar")
        avatarImageView.contentMode = .scaleAspectFill
        avatarImageView.layer.cornerRadius = 40
        avatarImageView.clipsToBounds = true
        
        // 4. 添加到视图层级
        view.addSubview(avatarImageView)
        view.addSubview(nameLabel)
        
        // 5. 设置约束
        avatarImageView.translatesAutoresizingMaskIntoConstraints = false
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            avatarImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
            avatarImageView.widthAnchor.constraint(equalToConstant: 80),
            avatarImageView.heightAnchor.constraint(equalToConstant: 80),
            
            nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20),
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
    
    // 6. 更新数据时需要手动刷新 UI
    func updateProfile(name: String, avatar: String) {
        nameLabel.text = name
        avatarImageView.image = UIImage(named: avatar)
    }
}

声明式 UI(SwiftUI)

声明式编程是一种现代的编程范式,开发者只需要描述“是什么”,而不需要关心“如何”实现。在 SwiftUI 中,你只需要:

  1. 描述界面的结构
  2. 绑定状态
  3. 系统自动处理更新

示例代码(SwiftUI)

import SwiftUI

struct ProfileView: View {
    let name: String
    let avatar: String
    
    var body: some View {
        VStack(spacing: 20) {
            // 1. 头像
            Image(avatar)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 80, height: 80)
                .clipShape(Circle())
            
            // 2. 姓名
            Text(name)
                .font(.system(size: 18, weight: .medium))
                .foregroundStyle(.primary)
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(.systemBackground))
    }
}

// 使用示例
ProfileView(name: "张三", avatar: "avatar")

核心差异

维度 命令式 UI(UIKit) 声明式 UI(SwiftUI)
代码量 多(需要手动管理所有细节) 少(只描述结果)
状态同步 手动更新 UI 自动同步状态
可读性 较低(逻辑分散) 高(逻辑集中)
维护性 较低(容易遗漏更新) 高(状态驱动自动更新)
错误率 较高(手动操作容易出错) 较低(框架保证一致性)
开发效率 低(重复代码多) 高(简洁明了)

2.2 View 协议与 body 计算属性

View 协议

在 SwiftUI 中,所有的视图都必须遵循 View 协议:

public protocol View {
    associatedtype Body : View
    @ViewBuilder var body: Self.Body { get }
}

核心概念

  • associatedtype Body:关联类型,表示视图的内容类型
  • body:计算属性,返回视图的内容
  • @ViewBuilder:属性包装器,允许使用声明式语法组合多个视图

body 计算属性

body 是 SwiftUI 视图的核心,它是一个计算属性,每次状态变化时都会重新计算。

重要特性

  1. 计算属性:不是存储属性,每次访问都会重新计算
  2. 轻量级:应该保持简洁,避免复杂计算
  3. 返回类型:必须返回一个遵循 View 协议的类型
  4. 自动合成:@ViewBuilder 允许使用简洁的语法组合多个视图

示例

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, SwiftUI!")
            Button("Tap Me") {}
        }
    }
}

理解 some View

some View 是一个不透明类型(Opaque Type),它表示:

  • 这个函数返回一个遵循 View 协议的类型
  • 但具体是什么类型,不需要暴露给调用者
  • 编译器可以进行更多的优化

2.3 结构体视图与值类型

视图是结构体

在 SwiftUI 中,视图是使用结构体(struct)实现的,这与 UIKit 中的类(class)不同。

结构体的优势

  1. 值类型:传递时会复制,避免引用计数问题
  2. 轻量级:分配在栈内存上,创建和销毁成本低
  3. 不可变:默认不可变,状态通过 @State 等包装器管理
  4. 线程安全:值类型天生线程安全

结构体的生命周期

SwiftUI 视图的生命周期与结构体的实例化无关:

  • 结构体可能被频繁创建和销毁
  • 但底层的真实视图对象由 SwiftUI 管理
  • 视图的身份(Identity)由其在视图树中的位置决定

示例

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

count 改变时:

  1. SwiftUI 检测到状态变化
  2. 重新创建 CounterView 结构体实例
  3. 调用 body 计算属性生成新的视图树
  4. 与旧视图树对比,只更新变化的部分

2.4 修饰符(Modifier)的使用

什么是修饰符?

修饰符是 SwiftUI 中用于修改视图属性和行为的方法。它们通常以点语法链式调用。

修饰符的工作原理

修饰符不是直接修改原视图,而是返回一个新的视图,这个新视图包含了原视图和应用的修改。

示例

Text("Hello")
    .font(.largeTitle)        // 返回一个新的 Text 视图,字体为 largeTitle
    .foregroundStyle(.blue)    // 返回一个新的视图,文本颜色为蓝色
    .padding()                // 返回一个新的视图,带有内边距

常用修饰符

布局修饰符

  • padding():添加内边距
  • frame():设置视图大小和对齐方式
  • background():设置背景
  • foregroundStyle():设置前景样式(颜色、渐变等)
  • clipShape():裁剪视图形状
  • overlay():在视图上叠加内容

排版修饰符

  • font():设置字体
  • bold():加粗文本
  • italic():斜体文本
  • multilineTextAlignment():多行文本对齐
  • lineLimit():限制文本行数

交互修饰符

  • onTapGesture():添加点击手势
  • disabled():禁用视图
  • accessibility():添加无障碍支持

动画修饰符

  • animation():添加动画
  • transition():添加转场动画

修饰符的顺序

修饰符的顺序很重要,因为每个修饰符都会作用于前一个修饰符返回的视图。

示例

// 先设置背景,再添加内边距
Text("Hello")
    .background(Color.blue)
    .padding()

// 先添加内边距,再设置背景
Text("Hello")
    .padding()
    .background(Color.blue)

这两种写法会产生不同的效果,第一种背景只覆盖文本区域,第二种背景会覆盖整个内边距区域。

实战:创建一个信息卡片

需求分析

创建一个包含以下元素的信息卡片:

  1. 标题
  2. 副标题
  3. 描述文本
  4. 图标
  5. 卡片样式(圆角、阴影)

代码实现

import SwiftUI

struct InfoCardView: View {
    // 卡片数据
    let title: String
    let subtitle: String
    let description: String
    let iconName: String
    let iconColor: Color
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // 图标和标题区域
            HStack(alignment: .center, spacing: 12) {
                // 图标
                Circle()
                    .fill(iconColor.opacity(0.1))
                    .frame(width: 48, height: 48)
                    .overlay {
                        Image(systemName: iconName)
                            .resizable()
                            .scaledToFit()
                            .frame(width: 24, height: 24)
                            .foregroundStyle(iconColor)
                    }
                
                // 标题和副标题
                VStack(alignment: .leading, spacing: 4) {
                    Text(title)
                        .font(.headline)
                        .fontWeight(.semibold)
                    Text(subtitle)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
            }
            
            // 描述文本
            Text(description)
                .font(.body)
                .foregroundStyle(.primary)
                .lineLimit(nil) // 不限制行数
        }
        .padding(16) // 卡片内边距
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(Color(.systemBackground))
                .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
        )
        .padding(.horizontal, 16) // 卡片外边距
    }
}

// 使用示例
struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            InfoCardView(
                title: "SwiftUI 简介",
                subtitle: "现代 UI 框架",
                description: "SwiftUI 是一个声明式 UI 框架,允许开发者使用 Swift 语言创建跨 Apple 平台的用户界面。它提供了简洁、直观的语法,使 UI 开发变得更加高效。",
                iconName: "star.fill",
                iconColor: .yellow
            )
            
            InfoCardView(
                title: "声明式编程",
                subtitle: "现代编程范式",
                description: "声明式编程让开发者只需要描述界面的样子,而不需要关心如何实现。系统会自动处理视图的创建、更新和销毁。",
                iconName: "code",
                iconColor: .blue
            )
            
            InfoCardView(
                title: "跨平台",
                subtitle: "一次编写,多处运行",
                description: "SwiftUI 支持 iOS、iPadOS、macOS、watchOS 和 tvOS,让你的代码可以在所有 Apple 平台上运行。",
                iconName: "globe",
                iconColor: .green
            )
        }
        .padding(.vertical, 16)
        .background(Color(.systemGroupedBackground))
    }
}

#Preview {
    ContentView()
}

代码解析

  1. 结构体参数:通过参数传递卡片数据,使视图可复用
  2. VStack 和 HStack:使用栈布局组织视图
  3. Circle:创建圆形背景
  4. overlay:在圆形上叠加图标
  5. RoundedRectangle:创建圆角矩形背景
  6. shadow:添加阴影效果
  7. spacing:设置栈视图的间距
  8. alignment:设置栈视图的对齐方式

小结

本章介绍了声明式 UI 的基础概念,包括:

  • 声明式 vs 命令式 UI 的对比
  • View 协议与 body 计算属性
  • 结构体视图与值类型的特性
  • 修饰符的使用方法和顺序
  • 一个信息卡片的实战实现

通过本章的学习,你已经了解了 SwiftUI 的基本工作原理和核心概念,为后续的学习打下了坚实的基础。

参考资料

第1章:SwiftUI 与开发环境简介

作者 90后晨仔
2026年4月15日 21:55

距离上一次学习SwiftUI已经过去几年的时间了,好多知识点都些忘记了,最近刚好有有一些时间就好好的在从零回顾一下吧。


1.1 什么是 SwiftUI?

官方定义

根据 Apple 官方文档,SwiftUI 是一个现代的声明式 UI 框架,它允许开发者使用 Swift 语言创建跨 Apple 平台的用户界面。

SwiftUI 的核心优势

  1. 声明式语法:描述界面“是什么”而不是“怎么做”
  2. 跨平台:一次编写,在 iOS、iPadOS、macOS、watchOS 和 tvOS 上运行
  3. 实时预览:在 Xcode 中实时查看界面效果
  4. 与 Swift 语言深度集成:充分利用 Swift 的类型安全和现代特性
  5. 自动适配:自动处理不同尺寸设备的布局

与 UIKit 的对比

特性 SwiftUI UIKit
编程范式 声明式 命令式
代码风格 简洁、直观 冗长、命令式
布局系统 自动布局,基于栈 Auto Layout,需要手动设置约束
状态管理 自动状态同步 手动更新 UI
跨平台 支持所有 Apple 平台 主要针对 iOS/tvOS
开发效率 相对较低

1.2 Xcode 开发环境配置

系统要求

  • macOS:最新版本(推荐 macOS Sonoma 或更高)
  • Xcode:最新版本(推荐 Xcode 15 或更高)
  • Swift:Swift 5.7 或更高
  • iOS:iOS 15.0 或更高(如果需要支持旧版本,最低可到 iOS 13.0)

安装 Xcode

  1. 打开 App Store
  2. 搜索 “Xcode”
  3. 点击 “获取” 进行安装
  4. 安装完成后,打开 Xcode 并同意许可协议

安装额外组件

首次打开 Xcode 时,会提示安装额外的组件,包括:

  • 命令行工具
  • 模拟器运行时
  • 其他必要的开发工具

1.3 创建你的第一个 SwiftUI 项目

步骤 1:打开 Xcode

步骤 2:创建新项目

  1. 点击 “Create a new Xcode project”
  2. 选择 “iOS” 标签页
  3. 选择 “App” 模板
  4. 点击 “Next”

步骤 3:配置项目信息

  1. Product Name:输入项目名称,例如 “SwiftUIHelloWorld”
  2. Team:选择你的开发团队(如果没有,可以选择 “None”)
  3. Organization Identifier:输入你的组织标识符,例如 “com.yourname”
  4. Interface:选择 “SwiftUI”
  5. Language:选择 “Swift”
  6. Life Cycle:选择 “SwiftUI App”
  7. 取消勾选 “Use Core Data”(暂时不需要)
  8. 点击 “Next”

步骤 4:选择保存位置

选择一个合适的文件夹保存项目,然后点击 “Create”

步骤 5:项目结构介绍

创建完成后,你会看到以下文件结构:

  • SwiftUIHelloWorldApp.swift:应用程序入口
  • ContentView.swift:主视图
  • Assets.xcassets:资源文件
  • Info.plist:应用配置

1.4 认识 Xcode 预览功能

预览面板

Xcode 右侧的预览面板是 SwiftUI 最强大的特性之一,它允许你实时查看界面效果。

使用预览

  1. 打开 ContentView.swift 文件
  2. 确保右侧的预览面板可见(如果不可见,点击 Xcode 顶部的 “Editor” → “Canvas”)
  3. 你会看到 ContentView 的实时预览
  4. 修改代码,预览会自动更新

预览配置

你可以在预览代码中添加多个预览,例如:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .previewDisplayName("默认预览")
            
            ContentView()
                .previewDisplayName("暗黑模式")
                .preferredColorScheme(.dark)
            
            ContentView()
                .previewDisplayName("iPhone 15 Pro")
                .previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro"))
        }
    }
}

预览快捷键

  • Option + Command + P:刷新预览
  • Command + K:清除构建

1.5 SwiftUI 项目结构解析

应用程序入口

SwiftUIHelloWorldApp.swift 是应用的入口点,它定义了应用的结构:

import SwiftUI

@main
struct SwiftUIHelloWorldApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

主视图

ContentView.swift 是应用的主视图:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

关键概念解析

  1. @main:标记应用程序的入口点
  2. App 协议:定义应用的结构
  3. Scene:表示应用的一个场景
  4. WindowGroup:创建应用的窗口
  5. View 协议:所有 SwiftUI 视图必须遵循的协议
  6. body:计算属性,返回视图的内容
  7. #Preview:Xcode 15+ 的新语法,用于创建预览

资源管理

  • Assets.xcassets:管理应用的图片、颜色等资源
  • Info.plist:应用的配置信息,如应用名称、版本号等

实战:创建一个简单的欢迎页面

需求分析

创建一个包含以下元素的欢迎页面:

  1. 应用图标
  2. 应用名称
  3. 欢迎标语
  4. 开始按钮

代码实现

import SwiftUI

struct WelcomeView: View {
    // 状态变量,用于控制是否显示欢迎页面
    @State private var isWelcomeShown = true
    
    var body: some View {
        if isWelcomeShown {
            VStack(spacing: 20) {
                // 应用图标
                Image(systemName: "star.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 100, height: 100)
                    .foregroundStyle(.yellow)
                
                // 应用名称
                Text("欢迎使用 SwiftUI")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                // 欢迎标语
                Text("一个现代、简洁的 UI 框架")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                
                // 开始按钮
                Button("开始探索") {
                    // 点击按钮后隐藏欢迎页面
                    isWelcomeShown = false
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(
                LinearGradient(
                    colors: [.blue.opacity(0.1), .purple.opacity(0.1)],
                    startPoint: .top,
                    endPoint: .bottom
                )
            )
        } else {
            // 主内容页面
            VStack {
                Text("探索 SwiftUI 的世界!")
                    .font(.title)
                Text("这里是你的应用主界面")
                    .foregroundStyle(.secondary)
            }
            .padding()
        }
    }
}

#Preview {
    WelcomeView()
}

代码解析

  1. @State:用于创建视图的本地状态
  2. VStack:垂直堆叠视图
  3. Image:显示图片,使用系统图标
  4. Text:显示文本
  5. Button:创建按钮,带有点击动作
  6. LinearGradient:创建线性渐变背景
  7. 条件渲染:使用 if-else 控制显示不同的内容

小结

本章介绍了 SwiftUI 的基本概念和开发环境搭建,包括:

  • SwiftUI 的核心优势和与 UIKit 的对比
  • Xcode 的安装和配置
  • 创建第一个 SwiftUI 项目的步骤
  • Xcode 预览功能的使用
  • SwiftUI 项目的基本结构
  • 一个简单欢迎页面的实现

通过本章的学习,你已经了解了 SwiftUI 的基本概念和开发环境,为后续的学习打下了基础。

参考资料

氪星晚报|Snap宣布将裁员约1000人;字节启动新一轮期权回购;苹果CEO库克斥资106万美元增持耐克

2026年4月15日 21:53

大公司:

Snap宣布将裁员约1000人

4月15日,社交媒体公司Snap宣布一系列调整,这些调整将影响Snap约1000名团队成员,包括16%的全职员工,此外,公司还将关闭300多个空缺职位。通过调整,公司预计到2026年下半年,年度成本将减少超过5亿美元。Snap表示,对即将离职的美国团队成员,公司将提供四个月的遣散费、医疗保险和股权激励,以及职业转型支持。在美国以外地区,公司将遵循当地流程,并努力提供符合当地规范的同等支持。

英特尔据悉将在未来几周向员工披露参与马斯克Terafab项目的细节

根据英特尔(CEO陈立武上周发出的一份备忘录,该公司计划在“未来几周”向员工披露其参与埃隆·马斯克雄心勃勃的“Terafab”芯片制造项目的“范围和性质”。陈立武上周五向英特尔员工发送了这份备忘录,此前两天,这家半导体巨头宣布与马斯克的公司——SpaceX、xAI 和特斯拉——共同参与Terafab项目,这对于正在经历最新转型尝试的英特尔来说,可能是一个福音。

特斯拉高管称上海工厂将为擎天柱机器人量产提供“金钥匙”

特斯拉中国区总经理王昊表示,特斯拉最大的生产基地——上海超级工厂未来有潜力生产人形机器人。该工厂的高效生产和创新能力被视为推动CEO埃隆·马斯克快速实现该技术商业化的关键因素。王昊在周二的一次发布会上表示,CEO马斯克曾指出,大规模生产是制造人形机器人的一个关键挑战。他认为上海制造部门是“解决这一挑战的金钥匙”,但没有具体说明运营将如何支持公司的机器人业务。

字节启动新一轮期权回购

字节发布内部邮件,启动最新一轮期权回购。其中:在职员工:229.5美元/股(约人民币1588元)离职员工:201.96美元/股(约人民币1397元)。字节上一轮回购期权时间是2025年10月,此次在职员工的回购价对比上一轮提升约14.5%。在2025年10月的回购中,在职员工回购价200.41美元,离职员工180.37美元。

广电总局发布微短剧精品创作传播计划

15日在成都开幕的第13届中国网络视听大会上,国家广播电视总局发布微短剧精品创作传播计划,并启动“长征:我们的故事”主题微短剧创作展播活动。广电总局相关负责人表示,我国微短剧已由粗放式增长进入到繁荣发展的关键阶段,正处于迈向精品化、数智化、产业化、国际化的战略机遇期。广电总局牵头构建“总局引领、省局支持、平台激励”的创作引导体系,启动微短剧精品创作传播计划,推动微短剧高质量发展。

哈啰回应超量投放共享单车被行政处罚

4月15日,针对哈啰违规超量投放共享单车被立案调查和约谈处罚一事,哈啰公司方面回应称,哈啰骑行北京分公司在新车置换与跨区域调度中,运力调度及响应效率上存在管理短板,未能将工作做实做细。对此,哈啰表示已启动全面整改:连夜增派运维力量,恢复重点区域的停放秩序,同时启动内部管理机制的调整优化。哈啰称,接受北京市主管部门的约谈与指导,并以此次整改为契机,深耕精细化运营,向主管部门及社会公众汇报进度。

苹果提高印度iPhone产量,塔塔集团组装份额应能达到10-15%

根据Counterpoint的数据,印度在全球组装业务中的份额从2022年的5.8%上升至2025年的22.2%。到2026年,这一份额将达到25-30%,到2027年将达到30-35%。鸿海仍是印度最大的组装商,其次是塔塔集团,该集团于2023年收购了纬创资通的工厂,并于2024年收购了和硕在当地工厂60%的股份。未来两年,塔塔集团的iPhone组装份额应能达到10-15%。

抖音集团将投入5亿专项资金扶持真人短剧

4月15日,2026年第十三届中国网络视听大会上,抖音集团宣布将投入5亿元专项资金扶持真人短剧内容创新与现实题材深耕。红果短剧总编辑乐力表示,真人短剧是行业高质量发展的“进行时”与“未来时”。只有各方积极引导扶持真人创作,才能让短剧行业行稳致远。

新产品:

千问AI眼镜S1宣布正式开售

36氪获悉,4月15日,千问AI眼镜S1宣布正式开售,最低到手价为3499元。该产品支持屏幕显示、1200万像素摄像头、热插拔换电池,并为全天候AI服务在整机层面进行了系统性重。

投融资:

“达美盛(DMS)”完成新一轮过亿元股权融资

36氪获悉,近日,工业智能化解决方案提供商“达美盛(DMS)”宣布完成了新一轮过亿元股权融资。本轮融资由天津智汇基金领投,中集资本、北洋海棠基金、中控技术等多家机构跟投。融资资金将主要用于工业智能体平台等核心技术研发、市场拓展以及生态建设。

苹果CEO库克斥资106万美元增持耐克

当地时间4月14日,耐克提交至美国证券交易委员会(SEC)的文件显示,苹果首席执行官蒂姆·库克于4月10日以42.43美元每股价格买入25,000股耐克股票,斥资约106万美元。

今日观点:

马斯克:特斯拉AI5芯片成功流片,将成为产量最高AI芯片之一

特斯拉首席执行官埃隆·马斯克4月15日宣布,特斯拉AI芯片设计团队已成功完成AI5芯片的流片。他同时透露,性能更强大的AI6、Dojo3以及其他多款芯片也正在研发中。马斯克表示,感谢合作伙伴台积电和三星在芯片量产过程中的支持,“AI5芯片未来将成为全球产量最大的AI芯片之一”。

其他值得关注的新闻:

李成钢会见美国福特汽车公司全球执行副总裁兼首席政务官柯立

4月13日,商务部国际贸易谈判代表兼副部长李成钢在京会见美国福特汽车公司全球执行副总裁兼首席政务官柯立。双方就中美经贸关系、福特公司在华发展等进行了交流。

科大讯飞发布AstronClaw智能体矩阵

2026年4月15日 20:59
36氪获悉,科大讯飞在广州举办AstronClaw升级发布会,推出全新软硬一体智能体矩阵。硬件端,讯飞办公本、AI眼镜、Guide01机器人全面接入Claw能力,WallEX、NOVA智能空间产品同步亮相。软件与生态层面,Loomy新增Buddy分身协作功能,招采Claw、陪练Skill等垂直场景应用全新升级,并发布企业级开源技能仓库SkillHub,构建覆盖个人办公、企业服务、家庭生活的全场景智能体生态。

东风天元智舱Plus平台搭载黑芝麻智能武当C1296芯片,打造首个本土舱驾一体量产化平台

2026年4月15日 20:53
4月15日,黑芝麻智能宣布,东风天元智舱Plus舱驾一体量产化平台搭载其武当C1296芯片,双方达成平台级合作。作为首个本土舱驾一体量产化平台,天元智舱Plus以单芯片同时支持智能座舱、L2+行车辅助及FAPA泊车功能,将率先搭载于东风集团旗下标杆车型东风奕派007,未来有望搭载东风全系车型,并计划2026年内至2027年持续量产。

猪价大降超30%,处7年低点

2026年4月15日 20:49
据农业农村部监测,4月份第二周全国生猪价格继续走低,已经连续10周下降,处于七年来的低点。农业农村部数据显示,4月份第二周全国生猪价格为每公斤10.03元,环比下降3.6%,同比下降33.7%。记者了解到,由于价格连创新低,生猪养殖已经处于亏损当中,部分养殖户开始淘汰产能。中国农业科学院农业经济与发展研究所研究员王祖力表示,短期来看,猪价仍处在一个“磨底期”。养殖场户每出栏一头商品猪,头均亏损超过300元。(央视财经)

宁德时代:二季度产能利用率预计维持在八到九成以上

2026年4月15日 20:42
在今日举行的宁德时代一季度业绩说明会上,公司管理层表示,动力和储能订单排产饱和,前期扩产起到成效, 但是完全释放产能还需要时间。目前公司产能利用率整体比较高,大概在八到九成的区间,二季度预计也维持在这个水平线之上。(财联社)

美格智能:拟3亿元投建AI研发及先进制造产业项目

2026年4月15日 20:39
36氪获悉,美格智能公告,公司与江苏省南通市北高新技术产业开发区管理委员会4月15日签署了《美格智能AI研发及先进制造产业项目合作协议书》,公司或公司全资子公司拟在南通市北高新技术产业开发区购置约46亩工业用地,建立研发中心及先进工艺中试、制造基地,投资建设美格智能AI研发及先进制造产业项目,预计项目总投资金额3亿元。
❌
❌