普通视图

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

OpenAI 或成史上烧钱最快的公司;特斯拉在印度两月,仅卖出100多辆车;《疯狂动物城2》刷新进口电影单日票房纪录|极客早知道

2025年11月30日 08:26

贾跃亭称生命中最重要的事是还债回国

11 月 29 日,贾跃亭表示,自己生命中最重要的事情就是还债回国。「还债回国是我生命中最重要的两件事之一。」日前,贾跃亭通过其个人社交平台宣布成立第二个债权人信托,旨在「加速偿还国内担保债务尽责到底早日回国」。

对此,一位不愿透露姓名的资本市场资深人士认为,贾跃亭此举更多的是博眼球、追流量。「到现在,也没有看到他在还(国内债务)。」

面对外界质疑和评价,贾跃亭称,不论外界怎么评价,其生命中只有两件事:「一个是打造 EAI 生态,给社会带来变革性的驱动作用,另一个是还清国内债务,早日回到祖国。」

据悉,法拉第未来(Faraday Future)管理层已批准了 FF 和 FX 品牌的五年商业计划,并已将其提交给董事会审核。按照公司的计划,未来五年累计产量目标为 40 万至 50 万辆。(来源:cnBeta)

OpenAI 可能成为史上「烧钱最快」的科技公司

11 月 29 日,据新智元报道,根据多份基于微软财报的数据揭示,OpenAI 的推理成本正在以远超收入的速度膨胀,可能成为史上「烧钱最快」的科技公司。

来自 2024 Q1 至 2025 Q3 的数据表明,OpenAI 的推理支出(即运行模型生成回答的成本)呈爆炸式增长:2025 年 Q3 支出飙升至 36.5 亿美元,而同期隐含收入仅 20.6 亿美元。这意味着 每赚 1 美元,OpenAI 要花 1.8 美元用于推理;2025 年 Q1 的这一比值甚至一度突破 2.0,陷入「越卖越亏」的循环。

年度层面,问题更为突出:2025 年前 9 个月的推理支出达 86.7 亿美元,是 2024 全年的 2.3 倍;同期收入仅增长 75%。2025 年前 9 月亏损已达 43.4 亿美元,远超上一年。

更令人担忧的是,媒体长期高估了 OpenAI 的营收。2024 年,微软财报反推收入为 24.7 亿美元,而媒体普遍报道为 37~40 亿美元;2025 年差距进一步拉大,仅上半年误差就达到 20 亿美元。这意味着外界对 OpenAI 的成长速度和商业化能力可能存在系统性高估。(来源:新智元)

 

研究揭示 OpenAI Sora2 监管漏洞:13 岁账号即可轻易生成「校园枪击」视频

11 月 29 日,据外媒 Futurism 报道,消费者监督组织 Ekō发布了一份名为《OpenAI Sora2:危害的新边界》的报告,展示研究人员使用以 13 岁和 14 岁青少年身份注册的账号生成的画面,画面包括青少年吸食违禁品和在公共区域或学校走廊持枪。

Ekō警告称,而 Sora2 生成暴力与令人不安内容的难度极低,这种内容很容易病毒式传播,从而将会进一步加剧风险。

报告明确指出,这些内容全部违反了 OpenAI 的使用政策和 Sora 的分发准则。

即便账号不主动生成内容,Sora2 的「For You」和「Latest」推荐页面也会推送令人震惊的视频,包括刻板化描绘犹太人或黑人、枪战场面以及性暴力等画面。(来源:IT之家)

 

iPhone 17 助力苹果手机 10 月份额创历史新纪录,中国市场贡献最大

11 月 29 日,在 iPhone 17 系列的助力下,苹果手机 10 月份额创下了历史新纪录。调研机构 CounterPoint Research 报告显示,得益于 iPhone 17 系列(尤其是标准版)在中国、美国等市场的强劲表现,苹果 2025 年 10 月 iPhone 销量同比增长 12%,终端销量份额飙升至 24.2%,创下历史新纪录。

报告中指出,中国成为苹果 10 月增长最快的市场。数据显示,在首发后的六周时间里,iPhone 系列在中国市场的销量同比激增 47%,远超美国(+11%)和日本(+8%)。

Counterpoint 高级分析师 Ivan Lam 指出,除了产品力本身,特殊的市场节点也助推了这一增长:新冠时期的换机潮达到顶峰,加上「双 11」大促提前开启,以及中秋国庆假期的销售热度延续到了 10 月。当然,iPhone Air 的销售就暗淡了很多,跟 iPhone 17 系列相比,完全不值得一提。(来源:cnBeta)

 

特斯拉登陆印度市场两个多月,仅卖出 100 多辆车

11 月 29 日,特斯拉想要拿下全球最大的汽车市场之一——印度,但现阶段的表现并不理想。

据外媒 Insideevs 报道,印度是全球第四大汽车市场,特斯拉今年 7 月终于在印度开出首家展厅。不过根据经销商提供的数据,特斯拉在当地运营两个多月,仅卖出 100 多辆车。

截至 9 月中旬,印度市场的 Model Y(目前在当地唯一正式销售的特斯拉车型)刚刚收到 600 多份订单,但真正转化为交付的只有极少部分。反观更昂贵的宝马、比亚迪和奔驰,由于享受税收减免,销量表现反而更强。

特斯拉最近又在北部城市古尔冈启用了更大的综合据点,集展厅、充电枢纽与售后服务于一体,品牌曝光度因此有所提升。但要提高销量,特斯拉仍有大量工作要做。

首先,印度市场的 Model Y 售价高达 70000 美元(现汇率约合 49.6 万元人民币),已进入豪华车区间。其次,全国范围内只有两座 Supercharger 直流快充站,分别位于孟买和新德里,后续仍计划陆续增加站点。(来源:IT之家)

北京 AI 产业规模今年将超 4500 亿元

11 月 29 日,2025 中国人工智能大会暨全国人工智能学院院长(系主任)年会开幕。开幕式上,北京市科委、中关村管委会发布《北京人工智能产业白皮书(2025)》,从全球和国家战略角度总结了人工智能发展现状,系统梳理了北京人工智能的创新资源和产业总体情况,并研判了未来发展趋势,推动北京市加快建设具有全球影响力的人工智能创新策源地和产业高地。其中提出,初步估算今年全年本市人工智能产业规模有望超过 4500 亿元。

数据显示,2025 年上半年,北京人工智能核心产业规模 2152.2 亿元,同比增长 25.3%。初步估算 2025 年全年,产业规模有望超过 4500 亿元。AI 企业超 2500 家,已备案大模型 183 款,持续保持全国第一。产业链日趋完整,形成了具有全球竞争力的产业生态。同时商业化路径也逐渐清晰,百度、抖音等公司的营收和产品活跃用户数均创新高。(来源:新浪科技)

 

打击虚拟货币交易炒作工作协调机制会议召开

11 月 28 日,中国人民银行召开打击虚拟货币交易炒作工作协调机制会议。会议强调,虚拟货币不具有与法定货币等同的法律地位,不具有法偿性,不应且不能作为货币在市场上流通使用,虚拟货币相关业务活动属于非法金融活动。

稳定币是虚拟货币的一种形式,目前无法有效满足客户身份识别、反洗钱等方面的要求,存在被用于洗钱、集资诈骗、违规跨境转移资金等非法活动的风险。(来源:中国人民银行网站)

 

00 后养宠人数约 2000 万,宠物经济岗位大涨 30%

11 月 29 日,智联招聘集团董事长郭盛在「第三届全国人力资源服务业发展大会」的论坛上分享了一组数据:2025 年前三季度招聘增速最高的十个岗位中,与宠物经济相关的职位排在第二,增速为 30.4%,仅次于机器学习工程师。(来自:华尔街见闻 APP)

 

我国已成为首个国内有效发明专利拥有量超过 500 万件的国家

11 月 29 日,国家知识产权局举行 11 月例行新闻发布会,介绍了加强知识产权保护和运用有关工作进展和举措。

从官方介绍获悉,目前,我国已经成为世界上首个国内有效发明专利拥有量超过 500 万件的国家,PCT 国际专利申请量连续 6 年位居全球第一。专利密集型产业增加值达到 16.87 万亿元,占 GDP 的比重提升到 13.04%;全球前 5000 个品牌中我国品牌价值达 1.81 万亿美元,位居全球第二;地理标志产品直接产值接近 9700 亿元,保持良好增长势头。

世界知识产权组织发布的《2025 年全球创新指数报告》中,我国排名提升至第 10 位,首次跻身全球前十,拥有的全球百强创新集群数量达到 24 个,连续三年位居各国之首,其中「深圳—香港—广州」集群首次登顶全球首位。(来源:IT之家)

日本公司推出 AI 自动洗澡机,约 272 万元人民币

11 月 29 日消息,以纳米气泡(Fine Bubble)技术闻名的日本 Science 株式会社曾在今年 4 月的大阪世博会上展示过一台「未来人体洗澡机」原型机,采用类似战斗机驾驶舱的流线型设计,配备后开式透明舱盖,吸引大量观众排队体验。

这款洗澡机同样应用了 Science 的纳米气泡技术。用户只需平躺于 2.3 米长的胶囊舱内,机器将自动向舱内注入热水并释放数百万微气泡进行全身清洁,并在 15 分钟内完成清洗与烘干流程。

沐浴过程中,位于用户背部的传感器可实时监测健康状态,通过 AI 算法调节水温并识别情绪状态,同时在透明舱盖投射影像,并播放音乐。

Science 发言人前仓幸子于 11 月 28 日表示,这款机器已在日本开售,零售价达 6000 万日元(现汇率约合 272 万元人民币)。前仓表示,这款洗澡机的稀缺性正是其魅力所在,公司计划仅生产 50 台左右。

Science 董事长青山康明表示:「目前国内已售出 8 台。我们正在考虑海外销售,预计到年底将售出约 15 台。」(来源:IT之家)

疯狂动物城 2 票房破 13 亿,单日票房超 5.58 亿,刷新中国影史进口电影单日票房纪录

11 月 29 日,截止至当日下午,据猫眼专业版数据,电影《疯狂动物城 2》上映 4 天,总票房破 13 亿。

同日,迪士尼动画电影《疯狂动物城 2》单日票房超 5.58 亿,刷新中国影史进口电影单日票房纪录。(来源:华尔街见闻)

每日一题-使数组和能被 P 整除🟡

2025年11月30日 00:00

给你一个正整数数组 nums,请你移除 最短 子数组(可以为 ),使得剩余元素的  能被 p 整除。 不允许 将整个数组都移除。

请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。

子数组 定义为原数组中连续的一组元素。

 

示例 1:

输入:nums = [3,1,4,2], p = 6
输出:1
解释:nums 中元素和为 10,不能被 p 整除。我们可以移除子数组 [4] ,剩余元素的和为 6 。

示例 2:

输入:nums = [6,3,5,2], p = 9
输出:2
解释:我们无法移除任何一个元素使得和被 9 整除,最优方案是移除子数组 [5,2] ,剩余元素为 [6,3],和为 9 。

示例 3:

输入:nums = [1,2,3], p = 3
输出:0
解释:和恰好为 6 ,已经能被 3 整除了。所以我们不需要移除任何元素。

示例  4:

输入:nums = [1,2,3], p = 7
输出:-1
解释:没有任何方案使得移除子数组后剩余元素的和被 7 整除。

示例 5:

输入:nums = [1000000000,1000000000,1000000000], p = 3
输出:0

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 1 <= p <= 109

🎨 用一次就爱上的图标定制体验:CustomIcons 实战

2025年11月29日 22:31

在前端项目里,图标不是“点缀”,它往往是信息结构与互动线索的关键。如何让图标既统一又可配、既美观又可国际化?这篇文章带你用 @infinilabs/custom-icons 打造一套“可配置、可主题、可国际化”的图标解决方案。

背景

  • Coco AI(开源) 用户需要可以配置自定义 Icon。
  • 多品牌与多区域:同一产品在不同客户、不同区域需要差异化的风格与语言。
  • 设计与工程协作:设计希望图标统一;工程需要灵活调整尺寸、颜色、类型、甚至自定义图片。
  • 运营与配置:希望在管理面板里直接挑选或调整图标,而不是改代码、发版本。

于是,我做了一个轻量、直观、开箱即用的组件库:@infinilabs/custom-icons

适用场景

  • 可视化配置台:在后台面板中为功能、菜单或模块选择与配置图标。
  • 多主题产品:快速切换深色/浅色主题,保证图标在不同背景下的对比度与风格。
  • 国际化应用:在不同语言环境下自动切换文案与控件标签。
  • 自定义品牌:支持上传自定义图片作为图标,满足品牌个性化需求。

主要能力

  • 图标渲染组件:ConfigurableIcon
    • 指定类型(如 lucide)、图标名、颜色与尺寸即可渲染。
    • 支持数据 URL(自定义图片)模式。
  • 图标选择器:IconPicker
    • 一站式选择与配置:类型、名称、尺寸、颜色与图片上传。
    • 可选主题与国际化支持。
    • 可通过 controls 精细开关各子控件。

快速开始

# 使用你熟悉的包管理器安装
pnpm add @infinilabs/custom-icons
# 或
npm i @infinilabs/custom-icons
# 或
yarn add @infinilabs/custom-icons

在项目中引用:

import { useState } from "react";
import { ConfigurableIcon, IconPicker } from "@infinilabs/custom-icons";

export default function Demo() {
  const [config, setConfig] = useState({
    type: "lucide",
    name: "Bot",
    size: 28,
    color: "#1e90ff",
    dataUrl: undefined,
  });

  return (
    <div style={{ padding: 24 }}>
      {/* 渲染当前配置的图标 */}
      <ConfigurableIcon
        type={config.type}
        name={config.name}
        size={config.size}
        color={config.color}
        dataUrl={config.dataUrl}
      />

      {/* 交互式选择与配置 */}
      <IconPicker
        value={config}
        onChange={setConfig}
        configurable
        theme="light"
        locale="zh-CN"
        controls={{
          type: true,
          name: true,
          size: true,
          color: true,
          image: true,
        }}
      />
    </div>
  );
}

如果你需要查看可选的 Lucide 图标名称,选择器旁已内置快捷链接:

基础效果

image.png

组件详解

ConfigurableIcon

用于在任意位置渲染一个图标。

  • 关键属性
    • type: 图标类型(如 lucide 或自定义)
    • name: 图标名称(type=lucide 时为 Lucide 名称)
    • size: 数值尺寸(px)
    • color: 颜色(十六进制或 CSS 颜色)
    • dataUrl: 当使用自定义图片时的 data: URL

示例(自定义图片):

<ConfigurableIcon
  type="custom"
  name="my-logo"
  dataUrl="data:image/png;base64,...."
  size={28}
  color="#1e90ff" // 自定义图片时通常忽略颜色
/>

IconPicker

一个将预览与配置控件整合在一起的选择器。可插在设置面板或表单中,让用户自行挑选或上传。

  • 常用属性

    • value: 当前图标配置对象
    • onChange(next): 配置变化回调
    • configurable: 是否展示配置面板
    • controls: 控件开关集合(type/name/size/color/image 等)
    • theme: light | dark
    • locale: zh-CN | en-US
    • i18n: 文案对象(可覆盖默认文案)
  • 控件开关示例

<IconPicker
  value={config}
  onChange={setConfig}
  configurable
  controls={{
    type: true,
    name: true,
    size: true,
    color: true,
    image: true, // 打开即出现上传控件
  }}
/>
  • 主题与国际化
<IconPicker
  value={config}
  onChange={setConfig}
  configurable
  theme="dark"
  locale="en-US"
/>

进阶示例:面板内批量配置

将多个图标配置成一组,供菜单或卡片模块统一管理:

function IconsPanel() {
  const [items, setItems] = useState([
    { id: 1, config: { type: "lucide", name: "Home", size: 24, color: "#444" } },
    { id: 2, config: { type: "lucide", name: "Settings", size: 24, color: "#444" } },
  ]);

  const updateItem = (id, next) =>
    setItems((prev) =>
      prev.map((it) => (it.id === id ? { ...it, config: next } : it))
    );

  return (
    <div style={{ display: "grid", gap: 16 }}>
      {items.map((it) => (
        <div key={it.id} style={{ padding: 12, border: "1px solid #eee", borderRadius: 8 }}>
          <ConfigurableIcon {...it.config} />
          <IconPicker
            value={it.config}
            onChange={(next) => updateItem(it.id, next)}
            configurable
            theme="light"
            locale="zh-CN"
            controls={{ type: true, name: true, size: true, color: true, image: false }}
          />
        </div>
      ))}
    </div>
  );
}

设计与工程协作建议

  • 设计提供命名规范:例如统一使用 Lucide 的图标名集合,避免随意命名。
  • 管理面板适配:通过 controls 开关不同角色看到的控件(运营只改颜色与大小、开发可修改类型与名称)。
  • 主题变量托管:将颜色与尺寸作为“设计令牌”,统一管理与回收。

常见问题

  • 自定义图片会应用颜色吗?
    • 通常不会;颜色更适用于矢量图标。自定义图片由图片本身决定视觉。
  • 如何选择 Lucide 图标名?
    • 打开 https://lucide.dev/icons/,在选择器里输入对应名称即可。

image.png

小结

@infinilabs/custom-icons 让“图标即配置”的能力落地:从主题与国际化,到自定义图片与统一风格,既能保证设计一致性,又给予业务足够自由度。把它接入你的管理面板或设置页,让图标成为产品的强大表达力,而不是维护负担。

如果你对更多场景(如基于角色的控件可见性、图标库扩展)有想法,欢迎继续交流与共建!

开源共建:github.com/infinilabs/…

【套路】前缀和+哈希表(Python/Java/C++/Go)

作者 endlesscheng
2023年3月9日 22:02

前置知识

模运算的世界:当加减乘除遇上取模

提示 1

例如 $\textit{nums}=[11,2,5,7,8,9]$,$p=10$,那么把 $[5,7]$ 去掉,剩余的数字相加等于 $30$,可以被 $p$ 整除。

所有元素的和 $42\bmod 10=2$,而 $(5+7)\bmod 10$ 也等于 $2$。

设所有元素的和为 $x$,去掉的元素和为 $y$。要使 $x-y$ 能被 $p$ 整除,根据前置知识中同余的定义,这等价于满足

$$
y \equiv x \pmod p
$$

提示 2

把 $y$ 用 前缀和 表示,问题转换成:在前缀和数组上找到两个数 $s[\textit{left}]$ 和 $s[\textit{right}]$,满足 $\textit{right}-\textit{left}$ 最小且

$$
s[\textit{right}]-s[\textit{left}]\equiv x \pmod p
$$

根据前置知识,将上式移项,得

$$
s[\textit{right}]-x \equiv s[\textit{left}]\pmod p
$$

上式相当于

$$
((s[\textit{right}]-x)\bmod p+p)\bmod p= s[\textit{left}]\bmod p
$$

也可以写成

$$
(s[\textit{right}]\bmod p-x\bmod p+p)\bmod p= s[\textit{left}]\bmod p
$$

提示 3

遍历 $s$ 的同时,用哈希表 $\textit{last}$ 记录 $s[i]\bmod p$ 最近一次出现的下标,如果 $\textit{last}$ 中包含 $(s[i]\bmod p-x\bmod p+p)\bmod p$,设其对应的下标为 $j$,那么 $[j,i)$ 是一个符合题目要求的子数组。

注意:本题可以移除空子数组,所以要先更新 $\textit{last}$,再更新答案。

枚举所有 $i$,计算符合要求的子数组长度的最小值,就是答案。如果没有符合要求的子数组,则返回 $-1$。

代码实现时,可以把答案初始化成 $\textit{nums}$ 的长度 $n$。如果最后答案等于 $n$,则表示没有符合要求的子数组,因为题目不允许将整个数组都移除。

答疑

:为什么不能用双指针(不定长滑动窗口)做?

:使用双指针需要满足单调性,但是 $s[i]\bmod p$ 并不是单调的,所以不能用双指针。具体请看【基础算法精讲 03】

class Solution:
    def minSubarray(self, nums: List[int], p: int) -> int:
        s = list(accumulate(nums, initial=0))
        x = s[-1] % p
        if x == 0:
            return 0  # 移除空子数组(这行可以不要)

        ans = n = len(nums)
        last = {}
        for i, v in enumerate(s):
            last[v % p] = i
            j = last.get((v - x) % p, -n)  # 如果不存在,-n 可以保证 i-j >= n
            ans = min(ans, i - j)
        return ans if ans < n else -1
class Solution {
    public int minSubarray(int[] nums, int p) {
        int n = nums.length;
        int[] s = new int[n + 1];
        for (int i = 0; i < n; i++) {
            s[i + 1] = (s[i] + nums[i]) % p;
        }
        int x = s[n];
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int ans = n;
        Map<Integer, Integer> last = new HashMap<>();
        for (int i = 0; i <= n; i++) {
            last.put(s[i], i);
            // 如果不存在,-n 可以保证 i-j >= n
            int j = last.getOrDefault((s[i] - x + p) % p, -n);
            ans = Math.min(ans, i - j);
        }
        return ans < n ? ans : -1;
    }
}
class Solution {
public:
    int minSubarray(vector<int> &nums, int p) {
        int n = nums.size(), s[n + 1];
        s[0] = 0;
        for (int i = 0; i < n; i++) {
            s[i + 1] = (s[i] + nums[i]) % p;
        }
        int x = s[n];
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int ans = n;
        unordered_map<int, int> last;
        for (int i = 0; i <= n; ++i) {
            last[s[i]] = i;
            auto it = last.find((s[i] - x + p) % p);
            if (it != last.end()) {
                ans = min(ans, i - it->second);
            }
        }
        return ans < n ? ans : -1;
    }
};
func minSubarray(nums []int, p int) int {
    n := len(nums)
    s := make([]int, n+1)
    for i, v := range nums {
        s[i+1] = (s[i] + v) % p
    }
    x := s[n]
    if x == 0 {
        return 0 // 移除空子数组(这个 if 可以不要)
    }

    ans := n
    last := map[int]int{}
    for i, v := range s {
        last[v] = i
        if j, ok := last[(v-x+p)%p]; ok {
            ans = min(ans, i-j)
        }
    }
    if ans < n {
        return ans
    }
    return -1
}

也可以不用前缀和数组,一边遍历 $\textit{nums}$ 一边计算前缀和。

class Solution:
    def minSubarray(self, nums: List[int], p: int) -> int:
        x = sum(nums) % p
        if x == 0:
            return 0  # 移除空子数组(这行可以不要)

        ans = n = len(nums)
        s = 0
        last = {s: -1}  # 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
        for i, v in enumerate(nums):
            s += v
            last[s % p] = i
            j = last.get((s - x) % p, -n)  # 如果不存在,-n 可以保证 i-j >= n
            ans = min(ans, i - j)  # 改成手写 min 会再快一些
        return ans if ans < n else -1
class Solution {
    public int minSubarray(int[] nums, int p) {
        long t = 0;
        for (int v : nums) {
            t += v;
        }
        int x = (int) (t % p);
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int n = nums.length;
        int ans = n;
        int s = 0;
        Map<Integer, Integer> last = new HashMap<>();
        last.put(s, -1); // 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
        for (int i = 0; i < n; i++) {
            s = (s + nums[i]) % p;
            last.put(s, i);
            // 如果不存在,-n 可以保证 i-j >= n
            int j = last.getOrDefault((s - x + p) % p, -n);
            ans = Math.min(ans, i - j);
        }
        return ans < n ? ans : -1;
    }
}
class Solution {
public:
    int minSubarray(vector<int> &nums, int p) {
        int x = reduce(nums.begin(), nums.end(), 0LL) % p;
        if (x == 0) {
            return 0; // 移除空子数组(这行可以不要)
        }

        int n = nums.size(), ans = n, s = 0;
        // 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
        unordered_map<int, int> last{{s, -1}};
        for (int i = 0; i < n; i++) {
            s = (s + nums[i]) % p;
            last[s] = i;
            auto it = last.find((s - x + p) % p);
            if (it != last.end()) {
                ans = min(ans, i - it->second);
            }
        }
        return ans < n ? ans : -1;
    }
};
func minSubarray(nums []int, p int) int {
    x := 0
    for _, v := range nums {
        x += v
    }
    x %= p
    if x == 0 {
        return 0 // 移除空子数组(这个 if 可以不要)
    }

    n := len(nums)
    ans, s := n, 0
    // 由于下面 i 是从 0 开始的,前缀和下标就要从 -1 开始了
    last := map[int]int{s: -1}
    for i, v := range nums {
        s += v
        last[s%p] = i
        if j, ok := last[(s-x+p)%p]; ok {
            ans = min(ans, i-j)
        }
    }
    if ans < n {
        return ans
    }
    return -1
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。
  • 空间复杂度:$\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自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

使数组和能被 P 整除

2023年3月9日 13:48

方法一:前缀和

定理一:给定正整数 $x$、$y$、$z$、$p$,如果 $y \bmod p = x$,那么 $(y - z) \bmod p = 0$ 等价于 $z \bmod p = x$。

证明:$y \bmod p = x$ 等价于 $y = k_1 \times p + x$,$(y-z) \bmod p = 0$ 等价于 $y - z = k_2 \times p$,$z \bmod p = x$ 等价于 $z = k_3 \times p + x$,其中 $k_1$、$k_2$、$k_3$ 都是整数,那么给定 $y = k_1 \times p + x$,有 $y - z = k_2 \times p \leftrightarrow z = (k_1 - k_2) \times p + x \leftrightarrow z = k_3 \times p + x$。

定理二:给定正整数 $x$,$y$,$z$,$p$,那么 $(y - z) \bmod p = x$ 等价于 $z \bmod p = (y - x) \bmod p$。

证明:$(y - z) \bmod p = x$ 等价于 $y - z = k_1 \times p + x$,其中 $k_1$ 是整数,经过变换有 $z = y - k_1 \times p - x = k_2 \times p + (y - x) \bmod p - k_1 \times p = (k_2 - k_1) \times p + (y - x) \bmod p$,等价于 $z \bmod p = (y - x) \bmod p$。

记数组和除以 $p$ 的余数为 $x$,如果 $x=0$ 成立,那么需要移除的最短子数组长度为 $0$。

记前 $i$ 个元素(不包括第 $i$ 个元素)的和为 $\textit{f}i$,我们考虑最右元素为 $\textit{nums}[i]$ 的所有子数组,假设最左元素为 $\textit{nums}[j]~(0 \le j \le i)$,那么对应的子数组和为 $\textit{f}{i+1}-\textit{f}j$,对应的长度为 $i-j+1$。由定理一可知,如果剩余子数组和能被 $p$ 整除,那么 $(\textit{f}{i+1}-\textit{f}j) \bmod p = x$。同时由定理二可知,$\textit{f}j \bmod p = (\textit{f}{i+1} - x) \bmod p$。因此当 $\textit{f}{i+1}$ 已知时,我们需要找到所有满足 $\textit{f}j \bmod p = (\textit{f}{i+1} - x) \bmod p$ 的 $\textit{f}_j$($0 \le j \le i$),从中找到最短子数组。

由于需要移除最短子数组,因此对于所有 $f_j$($0 \le j \le i$),只需要保存 $f_j \bmod p$ 对应的最大下标。

有些编程语言对负数进行取余时,余数为负数,因此计算 $f_{i+1} - x$ 除以 $p$ 的余数时,使用 $f_{i+1} - x + p$ 替代。

###Python

class Solution:
    def minSubarray(self, nums: List[int], p: int) -> int:
        x = sum(nums) % p
        if x == 0:
            return 0
        y = 0
        index = {0: -1}
        ans = len(nums)
        for i, v in enumerate(nums):
            y = (y + v) % p
            if (y - x) % p in index:
                ans = min(ans, i - index[(y - x) % p])
            index[y] = i
        return ans if ans < len(nums) else -1

###C++

class Solution {
public:
    int minSubarray(vector<int>& nums, int p) {
        int x = 0;
        for (auto num : nums) {
            x = (x + num) % p;
        }
        if (x == 0) {
            return 0;
        }
        unordered_map<int, int> index;
        int y = 0, res = nums.size();
        for (int i = 0; i < nums.size(); i++) {
            index[y] = i; // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
            y = (y + nums[i]) % p;
            if (index.count((y - x + p) % p) > 0) {
                res = min(res, i - index[(y - x + p) % p] + 1);
            }
        }
        return res == nums.size() ? -1 : res;
    }
};

###Java

class Solution {
    public int minSubarray(int[] nums, int p) {
        int x = 0;
        for (int num : nums) {
            x = (x + num) % p;
        }
        if (x == 0) {
            return 0;
        }
        Map<Integer, Integer> index = new HashMap<Integer, Integer>();
        int y = 0, res = nums.length;
        for (int i = 0; i < nums.length; i++) {
            index.put(y, i); // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
            y = (y + nums[i]) % p;
            if (index.containsKey((y - x + p) % p)) {
                res = Math.min(res, i - index.get((y - x + p) % p) + 1);
            }
        }
        return res == nums.length ? -1 : res;
    }
}

###C#

public class Solution {
    public int MinSubarray(int[] nums, int p) {
        int x = 0;
        foreach (int num in nums) {
            x = (x + num) % p;
        }
        if (x == 0) {
            return 0;
        }
        IDictionary<int, int> index = new Dictionary<int, int>();
        int y = 0, res = nums.Length;
        for (int i = 0; i < nums.Length; i++) {
            // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
            if (!index.ContainsKey(y)) {
                index.Add(y, i);
            } else {
                index[y] = i;
            }
            y = (y + nums[i]) % p;
            if (index.ContainsKey((y - x + p) % p)) {
                res = Math.Min(res, i - index[(y - x + p) % p] + 1);
            }
        }
        return res == nums.Length ? -1 : res;
    }
}

###C

#define MIN(a, b) ((a) < (b) ? (a) : (b))

typedef struct {
    int key;
    int val;
    UT_hash_handle hh;
} HashItem; 

HashItem *hashFindItem(HashItem **obj, int key) {
    HashItem *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    return pEntry;
}

bool hashAddItem(HashItem **obj, int key, int val) {
    if (hashFindItem(obj, key)) {
        return false;
    }
    HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
    pEntry->key = key;
    pEntry->val = val;
    HASH_ADD_INT(*obj, key, pEntry);
    return true;
}

bool hashSetItem(HashItem **obj, int key, int val) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        hashAddItem(obj, key, val);
    } else {
        pEntry->val = val;
    }
    return true;
}

int hashGetItem(HashItem **obj, int key, int defaultVal) {
    HashItem *pEntry = hashFindItem(obj, key);
    if (!pEntry) {
        return defaultVal;
    }
    return pEntry->val;
}

void hashFree(HashItem **obj) {
    HashItem *curr = NULL, *tmp = NULL;
    HASH_ITER(hh, *obj, curr, tmp) {
        HASH_DEL(*obj, curr);  
        free(curr);             
    }
}

int minSubarray(int* nums, int numsSize, int p) {
     int x = 0;
    for (int i = 0; i < numsSize; i++) {
        x = (x + nums[i]) % p;
    }
    if (x == 0) {
        return 0;
    }
    HashItem *index = NULL;
    int y = 0, res = numsSize;
    for (int i = 0; i < numsSize; i++) {
        hashSetItem(&index, y, i); // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
        y = (y + nums[i]) % p;
        if (hashFindItem(&index, (y - x + p) % p)) {
            int val = hashGetItem(&index, (y - x + p) % p, 0);
            res = MIN(res, i - val + 1);
        }
    }
    hashFree(&index);
    return res == numsSize ? -1 : res;
}

###JavaScript

var minSubarray = function(nums, p) {
    let x = 0;
    for (const num of nums) {
        x = (x + num) % p;
    }
    if (x === 0) {
        return 0;
    }
    const index = new Map();
    let y = 0, res = nums.length;
    for (let i = 0; i < nums.length; i++) {
        index.set(y, i); // f[i] mod p = y,因此哈希表记录 y 对应的下标为 i
        y = (y + nums[i]) % p;
        if (index.has((y - x + p) % p)) {
            res = Math.min(res, i - index.get((y - x + p) % p) + 1);
        }
    }
    return res === nums.length ? -1 : res;
};

###go

func minSubarray(nums []int, p int) int {
    sum := 0
    mp := map[int]int{0: -1}
    for _, v := range nums {
        sum += v
    }
    rem := sum%p
    if rem == 0 {
        return 0
    }
    minCount := len(nums)
    sum = 0
    for i := 0; i < len(nums); i++ {
        sum += nums[i]
        tempRem := sum%p
        k := (tempRem - rem + p) % p
        if _, ok := mp[k]; ok {
            minCount = min(minCount, i - mp[k])
        }
        mp[tempRem] = i
    }
    
    if minCount >= len(nums) {
        return -1
    }
    
    return minCount
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。遍历数组 $\textit{nums}$ 需要 $O(n)$ 的时间。

  • 空间复杂度:$O(n)$。保存哈希表需要 $O(n)$ 的空间。

做一题送一题,力扣上不少类似题

作者 liuyubobobo
2020年9月20日 09:39

首先,这道题的思路和 974 一样(当然有一些变化)。强烈建议对这个问题不熟悉的同学,看一下 974,搞懂以后,再回来看这道题:

974. 和可被 K 整除的子数组


假设 nums 的和除以 P,余数是 mod

如果 mod == 0,答案就是 0

如果 mod != 0,答案变成了找原数组中的最短连续子数组,使得其数字和除以 P,余数也是 mod


由于是求解连续子数组和的问题,很容易想到使用前缀和。

我们可以扫描一遍整个数组,计算到每个元素的前缀和。

假设当前前缀和除以 P 的余数是 curmod,为了找到一段连续子数组对 P 的余数是 mod,我们需要找到一段前缀和,对 P 的余数是 targetmod。其中 targetmod 的求法是:

如果 curmod >= mod,很简单:targetmod = curmod - mod

如果 curmod < mod,我们需要加上一个 Ptargetmod = curmod - mod + P

这样,我们可以保证,当前前缀和减去目标前缀和,剩余的数组对 P 的余数是 mod。我们只需要找最短的这样的数组就好。


最后,为了快速找到一段对 P 的余数为 targetmod 的前缀和,我们使用一个哈希表 table,来存储之前前缀和对 P 的余数和所在的索引。(key 为余数;value 为索引)。

table 在遍历过程中更新,以保证每次在 table 中查找到的,是离当前元素最近的索引,从而保证找到的是“最短”的连续子数组。


我的参考代码(C++):

class Solution {
public:
    int minSubarray(vector<int>& nums, int p) {

        long long sum = 0;
        for(int e: nums) sum += (long long)e;
        long long mod = sum % (long long)p;

        if(mod == 0ll) return 0;

        int res = nums.size();
        unordered_map<long long, int> table;
        table[0ll] = -1;

        sum = 0;
        for(int i = 0; i < nums.size(); i ++){
            sum += (long long)nums[i];
            long long curmod = sum % (long long)p;
            table[curmod] = i;

            long long targetmod = curmod >= mod ? (curmod - mod) : (curmod - mod + p);
            if(table.count(targetmod))
                res = min(res, i - table[targetmod]);
        }
        return res == nums.size() ? -1 : res;
    }
};

觉得有帮助请点赞哇!

昨天 — 2025年11月29日首页

WKWebView的重定向(objective_c)

2025年11月29日 18:12

背景

第三方支付回调时需要重定向到app的某个页面,比如支付完成后回到原生订单详情页,这个时间会有两种情况:

1、直接在web页面重定向到app的订单详情页,这个时候只需要实现 WKNavigationDelegate 中的一个核心方法webView:decidePolicyForNavigationAction:decisionHandler: 方法。

2、在支付中心跳转到第三方app然后支付完成后需要跳转回自己的app的订单详情页,这个时候可以采用Scheme方式或者是通用链接的方式解决

wkWebView重定向实现

实现这一目标,您需要让您的 WKWebView 所在的控制器遵循 WKNavigationDelegate 协议,并实现 webView:decidePolicyForNavigationAction:decisionHandler: 方法。

self.webView.navigationDelegate = self; // 设置代理

#pragma mark - WKNavigationDelegate 
- (**void**)webView:(WKWebView *)webView

decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction

decisionHandler:(**void** (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL *url = navigationAction.request.URL;
    NSString *scheme = url.scheme;
    // 1. 检查 URL Scheme 是否是我们的自定义 Scheme
    if ([scheme isEqualToString:@"coolpet"]) {
        // 1.1. 阻止 WKWebView 加载这个 URL
        decisionHandler(WKNavigationActionPolicyCancel);
        // 1.2. 实现了 handleCoolPetURL: 方法
        [self handleCoolPetURL:url];
        // 1.3. 跳转后关闭当前的 WebView 页面
        [self.navigationController popViewControllerAnimated:YES];
        return;
    }
    // 2. 对于其他 HTTP/HTTPS 链接,允许正常加载
    // 特别检查 navigationType 是否是新的主框架加载,例如用户点击了链接
//    if (navigationAction.navigationType == WKNavigationTypeLinkActivated && ![scheme hasPrefix:@"http"]) {
//        // 如果是点击了非 HTTP/HTTPS 的链接(但不是我们自定义的 Scheme),可以根据需要处理,
//        // 比如打开 App Store 或其他应用。这里我们通常允许其他系统 Scheme
//        // 允许继续,但更安全的做法是只允许 http(s)
//        // decisionHandler(WKNavigationActionPolicyAllow);
//    }
    // 3. 默认允许其他所有导航行为(如页内跳转、HTTP/HTTPS 加载等)
    decisionHandler(WKNavigationActionPolicyAllow);
}

// 通过URL跳转对应页面
- (void)handleCoolPetURL:(NSURL *)url {
    NSString *host = url.host;
    NSString *path = url.path;      // 路径: /order/detail
    NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
    NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
    for (NSURLQueryItem *item in components.queryItems) {
        queryParams[item.name] = item.value;
    }
    // 根据路径判断是否是订单详情页
    if ([host isEqualToString:kAPPUniversalTypeOrderDetailsHost] && [path isEqualToString:kAPPUniversalTypeOrderDetailsPath]) {
        // 获取我们需要的订单号
        NSString *tradeNo = [queryParams[@"tradeNo"] stringValue];
        // 执行跳转

        if (tradeNo.length > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                /// 做跳转
            });
        }
    }
}

Scheme方式

第三方支付平台完成支付后,是通过你App的 URL Scheme 来唤醒你的App并携带支付结果的。

  1. 配置 App URL Scheme
  • 操作: 在 Xcode 项目的 Info.plist 或项目设置的 Info 选项卡下的 URL Types 中添加你的 App 的 Scheme。

    • 例如,你可以设置一个 Scheme 叫 myscheme
  1. 处理 App Delegate 中的回调

App 被第三方支付应用唤醒后,系统会调用 AppDelegate 中的特定方法。你需要在这里接收并处理回调 URL。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {
    // 1. 检查是否是你的支付回调 Scheme
    if ([url.scheme isEqualToString:@"myappscheme"]) {
        [self handleCoolPetURL:url];
    }

    // 如果是其他URL(如通用链接),也在这里处理
    // ...
    return NO;
}

通用链接方式

当用户点击一个配置了通用链接的 HTTPS 链接时:

  1. 如果 App 已经安装,系统会直接调用 AppDelegate 中的这个方法。
  2. 如果 App 未安装,该链接会直接在 Safari 中打开。

这个机制的主要优点是安全(基于 HTTPS)和用户体验更好(避免了 URL Scheme 引起的跳转确认和安全问题)。

🔗 通用链接(Universal Links)实现指南

步骤 1: 服务器端配置(Association File)

这是通用链接能够工作的基础。您需要在您的 Web 服务器上创建一个特殊的 JSON 文件,告诉 iOS 系统哪些路径应该由您的 App 处理。

1. 创建 apple-app-site-association 文件
  • 文件名: 必须是 apple-app-site-association(注意,没有 .json 扩展名)。

  • 内容格式(JSON):

    {
        "applinks": {
            "apps": [],
            "details": [
                {
                    "appID": "TeamID.BundleID",
                    "paths": [
                        "/orders/*",    // 匹配所有 /orders/ 下的路径
                        "/products/*",  // 匹配所有 /products/ 下的路径
                        "NOT /account/login/*" // 排除某些路径
                    ]
                }
            ]
        }
    }
    
    • TeamID 您的 Apple Developer Team ID。
    • BundleID 您的 App 的 Bundle Identifier。
    • paths 定义您希望 App 能够处理的 URL 路径。
2. 部署文件
  • 部署位置: 将此文件上传到您的域名根目录或 .well-known/ 目录下。

    • 例如:https://yourdomain.com/apple-app-site-association
    • 或者:https://yourdomain.com/.well-known/apple-app-site-association
  • 内容类型: 确保服务器以正确的 MIME 类型提供此文件:application/jsontext/plain

  • HTTPS: 您的整个网站必须使用 HTTPS

步骤 2: App 端配置(Xcode & Objective-C)

1. 开启 Associated Domains Capability

在 Xcode 中为您的 App 开启 Associated Domains 功能。

  • 路径: Xcode -> 项目设置 -> 目标 (Target) -> Signing & Capabilities 选项卡

  • 操作: 点击 + Capability,添加 Associated Domains

  • 添加域名: 在列表中添加您的域名,格式为:

    applinks:yourdomain.com
    

    注意: 不带 https://http://

2. 在 AppDelegate 中接收回调

当用户点击一个通用链接并唤醒 App 时,系统会调用 AppDelegate 中的 continueUserActivity 方法。您需要在此方法中解析 URL 并进行页面跳转。

// AppDelegate.m

#import "OrderViewController.h" // 假设您的订单处理页面

// ...

- (BOOL)application:(UIApplication *)application 
continueUserActivity:(NSUserActivity *)userActivity 
  restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
    
    // 1. 检查活动类型是否为 Universal Link
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        
        // 2. 获取用户点击的 HTTPS URL
        NSURL *webpageURL = userActivity.webpageURL;
        
        if (webpageURL) {
            NSLog(@"Received Universal Link: %@", webpageURL.absoluteString);
            
            // 3. 将 URL 转发给路由处理方法
            [self handleUniversalLinkURL:webpageURL];
            
            return YES;
        }
    }
    
    return NO;
}

// 通用链接路由处理方法
- (void)handleUniversalLinkURL:(NSURL *)url {
    
    // 示例:解析路径并跳转到订单详情
    if ([url.path hasPrefix:@"/orders/detail"]) {
        
        // 解析查询参数,例如 order_id=12345
        NSString *orderID = [self extractParameter:@"order_id" fromURL:url];
        
        if (orderID.length > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                // 执行跳转逻辑
                UINavigationController *nav = (UINavigationController *)self.window.rootViewController;
                OrderViewController *orderVC = [[OrderViewController alloc] init];
                orderVC.orderID = orderID;
                [nav pushViewController:orderVC animated:YES];
            });
        }
    }
}

// 辅助方法 (需要您自行实现,或使用前文提到的 dictionaryWithQueryString: 方法)
- (NSString *)extractParameter:(NSString *)paramName fromURL:(NSURL *)url {
    // ... 解析 url.query 字符串,提取指定参数 ...
    return nil; 
}

华为Mate X7 云锦天章:高级感,从何而来?

作者 谢东成
2025年11月29日 18:05

《红楼梦》中最盛大的一幕,莫过于元春省亲——「锦幢云幕,珠履罗衢」。

「翻阅《红楼梦》,满目皆云锦。」曹雪芹并不靠堆砌珠宝来描绘华贵,而是以织物来传达秩序之美:锦帐、妆花、帷幕层层递进,礼与雅由经纬而生。也正因如此,谈云锦,似乎天然就带着一份制度与审美同构的分寸感。

如今,华为Mate X7 的「光织云锦」背板,正是将这门古老工艺从礼制器物转译为当代材料语言的大胆尝试——让云锦在今天的工业体系中焕发新辉,把文化的经纬,织入日用之物的触感与光影。

由经纬入骨,由材质成形

早于公元 417 年,东晋于建康设立「锦署」,自此南京云锦与权力与礼制相系相生。及至元、明、清三代,云锦均为皇室御用品,龙袍、冕服皆出其间,成为礼制文明最华美的注脚。

云锦之珍稀,不独在纹样之繁复,亦在工具与手艺之精微。

「大花楼」织机通高四米:楼上拽花提经,楼下抛梭织纬,二人合拍,一经一纬,一日仅得五六厘米。

▲ 位于南京云锦博物馆的大花楼木织机,图片来自:金羊网

云锦之所以叫云锦,正因为它「灿若云霞」,其「妆花」是云锦中艺术成就最高的品类,其要诀在于「挖花盘织」「逐花异色」:在同一幅布面上,以丝线、金线、银线、孔雀羽等异材并用、分区换色,分别织出各异纹样。

换个角度看,光影会变,层次会变,仿佛织物在呼吸,生机自现。

在嘉庆年间,南京云锦织造达到顶峰,全城织机三万多台,二十多万人以此为业,秦淮河一带机户云集,机杼声彻夜不绝。只可惜,清末以降行当凋敝,织造手艺一度失传。至 1949 年南京仅存可生产织机四台。1980 年,南京云锦研究所承接北京定陵博物馆龙袍料复织之任,由复原入手,重新体悟与还原这门技艺的工序与精义。

终于在 2009 年,云锦织造技艺入列联合国人类非物质文化遗产代表作名录,云锦技艺谱系由此再度庄严确立,并得以传承。

▲ 云锦复原款,图片来自:南京云锦博物馆

若要将「云锦」这套关于经纬、叠层、秩序的复杂逻辑,与现代智能手机相结合,就必须从材料逻辑而非图案贴附入手。

正如华为Mate X7 所带来的「光织云锦」背板,就是把云锦的织造逻辑,用现代材料科学重新实现一遍。

传统云锦以无数根经纬织就纹理,经线为纵向骨架,纬线为横向填充,经纬相交、密度有别、材质各异,便生出不同的纹理与光泽。

基于这套方法论,华为Mate X7 则在背板中以纳米级纤维重现这套「经/纬」的排布逻辑。通过约 900 根经线 × 1700 根纬线的精密交织,呈现祥云纹理,让纤维具备明确的方向性与密度差,在不同视线角度下,会呈现出层次各异的光影变化。

与此同时,华为Mate X7 亦从云锦「孔雀羽」以结构色呈现鲜艳反光汲取灵感,将纳米级多层光泽膜切丝入捻,融入纤维体系。

随光而动,呈现细腻而鲜明的色彩流变——一处是丝绸的柔哑,一处是金属的冷光,间或隐现孔雀羽的幽蓝,一面之上自成多层光影。

远看是层层叠叠的金属流光,近看是细密纤维,如「金丝」勾勒星河,云纹交织万象,营造出「手可摘星辰」的诗意与浪漫质感,观感与触感彼此叠映,既克制雅致,又丰盈饱满。

▲ 华为Mate X7 云锦白、云锦蓝配色,图片来自:燕山派

华为Mate X7 把这种经纬逻辑固化到工业产线,让云锦质感得以复现,带来云锦白、云锦蓝两款极具东方美学意境的配色,并将轻量化、高强度与卓越抗冲击性巧妙融合,在确保观感与手感的前提下,也不牺牲日常所需的耐用与防护。

承托这块云锦背板的,是全新超可靠折叠玄武架构机身,外有第二代玄武钢化昆仑玻璃外屏,内有超韧三重复合叠层内屏,中间由玄武水滴铰链和超强机翼铝中框共同支撑。

这套架构的存在,让华为Mate X7 用起来更踏实,反复开合不心疼,且华为Mate X7 达到 IP58 和 IP59 防尘抗水的顶级防护水准,80℃ 热水喷淋也不犯怵。

如此一来,手机不再是把非遗技艺做成表层贴花,而是让云锦织造逻辑内嵌于材料与结构,成为既有层次与意蕴、也经得起日常使用的掌心器物。

一瞥惊鸿的「时空之门」

门,是人类为世界划出的第一道分界:门内是秩序与日常,门外是未知与远方。

折叠屏手机的每一次开合,也像是在一扇门之间来回穿梭:在灵动外屏和开阔内屏之间,在速览与沉浸之间,也在掏出记录与铺开创作之间。

信息与视野随之跃迁,叙事与效率也在开合之间里完成切换。

华为Mate X7 将这层意象做成了看得见的标志性符号,收归于名为「时空之门」的一体化镜头模组之中。

这道时空之门延续了 Mate X 折叠屏系列从寰宇舷窗、寰宇星门、寰宇星轨演变而来的寰宇系列设计语言。

立体多维的「时空之门」模组,被安排在机身中轴偏上的位置,四周通过不同材质和颜色的过渡做出一种类似「门框」的效果。

边缘有切面、有高光、有层级感,给华为Mate X7 定下一个过目难忘的视觉重心。

在视觉上,它和云锦/素皮背板形成对话。一个提供温润的、流动的、有呼吸感的背景;一个提供理性的、几何的、稳定的视觉锚点。

两者结合,让整台机器在折叠和展开时,都有清晰的设计逻辑可循。

当然,镜头模组本身,就是通往光影世界的入口,也要为了内在影像表达的功能性而服务。

华为Mate X7 搭载了「第二代红枫原色摄像头」,其「光谱感知」能力得到跨越升级,色彩分辨力提升 25%、进光量提升 96%、色彩还原能力提升 43%,在复杂的混光场景也绝不偏色。

更重要的是,主摄采用了首创四切片镜片设计,配合 0.4mm 业界超薄玻璃镜片,既提升了进光量,又缩小了模组体积,本是相互冲突的因素,在华为Mate X7 身上,得到了妥善平衡的答案。

典藏版搭载折叠屏业界最大底——1/1.28 英寸 RYYB 超光变传感器,拥有 5000 万高像素,配备 F1.49 – F4.0 十档可变光圈,超强感光,无论明暗准出片。

配备的业界最大光圈潜望式长焦镜头,则通过分群对焦镜片组与长焦微距直立潜望系统,融合多维一体化对焦防抖马达,在极限体积里实现了光学规格的跃升。

这意味着,华为Mate X7 把堪比直板旗舰的超高规格影像,成功地塞进了极致轻薄的折叠机身之中,不再需要为了轻薄和折叠结构,而作出任何妥协。

真正高端的折叠屏手机,如何表达「高级感」设计?

历经六年迭代,折叠屏手机早已迈过了从尝鲜到常用的临界点。而在这个细分品类中,华为长期稳居份额第一,已成为公认的「折叠标杆」。

在这样的基础上,华为Mate X7 再度以「集大成」的姿态,给出更完整、更立体的答案:把看得见的设计、摸得着的质感、用得住的可靠、拍得好的影像、离不开的智能,串成一整段连贯的使用体验。

它以超可靠玄武折叠架构、耐用铰链与高等级防护把「轻薄」与「耐用」同时落地;再以第二代红枫影像与更强的主摄/长焦协同,让复杂光线与远近场景都能稳定出片;而在大屏场景与 AI 体验上,鸿蒙 6 大屏 AI、分屏协作与跨设备互联,让折叠形态从「好看」走向「高效」。

在解决轻薄、耐用、性能、影像等一个个棘手问题之后,高端旗舰手机又开始回到那个基础的命题:

到底应该用什么,来定义产品的「高级感」?

这并非突发的转向,而是长期积累后的必然。

随着中国厂商在供应链、工艺与设计上的成熟与自信,他们既有能力、也有必要回答更深的问题:中国高端手机到底应该长什么样?它的质感由何承载?用什么材料与表达去呈现?

作为折叠屏手机的佼佼者,华为Mate X7 的回答,是将云锦这种承载了 1600 年历史的非遗工艺,用现代工业的方式,在一块手机背板上重新演绎。

这并非简单粗暴地把纹样贴上去,而是选择先读懂云锦的核心:

经纬如何组织秩序,多材质叠层如何生成光影,逐花异色如何堆叠细节;随后再用现代材料科学,把这套逻辑重建为纳米纤维的经纬与工艺叠层,让纹理从材质里「长」出来。

这样的尝试,在中国消费电子史上并不鲜见,失败的例子也很多:仿陶瓷、仿织造、仿书画,最后只剩「形似神离」。

但华为Mate X7 的回答,不是模仿表面,而是将文化内核写进材料与结构本身。

放眼长远,华为Mate X7 站在中国高端手机与折叠形态演进的时间轴上,是一个值得标注的坐标:它昭示着中国高端智能手机正在以非遗的材料语言,给出关于「高级感」的答案。

夜里关灯前,你合上手机,随手搁在床头。微光掠过背板,那层纳米纤维经纬轻轻起伏;你未必了解「大花楼」与纳米工艺,但在每天打开它的那一刻,触摸到与玻璃、塑料不一样的指尖质感。

这份细微差异,无法明确写进参数表,却会慢慢渗入日常。

几百年前,云锦是权力与礼制的象征;几百年后,它亦化作华为Mate X7 的机身外壳,合于掌心阅信,展为大屏协作,立在桌角静赏光影——从宫廷器物到日用之物,文化的经与纬,仍在掌心流转。

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

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


mac电脑安装nvm

2025年11月29日 18:03

方案一、常规安装

  1. 下载安装脚本:在终端中执行以下命令来下载并运行 NVM 的安装脚本3:

    bash

    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.5/install.sh | bash
    

  2. 配置环境变量:安装完成后,需要配置环境变量。如果你的终端使用的是 bash,打开或创建~/.bash_profile文件,添加以下内容3:

    bash

    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # 加载nvm
    [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # 加载bash自动补全(可选)
    

    如果使用的是 zsh,则打开或创建~/.zshrc文件,添加相同内容。然后执行source ~/.bash_profilesource ~/.zshrc使配置生效。

方案二、解决网络问题的安装

如果因为网络原因无法直接访问官方源,可以尝试以下方法:

  1. 通过国内镜像下载安装脚本:可以从 gitee 等国内代码托管平台的镜像下载安装脚本,例如:

    bash

    curl -o- https://gitee.com/cunkai/nvm-cn/raw/master/install.sh | bash
    

  2. 配置 NVM 使用国内镜像:安装完成后,编辑~/.zshrc(或~/.bashrc),添加以下内容来配置 NVM 使用国内的 Node.js 镜像源:

    bash

    export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
    export NVM_IOJS_ORG_MIRROR=https://npmmirror.com/mirrors/iojs
    

    保存后执行source ~/.zshrcsource ~/.bashrc使配置生效。

安装完成后,可以通过nvm -v命令查看 NVM 的版本,以确认是否安装成功。

nvm常用命令

;安装node18.16.0
nvm install 18.16.0

;查看nvm安装的node版本
nvm list

;通过nvm list查看电脑已有的版本号,设置默认的版本
nvm alias default v22.16.0

iOS 语音房(拍卖房)开发实践

作者 KangJX
2025年11月29日 17:49

本文基于一个真实的iOS语音房项目案例,详细讲解如何使用状态模式来管理复杂的业务流程,以及如何与权限中心协同工作,因为在拍卖房间中不只有不同的房间阶段变化(状态)还有不同角色拥有不同的权限(权限中心)。

业务场景

拍拍房是一个实时拍卖房间系统,类似于语音房间+拍卖的结合体。用户可以在房间内:

  • 作为房主主持拍卖
  • 作为拍卖人上传物品并介绍
  • 作为竞拍者出价竞拍
  • 作为观众观看拍卖过程

核心业务流程

一个完整的拍卖流程需要经历4个明确的阶段:

准备阶段 → 上拍 → 拍卖中 → 定拍 → (循环)准备阶段

每个阶段都有:

  • 不同的允许操作(如只能在准备阶段上传物品)
  • 不同的状态转换规则(如只能从拍卖中进入定拍)
  • 不同的业务逻辑(如只有拍卖中才能出价)

技术挑战

  1. 状态多:4个主要状态,每个状态行为差异大
  2. 转换复杂:状态之间的转换有严格的规则
  3. 权限交织:每个操作还需要考虑用户角色权限
  4. 易扩展性:未来可能增加新的拍卖模式

为什么选择状态模式

❌ 不使用状态模式的问题

如果使用传统的 if-elseswitch-case 来处理:

// 反例:所有逻辑堆砌在一起
func placeBid(amount: Decimal) {
    if currentState == .preparing {
        print("拍卖还未开始")
        return
    } else if currentState == .listing {
        print("拍卖还未正式开始")
        return
    } else if currentState == .auctioning {
        // 执行出价逻辑
        if user.role == .viewer {
            print("观众不能出价")
            return
        }
        if user.id == auctioneer.id {
            print("拍卖人不能给自己出价")
            return
        }
        if amount < currentPrice + incrementStep {
            print("出价金额不足")
            return
        }
        // 终于可以出价了...
    } else if currentState == .closed {
        print("拍卖已结束")
        return
    }
}

问题显而易见

  1. 🔴 代码臃肿:所有状态的逻辑混在一起
  2. 🔴 难以维护:修改一个状态可能影响其他状态
  3. 🔴 不易扩展:增加新状态需要修改多处代码
  4. 🔴 权限混乱:业务逻辑和权限判断纠缠在一起
  5. 🔴 测试困难:无法单独测试某个状态的逻辑

✅ 使用状态模式的优势

// 状态模式:每个状态独立处理
class AuctioningState: RoomStateProtocol {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 只关注拍卖中状态的出价逻辑
        let bid = Bid(...)
        room.addBid(bid)
        return true
    }
}

class PreparingState: RoomStateProtocol {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 准备阶段直接拒绝
        print("拍卖还未开始")
        return false
    }
}

优势明显

  1. 职责单一:每个状态类只关注自己的逻辑
  2. 易于维护:修改某个状态不影响其他状态
  3. 开闭原则:新增状态只需添加新类,不修改现有代码
  4. 清晰直观:状态转换一目了然
  5. 便于测试:可以单独测试每个状态

状态模式设计

整体架构

┌─────────────────────────────────────────┐
│           Room(房间上下文)             │
│  - currentState: RoomStateProtocol      │
│  - changeState(to: RoomState)           │
└──────────────┬──────────────────────────┘
               │ 持有
               ↓
┌─────────────────────────────────────────┐
│      RoomStateProtocol(状态协议)       │
│  + startAuction(room: Room) -> Bool     │
│  + placeBid(room: Room, ...) -> Bool    │
│  + endAuction(room: Room) -> Bool       │
│  + uploadItem(room: Room, ...) -> Bool  │
└─────────────┬───────────────────────────┘
              │ 实现
    ┌─────────┼─────────┬─────────┐
    ↓         ↓         ↓         ↓
┌──────┐ ┌────────┐ ┌────────┐ ┌────────┐
│准备  │ │上拍    │ │拍卖中  │ │定拍    │
│State │ │State   │ │State   │ │State   │
└──────┘ └────────┘ └────────┘ └────────┘

核心组件

1. 状态枚举

enum RoomState: String {
    case preparing      // 准备阶段
    case listing        // 上拍
    case auctioning     // 拍卖中
    case closed         // 定拍
}

2. 状态协议

protocol RoomStateProtocol {
    var stateName: RoomState { get }
    
    // 状态转换
    func startAuction(room: Room) -> Bool
    func endAuction(room: Room) -> Bool
    
    // 业务操作
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool
    
    // 状态描述
    func getStateDescription() -> String
}

状态转换图

┌─────────────┐
│  准备阶段    │ 拍卖人上传物品、设置规则
│  Preparing  │ 房主可以开始拍卖
└──────┬──────┘
       │ startAuction()
       ↓
┌─────────────┐
│    上拍     │ 展示物品信息
│   Listing   │ 倒计时准备(3秒)
└──────┬──────┘
       │ 自动转换 / 房主提前开始
       ↓
┌─────────────┐
│   拍卖中    │ 用户可以出价
│ Auctioning  │ 倒计时重置机制
└──────┬──────┘
       │ endAuction() / 倒计时归零
       ↓
┌─────────────┐
│    定拍     │ 展示成交结果
│   Closed    │ 可以开启下一轮
└──────┬──────┘
       │ startAuction() (开启下一轮)
       ↓
┌─────────────┐
│  准备阶段    │ 回到初始状态
│  Preparing  │
└─────────────┘

具体实现

1. 准备阶段(Preparing)

class PreparingState: RoomStateProtocol {
    var stateName: RoomState { return .preparing }
    
    // ✅ 允许:开始拍卖
    func startAuction(room: Room) -> Bool {
        guard room.currentItem != nil else {
            print("⚠️ 没有拍卖物品,无法开始")
            return false
        }
        
        // 状态转换:准备 → 上拍
        room.changeState(to: .listing)
        
        // 3秒后自动进入拍卖中
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            room.changeState(to: .auctioning)
        }
        
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖还未开始,无法出价")
        return false
    }
    
    // ✅ 允许:上传物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        room.setAuctionItem(item, rules: rules)
        return true
    }
    
    // ❌ 不允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖还未开始")
        return false
    }
    
    func getStateDescription() -> String {
        return "准备阶段:拍卖人可以上传物品并设置规则"
    }
}

关键点

  • ✅ 只允许上传物品和开始拍卖
  • ✅ 自动触发状态转换(准备 → 上拍 → 拍卖中)
  • ✅ 逻辑清晰,职责单一

2. 上拍阶段(Listing)

class ListingState: RoomStateProtocol {
    var stateName: RoomState { return .listing }
    
    // ✅ 允许:房主提前开始
    func startAuction(room: Room) -> Bool {
        room.changeState(to: .auctioning)
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖还未正式开始,无法出价")
        return false
    }
    
    // ❌ 不允许:修改物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 上拍阶段无法修改物品")
        return false
    }
    
    // ❌ 不允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖还未正式开始")
        return false
    }
    
    func getStateDescription() -> String {
        return "上拍中:展示拍卖物品,倒计时后自动开始"
    }
}

关键点

  • 🎯 过渡状态:用于展示物品信息
  • ✅ 房主可以提前开始
  • ❌ 大部分操作被禁止,保证流程的严谨性

3. 拍卖中(Auctioning)⭐ 核心状态

class AuctioningState: RoomStateProtocol {
    var stateName: RoomState { return .auctioning }
    
    // ❌ 不允许:重复开始
    func startAuction(room: Room) -> Bool {
        print("⚠️ 拍卖已经在进行中")
        return false
    }
    
    // ✅ 允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        room.changeState(to: .closed)
        
        if let winner = room.currentBid {
            room.addSystemMessage("🎉 成交!恭喜 (winner.bidderName) 以 ¥(winner.price) 拍得")
        } else {
            room.addSystemMessage("流拍:没有人出价")
        }
        
        return true
    }
    
    // ✅ 允许:出价(核心逻辑)
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 创建出价记录
        let bid = Bid(
            id: UUID().uuidString,
            price: amount,
            bidderId: user.id,
            bidderName: user.nickname,
            timestamp: Date()
        )
        
        // 记录出价
        room.addBid(bid)
        
        print("💰 (user.nickname) 出价 ¥(amount)")
        
        // 这里可以重置倒计时(简化版省略)
        // resetCountdown()
        
        return true
    }
    
    // ❌ 不允许:修改物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 拍卖进行中,无法修改物品")
        return false
    }
    
    func getStateDescription() -> String {
        return "拍卖中:竞拍者可以出价,倒计时结束后定拍"
    }
}

关键点

  • 💰 核心业务逻辑:处理出价
  • 📊 实时更新:记录每次出价
  • ⏱️ 倒计时机制:有出价时重置(可扩展)
  • 🔄 状态转换:可以结束进入定拍

4. 定拍阶段(Closed)

class ClosedState: RoomStateProtocol {
    var stateName: RoomState { return .closed }
    
    // ✅ 允许:开启下一轮
    func startAuction(room: Room) -> Bool {
        // 重置房间状态
        room.changeState(to: .preparing)
        room.currentItem = nil
        room.currentBid = nil
        room.addSystemMessage("🔄 准备下一轮拍卖")
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖已经结束,无法出价")
        return false
    }
    
    // ❌ 不允许:重复结束
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖已经结束")
        return false
    }
    
    // ❌ 不允许:上传物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 拍卖已结束,请开启下一轮")
        return false
    }
    
    func getStateDescription() -> String {
        return "已定拍:拍卖结束,可以开启下一轮"
    }
}

关键点

  • 🎉 展示成交结果
  • 🔄 支持循环拍卖:可以开启下一轮
  • 🔒 所有拍卖操作被锁定

与权限中心协作

设计哲学:分离关注点

┌─────────────────────────────────────┐
│         用户发起操作                 │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│      RoomManager(协调层)           │
└──────────────┬──────────────────────┘
               ↓
        ┌──────┴──────┐
        ↓             ↓
┌──────────────┐ ┌──────────────┐
│ 权限中心      │ │ 状态对象      │
│"能不能做"    │ │"怎么做"      │
└──────────────┘ └──────────────┘

协作流程

class RoomManager {
    func placeBid(user: User, room: Room, amount: Decimal) -> Result<Void, RoomError> {
        // 第一步:权限中心检查"能不能做"
        let result = permissionCenter.checkPermission(
            action: .placeBid,
            user: user,
            room: room,
            metadata: ["amount": amount]
        )
        
        guard result.isAllowed else {
            return .failure(.permissionDenied(result.deniedReason ?? "权限不足"))
        }
        
        // 第二步:状态对象执行"怎么做"
        let success = room.stateObject.placeBid(room: room, user: user, amount: amount)
        
        if success {
            return .success(())
        } else {
            return .failure(.operationFailed("出价失败"))
        }
    }
}

权限规则示例

// 权限中心:检查"能不能做"
PermissionRule(
    action: .placeBid,
    priority: 100,
    description: "只能在拍卖中状态出价"
) { context in
    guard context.room.state == .auctioning else {
        return .denied(reason: "❌ 当前不在拍卖阶段,无法出价")
    }
    return .allowed
}

PermissionRule(
    action: .placeBid,
    priority: 90,
    description: "拍卖人不能给自己出价"
) { context in
    if context.user.role == .auctioneer,
       context.user.id == context.room.currentItem?.auctioneerId {
        return .denied(reason: "❌ 您是拍卖人,不能对自己的物品出价")
    }
    return .allowed
}

为什么要分离?

如果不分离

// ❌ 反例:状态和权限混在一起
class AuctioningState {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 权限判断
        if user.role == .viewer {
            return false
        }
        if user.role == .auctioneer && user.id == auctioneer.id {
            return false
        }
        if amount < currentPrice + increment {
            return false
        }
        
        // 业务逻辑
        room.addBid(...)
        return true
    }
}

分离后

// ✅ 状态对象:只关心业务逻辑
class AuctioningState {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        room.addBid(...)  // 纯粹的业务逻辑
        return true
    }
}

// ✅ 权限中心:只关心权限验证
PermissionCenter.check(.placeBid, user, room)

优势

  1. 单一职责:状态对象不关心权限
  2. 易于扩展:新增权限规则不影响状态
  3. 易于测试:可以独立测试权限和状态
  4. 灵活配置:权限规则可以动态调整

实际应用场景

场景1:完整拍卖流程

// 1. 创建房间(自动进入准备阶段)
let room = Room(name: "今晚靓号专场", owner: host)
print("房间状态:(room.state.displayName)") // 准备中

// 2. 拍卖人上传物品
let item = AuctionItem(name: "手机号 13888888888", ...)
room.stateObject.uploadItem(room: room, item: item, rules: rules)
// ✅ 准备阶段允许上传物品

// 3. 房主开始拍卖
room.stateObject.startAuction(room: room)
// 状态转换:准备 → 上拍
print("房间状态:(room.state.displayName)") // 上拍中

// 4. 3秒后自动进入拍卖中
// 状态转换:上拍 → 拍卖中
print("房间状态:(room.state.displayName)") // 拍卖中

// 5. 竞拍者出价
room.stateObject.placeBid(room: room, user: bidder1, amount: 120)
// ✅ 拍卖中状态允许出价
print("当前价格:¥(room.currentPrice)") // ¥120

room.stateObject.placeBid(room: room, user: bidder2, amount: 150)
print("当前价格:¥(room.currentPrice)") // ¥150

// 6. 房主结束拍卖
room.stateObject.endAuction(room: room)
// 状态转换:拍卖中 → 定拍
print("房间状态:(room.state.displayName)") // 已定拍
print("成交:(room.currentLeader) - ¥(room.currentPrice)")

// 7. 开启下一轮
room.stateObject.startAuction(room: room)
// 状态转换:定拍 → 准备
print("房间状态:(room.state.displayName)") // 准备中

场景2:错误的操作被拒绝

// 尝试在准备阶段出价
let room = Room(...)
room.stateObject.placeBid(room: room, user: bidder, amount: 200)
// ❌ 输出:"拍卖还未开始,无法出价"
// 返回:false

// 尝试在拍卖中上传物品
room.stateObject.startAuction(room: room)  // 进入拍卖中
room.stateObject.uploadItem(room: room, item: item, rules: rules)
// ❌ 输出:"拍卖进行中,无法修改物品"
// 返回:false

// 尝试在定拍后出价
room.stateObject.endAuction(room: room)  // 进入定拍
room.stateObject.placeBid(room: room, user: bidder, amount: 300)
// ❌ 输出:"拍卖已经结束,无法出价"
// 返回:false

场景3:状态转换的严格性

let room = Room(...)

// ✅ 正确的转换
room.state  // .preparing
room.stateObject.startAuction(room: room)
room.state  // .listing → .auctioning

// ❌ 不允许跳过状态
room.state  // .preparing
room.stateObject.endAuction(room: room)
// 输出:"拍卖还未开始"
// 状态不变,仍然是 .preparing

优势与挑战

✅ 优势

1. 代码组织清晰

对比

传统方式(500行的switch):

func handleOperation() {
    switch currentState {
    case .preparing:
        // 100行代码
    case .listing:
        // 100行代码
    case .auctioning:
        // 200行代码
    case .closed:
        // 100行代码
    }
}

状态模式(每个文件<100行):

PreparingState.swift    // 80行
ListingState.swift      // 60行
AuctioningState.swift   // 100行
ClosedState.swift       // 60行

2. 易于维护

修改"拍卖中"的逻辑:

  • ❌ 传统方式:在500行代码中找到对应的case,小心翼翼地修改
  • ✅ 状态模式:直接打开AuctioningState.swift,放心修改

3. 符合开闭原则

新增"暂停"状态:

  • ❌ 传统方式:修改所有的switch语句,增加新的case
  • ✅ 状态模式:创建PausedState.swift,不修改现有代码

4. 便于测试

// 可以单独测试某个状态
func testAuctioningState() {
    let state = AuctioningState()
    let room = MockRoom()
    let result = state.placeBid(room: room, user: mockUser, amount: 100)
    XCTAssertTrue(result)
}

5. 团队协作友好

多人开发时:

  • 小明负责 PreparingState
  • 小红负责 AuctioningState
  • 小刚负责 ClosedState

互不干扰,Git冲突少。

⚠️ 挑战

1. 类的数量增加

  • 4个状态 = 4个类文件
  • 如果有10个状态,就需要10个文件

应对:合理的文件组织和命名规范

2. 状态转换的复杂性

需要仔细设计状态转换图,避免:

  • 死锁状态
  • 循环转换
  • 无法到达的状态

应对

  • 绘制状态图
  • 编写状态转换测试
  • 文档化转换规则

3. 状态间的数据共享

状态对象是无状态的,数据存储在Room对象中:

class Room {
    var stateObject: RoomStateProtocol  // 当前状态对象
    var currentItem: AuctionItem?       // 状态间共享的数据
    var currentBid: Bid?                // 状态间共享的数据
}

应对

  • 明确哪些数据属于上下文(Room)
  • 哪些数据属于状态对象

4. 调试可能更困难

调用链变长:

ViewController → RoomManager → PermissionCenter → StateObject

应对

  • 添加详细的日志
  • 使用断点调试
  • 编写单元测试

最佳实践

1. 状态对象应该是无状态的

// ❌ 错误:状态对象持有数据
class AuctioningState {
    var currentPrice: Decimal = 0  // 不应该在这里
    var bidHistory: [Bid] = []     // 不应该在这里
}

// ✅ 正确:数据存储在上下文中
class Room {
    var currentPrice: Decimal
    var bidHistory: [Bid]
    var stateObject: RoomStateProtocol
}

2. 使用工厂方法创建状态

class Room {
    func changeState(to newState: RoomState) {
        self.state = newState
        
        // 工厂方法
        switch newState {
        case .preparing:
            self.stateObject = PreparingState()
        case .listing:
            self.stateObject = ListingState()
        case .auctioning:
            self.stateObject = AuctioningState()
        case .closed:
            self.stateObject = ClosedState()
        }
        
        addSystemMessage("房间状态变更为:(newState.displayName)")
    }
}

3. 记录状态转换日志

func changeState(to newState: RoomState) {
    let oldState = self.state
    self.state = newState
    
    // 记录状态转换
    print("🔄 状态转换:(oldState.displayName) → (newState.displayName)")
    
    // 可以添加到数据库或分析系统
    Analytics.trackStateChange(from: oldState, to: newState)
}

4. 验证状态转换的合法性

func changeState(to newState: RoomState) {
    // 验证转换是否合法
    guard isValidTransition(from: self.state, to: newState) else {
        print("⚠️ 非法的状态转换:(self.state) → (newState)")
        return
    }
    
    // 执行转换
    self.state = newState
    self.stateObject = createState(newState)
}

private func isValidTransition(from: RoomState, to: RoomState) -> Bool {
    let validTransitions: [RoomState: [RoomState]] = [
        .preparing: [.listing],
        .listing: [.auctioning],
        .auctioning: [.closed],
        .closed: [.preparing]
    ]
    
    return validTransitions[from]?.contains(to) ?? false
}

5. 提供状态查询接口

extension Room {
    var canStartAuction: Bool {
        return stateObject.startAuction(room: self)
    }
    
    var canPlaceBid: Bool {
        return state == .auctioning
    }
    
    var canUploadItem: Bool {
        return state == .preparing
    }
}

// 使用
if room.canPlaceBid {
    room.stateObject.placeBid(...)
}

6. 编写完整的单元测试

class StatePatternTests: XCTestCase {
    func testStateTransitions() {
        let room = Room(...)
        
        // 测试初始状态
        XCTAssertEqual(room.state, .preparing)
        
        // 测试状态转换
        room.stateObject.startAuction(room: room)
        XCTAssertEqual(room.state, .listing)
        
        // 等待自动转换
        wait(for: 3)
        XCTAssertEqual(room.state, .auctioning)
    }
    
    func testInvalidOperations() {
        let room = Room(...)
        
        // 在准备阶段不能出价
        let result = room.stateObject.placeBid(...)
        XCTAssertFalse(result)
    }
}

总结

何时使用状态模式

适合使用的场景

  1. 对象行为随状态改变而改变
  2. 有明确的状态转换规则
  3. 状态相关的代码较多
  4. 需要避免大量的条件判断

不适合使用的场景

  1. 状态很少(2-3个)
  2. 状态间没有明确的转换规则
  3. 状态逻辑非常简单
  4. 性能要求极高的场景

状态模式的价值

在拍拍房项目中,状态模式:

  1. 将复杂的业务流程结构化
    • 4个状态,4个类,清晰明了
    • 每个状态独立,互不干扰
  1. 提高代码质量
    • 避免了数百行的switch语句
    • 符合单一职责原则
    • 符合开闭原则
  1. 增强可维护性
    • 修改某个状态不影响其他状态
    • 新增状态只需添加新类
    • 状态转换一目了然
  1. 改善团队协作
    • 不同开发者可以独立开发不同状态
    • 减少Git冲突
    • 代码审查更容易
  1. 与权限中心完美配合
    • 状态负责"怎么做"
    • 权限负责"能不能做"
    • 职责清晰,耦合度低

最后的建议

  1. 不要过度设计:如果只有2-3个简单状态,可能不需要状态模式
  2. 绘制状态图:在实现之前先画出状态转换图
  3. 编写测试:为每个状态编写单元测试
  4. 文档化:记录每个状态的职责和转换规则
  5. 逐步重构:可以先用简单方式实现,再重构为状态模式

参考资源

设计模式相关

  • 《设计模式:可复用面向对象软件的基础》- GoF
  • 《Head First 设计模式》

本项目相关

5 分钟把 Coze 智能体嵌入网页:原生 JS + Vite 极简方案

作者 烟袅
2025年11月29日 17:49

你已经创建好了 Coze 智能体,现在想快速把它接入一个网页?不用 React、不用 Vue,甚至不用手敲 npm create —— 本文教你用 trae 的 AI 助手 + 原生 HTML/JS,5 分钟搭建一个可运行、可部署、安全调用 Coze OpenAPI 的前端 Demo。

我们将实现:

  • 通过 trae AI 一键生成项目并初始化 Vite
  • 安全注入 Bot ID 和 API Token
  • 调用 Coze 接口实现问答交互

一、用 trae AI 快速搭建项目(无需手动命令)

告别 npm init 和配置文件!我们借助 trae 的右侧 AI 对话栏,全自动完成项目创建。

操作步骤如下:

  1. 打开 trae 平台,进入任意工作区

  2. 在右侧 AI 对话框 中输入:

    创建一个通用的原生HTML/CSS/JS 项目
    
  3. 等待 AI 生成基础结构(通常包含 index.htmlmain.jsstyle.css

  4. 接着在同一对话中继续输入:

    帮我初始化vite配置
    
  5. AI 会自动为你:

    • 创建 vite.config.js
    • 添加 package.json 脚本(如 devbuild
    • 安装 vite 依赖(或提示你运行 npm install

✅ 此时你已拥有一个标准的 Vite 原生 JS 项目,无需任何手动配置!

将项目同步到本地后,执行:

npm run dev

确保页面能正常打开,接下来我们集成 Coze。


二、获取 Coze 智能体凭证

  1. 复制两个关键信息:

    • Bot ID 进入你的智能体,在链接最后那一串就是你的ID,选择复制

    • API Key 点击Web SDK 将其中的token复制下来

image.png

⚠️ 这个 API Key 具有调用权限,请务必保密!

关于智能体具体的创建 juejin.cn/post/757769… 这篇文章里面有,当然智能体发布的时候一定要选择API选项


三、安全注入环境变量

在项目根目录创建 .env.local 文件:

VITE_BOT_ID=your_actual_bot_id
VITE_API_KEY=your_actual_api_key

🔒 Vite 只会暴露以 VITE_ 开头的变量到客户端代码,这是官方推荐的安全做法。


四、编写前端交互逻辑

1. index.html

可以把trae生成的代码删掉用下面这份

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Coze API Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>
</head>
<body>
  <h1>Coze API Demo 随处智能</h1>
  <input type="text" id="ipt" placeholder="请输入问题">
  <div id="reply">think...</div>
  <script type="module" src="./script.js"></script>
</body>
</html>

在这段代码看起有点不一样

<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>

是哪里冒出来的呢?

其实加上这个主要是为了待会我们从智能体那里获取图片展示到网页上,如果不加的话我们只会获得图片的链接,这还要结合待会的js一起使用

2. main.js

const ipt = document.getElementById('ipt');
const reply = document.getElementById('reply');
const endpoint = 'https://api.coze.cn/open_api/v2/chat';
// DOM 2 
ipt.addEventListener('change',async function(event) {
  const prompt = event.target.value;
  console.log(prompt);
  const payload = {
    bot_id: import.meta.env.VITE_BOT_ID,
    user: 'yvo',
    query: prompt,
    chat_history:[],
    stream: false,
    custom_variables: {
      prompt: '你是一个AI助手'
    }
  }
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${import.meta.env.VITE_API_KEY}`
    },
    body: JSON.stringify(payload)
  })
  const data = await response.json();
  console.log(data, '////');
  // reply.innerHTML = data.messages[1].content;
  reply.innerHTML=marked.parse(data.messages.find(item => item.type === 'answer').content);
})

代码分析

  reply.innerHTML=marked.parse(data.messages.find(item => item.type === 'answer').content);

这段代码可能看起来有点突兀,那我们拆开来看首先我们看吧

data.messages.find(item => item.type === 'answer').content

这主要是获取智能体的回答,这时就有人问了一般获取信息不都是使用 .choices[0].message.content来获取吗?

但是coze的智能体返回的结构是不一样的

image.png

看这个结构很容易观察到其实coze智能体返回的结构需要在messages[1].content或type:"answer"才能拿到结果,这就是coze与我们调用一般的llm不一样的地方。

接下来我们继续分析

marked.parse()

将 Markdown 格式的字符串 → 转换成 HTML 字符串

这样浏览器才能正确显示标题、列表、链接、图片等内容。

这也就实现了我们能在页面上获取智能体给我们的图片了。 我们可以删去试试看效果

image.png

我们并没有得到我们想要的只获得了https地址

那加上试试呢?

image.png

成功将照片拿到。

 const payload = {
    bot_id: import.meta.env.VITE_BOT_ID,
    user: 'yvo',
    query: prompt,
    chat_history:[],
    stream: false,
    custom_variables: {
      prompt: '你是一个AI助手'
    }

这段代码好像见的也不多,这段其实就要根据www.coze.cn/open/docs/d… coze的官方文档去使用了


五、启动 & 验证

npm run dev

在浏览器输入问题(如“JavaScript 如何判断数组?”),即可看到 Coze 智能体的实时回复!


七、常见问题

Q:返回 {"code":4101,"msg":"The token you entered is incorrect"}
A:请检查:

  • .env.local 是否命名正确
  • Token 是否正确或过期

结语

通过 trae AI + Vite + Coze OpenAPI,我们用最轻量的方式实现了智能体前端集成。整个过程:

  • 无框架负担
  • 无复杂构建
  • 环境变量安全隔离
  • 代码清晰可维护

一个输入框,一行 API 调用,背后是千行训练数据与万亿参数的智能体在为你思考。
而你,只用了 5 分钟,就把它请进了自己的网页。
这不是魔法——这是新时代前端工程师的日常。

从「似懂非懂」到「了如指掌」:Promise 与原型链全维度拆解

2025年11月29日 17:36

前言

在前端世界里,Promise原型链(Prototype) 是两个看似毫不相干,却又互相影响、甚至能相互解释的重要概念。

很多人学习 Promise 时,会关注它的使用:thencatchfinallyPromise.all 等;
学习原型链时,又会关注 __proto__prototype、构造函数与实例之间的关系。

但鲜有人把 Promise 本身也是一个对象,它也依赖原型链运作 这件事真正联系起来讲透。

本文将以一次完整的 Promise 异步流程为主线,把“原型链 + 状态机 + 微任务”融合讲解,让你完全理解 Promise 到底是怎么在底层“跑”起来的。


一、Promise 为什么是“对象”?

我们常常写:

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve('OK'), 1000)
})

很多人知道 Promise 是“异步解决方案”,但忽略了一个基本事实:

Promise 是一个构造函数(类),你创建的是它的实例。

也就是说:

  • Promise —— 构造函数(带 prototype
  • p —— 实例对象(带 __proto__

打开控制台试试:

console.log(p.__proto__ === Promise.prototype) // true

这里马上就把原型链扯进来了。

🔍 Promise.prototype 上都有啥?

输入:

console.log(Promise.prototype)

你会看到:

then: ƒ then()
catch: ƒ catch()
finally: ƒ finally()
constructor: ƒ Promise()
...

这说明:

所有 Promise 实例都是通过原型链访问 then/catch/finally 的。

也就是说 p.then() 并不是实例自身有,而是:

p ---> Promise.prototype ---> Object.prototype ---> null

这为后文理解 Promise “链式调用”机制奠定基础。


二、原型链视角下,看懂 Promise 的执行流

我们直接看一个你提供的代码精简版:

const p = new Promise((resolve, reject) => {
  console.log(111)
  setTimeout(() => {
    reject('失败1')
  }, 1000)
})

console.log(222)

p.then(data => {
  console.log(data)
}).catch(err => {
  console.log(err)
}).finally(() => {
  console.log('finally')
})

输出顺序:

111
222
失败1
finally

要理解为什么 Promise 能这样执行,必须从两个角度讲:

  • (1)Promise 内部是状态机(pending → fulfilled / rejected)
  • (2)then/catch/finally 是通过原型链挂载的“回调注册器”

我们分开看看。


1)Promise 内部是一个状态机

内部状态(无法手动修改):

状态 描述 何时出现
pending 初始状态 执行 executor 期间
fulfilled resolve 被调用 成功
rejected reject 被调用 失败

也就是说:

new Promise(executor)

执行后:

  • 立即执行 executor
  • executor 只在同步阶段运行
  • 真正的 resolve/reject 回调是“挂起来”,等事件循环驱动

所以你看到:

111(executor 同步执行)
222(外部同步执行)
失败1(异步到点后 reject)
finally(状态 settled 后触发)

2)then/catch/finally:它们不是魔法,是原型链的方法

看看这段链式调用:

p.then(...).catch(...).finally(...)

为什么可以一直“链式”?

因为每次调用 then 都 返回一个新的 Promise 实例

p.then(...) → p2
p2.catch(...) → p3
p3.finally(...) → p4

这几个实例的原型链依然是:

p2.__proto__ === Promise.prototype
p3.__proto__ === Promise.prototype
...

因此:

链式本质 = 每次链式都返回一个新的 Promise 实例,然后继续在原型链上查找 then/catch/finally。

这就是原型链在 Promise 底层的重要性。


三、原型链的类比:Promise 就像“火车头 + 车厢”系统

你提到的类比非常棒,我把它整理成完整模型:

✨ Promise = 火车系统

  • 构造函数(Promise) = 火车制造厂
  • 原型对象(Promise.prototype) = “火车车厢模板”
  • 实例(p) = 火车头
  • then/catch/finally = 可以接在车头后的“车厢类型”

于是我们看到:

p(车头).then(挂一个车厢)
         .then(再挂一节)
         .catch(挂一个处理失败的车厢)
         .finally(挂尾部的清理车厢)

每次挂车厢(调用 then/catch)时,都会生成 新的火车车头(新的 Promise 实例)

整个火车最终沿着轨道(事件循环)开动到终点。

⚠️ 注意:为什么 finally 一定执行?

因为 finally 不关心结果,只关心火车是否开到终点(settled)。


四、Promise 与普通对象原型链的对比

你提供了一个经典例子:

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.speci = '人类'

let zhen = new Person('白兰地空瓶', 18)
console.log(zhen.speci)

const kong = {
  name: '空瓶',
  hobbies: ['读书', '喝酒']
}

zhen.__proto__ = kong

console.log(zhen.hobbies, zhen.speci)

输出:

人类
['读书','喝酒'] undefined

这个例子非常适合用来对比 Promise 的原型链逻辑。

对比 1:实例可以动态改原型(不推荐)

zhen.__proto__ = kong 改掉了原来的 Person.prototype

所以:

  • 能访问 hobbies:因为来自 kong
  • 不能访问 speci:因为已脱离 Person.prototype

Promise 则不能做这种事

你不能这样做:

p.__proto__ = {}

否则:

p.then is not a function

因为 then/catch/finally 都来自 Promise.prototype。

这反而让我们更清楚地理解:

Promise 的能力几乎全部来自原型链。


五、Promise.all 的底层逻辑:一辆多车头的“联挂火车”

提到 Promise.all,这里正好顺便讲讲它的底层设计。

Promise.all([p1, p2, p3])

机制可以用一个形象类比解释:

  • 假设有三辆火车(p1/p2/p3)
  • Promise.all 创建一辆“总火车头” pAll
  • pAll 盯着三个火车头,只要全部变成 fulfilled,就把所有结果一次性返回
  • 如果有一个 reject,则整个 pAll 变成 rejected(列车脱轨)

也就是说:

Promise.all = 多个 Promise 状态机的并联 + 一个新的总状态机。

为什么它能做到?

答案依旧在原型链:

  • Promise.all 本质是一个静态方法,返回新的 Promise 实例
  • 新的 Promise 实例依然沿用同一套路(prototype → then/catch)

六、用真实工程场景收尾:Promise 原型链为什么重要?

在真实项目里,理解 Promise 的原型机制有三个实际价值:

① debugger 时能看清原型链,定位异步回调来源

你能区分:

  • then 回调从哪里来的?(Promise.prototype.then)
  • promise 链断在哪一层?

② 手写 Promise 时必须实现 then/catch/finally

如果你手写 Promise A+:

MyPromise.prototype.then = function(onFulfilled, onRejected) {}

这里你就必须自己处理链式、状态机、回调队列。

③ 能理解 async/await 的底层依赖 Promise 链式调度

await 会把后续步骤注册到 promise.then 中。

理解 then 的原型链,就能理解 async/await 的机制本质。


七、总结:Promise + 原型链的全景图

// 创建实例
const p = new Promise(executor)

// 原型链:调用能力来自这里
p.__proto__ = Promise.prototype

// 状态机:内部维护 pending → fulfilled/rejected

// then/catch/finally:注册微任务

// 链式调用:每次都返回一个新的 Promise 实例

// Promise.all:多个状态机的并联

一句话总结:

Promise 本质是一个基于“原型链 + 状态机 + 微任务队列”的异步调度框架。

它既是面向对象设计(通过原型链复用方法),又是异步控制核心工具(内部状态机)。

理解二者的融合,你就真正吃透了 Promise。

🧠 深入理解 JavaScript Promise 与 `Promise.all`:从原型链到异步编程实战

2025年11月29日 17:32

在现代 JavaScript 开发中,Promise 是处理异步操作的核心机制之一。ES6 引入的 Promise 极大地简化了“回调地狱”(Callback Hell)问题,并为后续的 async/await 语法奠定了基础。而 Promise.all 则是并发执行多个异步任务并统一处理结果的强大工具。

本文将结合 原型链原理Promise 基础用法实际示例代码,带你系统掌握 Promise 及其静态方法 Promise.all 的使用与底层逻辑。


🔗 一、JavaScript 的面向对象:原型链而非“血缘”

在深入 Promise 之前,我们先厘清一个关键概念:JavaScript 的继承不是基于“类”的血缘关系,而是基于原型(prototype)的链式查找机制

1.1 🏗️ 构造函数与原型对象

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.speci = '人类';

let zhen = new Person('张三', 18);
console.log(zhen.speci); // 输出: "人类"
  • Person 是构造函数。
  • Person.prototype 是所有 Person 实例共享的原型对象。
  • zhen.__proto__ 指向 Person.prototype
  • Person.prototype.constructor 又指回 Person,形成闭环。

🚂 小比喻:可以把 constructor 看作“车头”,prototype 是“车身”。实例通过 __proto__ 连接到车身,而车身知道自己的车头是谁。

1.2 ⚡ 动态修改原型链(不推荐)

const kong = {
    name: '孔子',
    hobbies: ['读书', '喝酒']
};

zhen.__proto__ = kong;
console.log(zhen.hobbies);     // ✅ 输出: ['读书', '喝酒']
console.log(kong.prototype);   // ❌ undefined!普通对象没有 prototype 属性

⚠️ 注意:

  • 只有函数才有 prototype 属性;
  • 普通对象(如 kong)只有 __proto__,没有 prototype
  • 在这里kong是object的一个实例kong.__prpto__ == object.prototype

💡 虽然可以动态修改 __proto__,但会破坏代码可预测性,影响性能,应避免使用。


⏳ 二、Promise:ES6 的异步解决方案

2.1 🧩 Promise 基本结构

<script>
const p = new Promise((resolve, reject) => {
    console.log(111); // 同步执行
    setTimeout(() => {
        console.log(333);
        // resolve('结果1');  // 成功
        reject('失败1');      // 失败
    }, 1000);
});

console.log(222);
console.log(p, '////////'); // 此时 p 状态仍是 pending
console.log(p.__proto__ == Promise.prototype); // true
</script>

📋 执行顺序分析:

  1. 111 立即输出(executor 函数同步执行)✅
  2. 222 紧接着输出 ✅
  3. p 此时处于 pending(等待) 状态 ⏳
  4. 1 秒后,333 输出,调用 reject('失败1'),状态变为 rejected
  5. .catch() 捕获错误,.finally() 无论成功失败都会执行 🔁

2.2 🎯 Promise 的三种状态

  • ⏳ pending:初始状态,既不是成功也不是失败。
  • ✅ fulfilled:操作成功完成(通过 resolve 触发)。
  • ❌ rejected:操作失败(通过 reject 触发)。

🔒 核心特性:一旦状态改变,就不可逆。这是 Promise 的设计基石。

2.3 🔍 原型关系验证

console.log(p.__proto__ === Promise.prototype); // ✅ true
  • pPromise 的实例。
  • 所有 Promise 实例的 __proto__ 都指向 Promise.prototype
  • Promise.prototype 上定义了 .then(), .catch(), .finally() 等方法。
  • Promise.prototype.__proto__ == object.prototype

🚀 三、Promise.all:并发处理多个异步任务

3.1 ❓ 什么是 Promise.all

Promise.all(iterable) 接收一个可迭代对象(如数组),其中包含多个 Promise。它返回一个新的 Promise:

  • ✅ 全部成功 → 返回一个包含所有结果的数组(顺序与输入一致)。
  • ❌ 任一失败 → 立即 rejected,返回第一个失败的原因。

3.2 💻 使用示例

const task1 = fetch('/api/user');       // 假设返回 { id: 1, name: 'Alice' }
const task2 = fetch('/api/posts');      // 假设返回 [{ title: 'JS' }]
const task3 = new Promise(resolve => setTimeout(() => resolve('done'), 500));

Promise.all([task1, task2, task3])
  .then(([user, posts, msg]) => {
    console.log('全部完成:', user, posts, msg);
  })
  .catch(err => {
    console.error('某个任务失败:', err);
  });

🌐 适用场景:需要同时加载用户信息、文章列表、配置数据等,全部就绪后再渲染页面。

3.3 ⚠️ 错误处理演示

const p1 = Promise.resolve('成功1');
const p2 = Promise.reject('失败2');
const p3 = Promise.resolve('成功3');

Promise.all([p1, p2, p3])
  .then(results => console.log('不会执行'))
  .catch(err => console.log('捕获错误:', err)); // 输出: "失败2"

关键点:只要有一个失败,整个 Promise.all 就失败,其余成功的 Promise 结果会被丢弃。

3.4 🛡️ 替代方案:Promise.allSettled(ES2020)

如果你希望无论成功失败都等待所有任务完成,可以使用 Promise.allSettled

Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach((res, i) => {
      if (res.status === 'fulfilled') {
        console.log(`✅ 任务${i} 成功:`, res.value);
      } else {
        console.log(`❌ 任务${i} 失败:`, res.reason);
      }
    });
  });

✅ 适用于:批量上传、日志收集、非关键资源加载等场景。


📚 四、总结:从原型到实践

概念 说明
🔗 原型链 JS 对象通过 __proto__ 查找属性,constructor 指回构造函数
Promise 表示异步操作的最终完成或失败,具有 pending/fulfilled/rejected 三种状态
🧩 Promise.prototype 所有 Promise 实例的方法来源(.then, .catch 等)
🚀 Promise.all 并发执行多个 Promise,全成功则成功,任一失败则整体失败
🛡️ 最佳实践 使用 Promise.all 提升性能;用 allSettled 处理非关键任务

💭 五、思考题

  1. 🤔 为什么 console.log(p)setTimeout 之前打印时,状态是 pending
  2. 🛠️ 能否通过修改 Promise.prototype.then 来全局拦截所有 Promise 的成功回调?这样做有什么风险?
  3. 📦 如果 Promise.all 中传入空数组 [],结果会是什么?

💡 答案提示

  1. 因为异步任务尚未执行,状态未改变。
  2. 技术上可行,但会破坏封装性、可测试性和团队协作,强烈不推荐
  3. 立即 resolved,返回空数组 [] —— 这是符合规范的!

通过本文,你不仅掌握了 PromisePromise.all 的用法,还理解了其背后的 原型机制异步执行模型。这将为你编写健壮、高效的异步代码打下坚实基础。🌟

Happy Coding! 💻✨

北京AI产业规模今年将超4500亿元

2025年11月29日 17:06
北京市科学技术委员会、中关村科技园区管委会今天(11月29日)正式发布《北京人工智能产业白皮书(2025)》。《白皮书》数据显示,2025年上半年,北京全市人工智能核心产业规模2152.2亿元,同比增长25.3%。初步估算2025年全年,产业规模有望超过4500亿元。(财联社)

国家航天局设立商业航天司 持续推动商业航天高质量发展

2025年11月29日 16:57
据国家航天局消息,该局已于近期设立商业航天司,相关业务正在逐步开展,标志着我国商业航天产业迎来专职监管机构,未来将持续推动我国商业航天高质量发展,产业链有望全线受益。记者了解到,国家航天局近日公布推进商业航天高质量安全发展行动计划(2025—2027年),提出将商业航天纳入国家航天发展总体布局,加快形成航天新质生产力,实现航天发展效能整体提升,有力支撑航天强国建设。这项计划明确,到2027年,商业航天产业生态高效协同,科研生产安全有序,产业规模显著壮大,创新创造活力显著增强,资源能力实现统筹建设和高效利用,行业治理能力显著提升,基本实现商业航天高质量发展。(财联社)

央行:继续坚持对虚拟货币的禁止性政策 持续打击虚拟货币相关非法金融活动

2025年11月29日 16:57
中国人民银行2025年11月28日召开打击虚拟货币交易炒作工作协调机制会议。会议要求,各单位要坚持以习近平新时代中国特色社会主义思想为指导,全面落实党的二十大和二十届历次全会精神,把防控风险作为金融工作的永恒主题,继续坚持对虚拟货币的禁止性政策,持续打击虚拟货币相关非法金融活动。各单位要深化协同配合,完善监管政策和法律依据,聚焦信息流、资金流等重点环节,加强信息共享,进一步提升监测能力,严厉打击违法犯罪活动,保护人民群众财产安全,维护经济金融秩序稳定。(财联社)

从摄影新手到三维光影师:Three.js 核心要素的故事

作者 一千柯橘
2025年11月29日 16:43

当我第一次学习摄影时,老师告诉我一句话:

“你不是在拍东西,而是在拍光。”

后来我学习 Three.js 时突然意识到:
这句话原来依旧成立。

Three.js 不只是一个 3D 引擎,更像是一台虚拟相机。要拍好这张“虚拟的照片”,我们必须掌握三个核心要素:

场景(Scene)

相机(Camera)
灯光与材质(Light & Material)

于是,我把学习过程想象成一个摄影新手成长为三维光影师的故事。

空无一物的影棚 —— Scene 场景

故事从一个空影棚开始。

当我第一次打开 Three.js 时,教程告诉我:

const scene = new THREE.Scene();

这就像摄影师走进了一个空旷的工作室:
没有布景、没有模特、没有灯光,甚至连相机都还没架好, 在影棚这个场景中,摄影师可以在这个场景中放任何的东西:

  • 架好摄像机(Camera 📹)
  • 拍照的物体(Mesh 网格物体)、物体拥有着自己的形状(Geometry几何体)和材质(Material)
  • 摆设好灯光(Light)
  • 也可以是任意的对象 (Object3D)

摄影师往 Scene 里布置道具,而程序员的你往 Scene 里添加各种对象,因此 场景就是一个可以放任何东西的容器

找到你要观看的角度 —— Camera 相机

刚学摄影时,我最常做的事情,就是移动、蹲下、趴着、绕圈……
只为了找到一个“对的角度”。

Three.js 的相机就是你的眼睛。创建相机就像准备拍摄时拿起单反:

const camera = new THREE.PerspectiveCamera(const camera = new THREE.PerspectiveCamera(
  50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
  1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);

// 2.1 设置相机的位置,放在不同的位置看到的风景当然不一样
camera.position.set(5, 10, 10); // x, y, z

camera.lookAt(0, 0, 0); // 设置相机方向(这就是你女朋友让你找最佳角度的原因)

摄影师会说:“我走两步,让模特在背景中更突出。”
程序员会说:

camera.position.z = 3;
camera.lookAt(0, 0, 0)

本质完全一样:
都是在调整观察世界的方式。

让世界真正亮起来 —— Light & Material 灯光与材质

你可以有再漂亮的模特、再好的相机,如果没有光——
一切都会变成漆黑一片。

Three.js 也是如此。你搭了一个完美的 3D 模型,如果没有光,它看起来只是纯黑。

于是我制作“虚拟布光”:

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 5);
scene.add(light);

摄影师打灯,而我在代码里放置光源:

  • DirectionalLight(平行光)= 太阳光
  • PointLight(点光源)= 想象灯泡发光,由点向八方发射
  • SpotLight(聚光灯)= 舞台灯,从上打下来,呈现圆锥体,它离光越远,它的尺寸就越大。这种光源会产生阴影
  • AmbientLight(环境光)= 影棚柔光,环境光没有特定的来源方向,不会产生阴影

同时材质(Material)也等同于现实世界的“被光击中时的反应”:

  • 皮肤 = standard material
  • 金属 = metalness 高
  • 塑料 = roughness 较高
  • 玻璃 = transparent=True + envMap

想要一个皮肤质感的物体?
那么你就得给材质加入 roughness、metalness、normalMap 就像摄影师在打柔光,为人物皮肤创造质感。

光与材质的搭配,就是 Three.js 里的“布光艺术”。

最终章:按下快门 —— Renderer 渲染器

当场景布好、相机调好、灯光到位后——
摄影师要做的就是按下快门。

在 Three.js 里:

renderer.render(scene, camera);

渲染器就是那个“快门”,
真正把世界投射到屏幕上。

摄影师用快门把现实世界的光记录下来;
Three.js 用 GPU 把虚拟世界的光影计算出来。

本质上,两者做的是同一件事:

把真实或虚拟的三维世界,投射成一张二维图像。

import * as THREE from "three";

// 1. 创建场景
const scene = new THREE.Scene();

// 2. 创建相机(透视投影相机)
const camera = new THREE.PerspectiveCamera(
  50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
  1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);

// 2.1 设置相机的位置
camera.position.set(5, 10, 10); // x, y, z

camera.lookAt(0, 0, 0); // 设置相机方向(默认看向场景原点)

// 3. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿,使边缘更平滑
// 3.1 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 3.2 将渲染器的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// 4. 创建一个立方体几何体
const geometry = new THREE.BoxGeometry(4, 4, 4); // 宽、高、深

// 为了让光源有效果,我们使用 MeshLambertMaterial 或 MeshPhongMaterial
//  创建材质 MeshLambertMaterial (兰伯特材质) 是一种非光泽材质,会受光照影响,但没有镜面高光
const material = new THREE.MeshLambertMaterial({
  color: 0x00ff00, // 颜色
  // wireframe: true, // 如果需要线框效果可以加上
});

// 6. 创建一个网格模型(网格模型由几何体和材质组成)
// Mesh 构造函数通常只接受一个材质。如果需要多材质,Three.js 有专门的 MultiMaterial 或 Group 来处理
const cube = new THREE.Mesh(geometry, material); // 使用 MeshLambertMaterial

// 6.1 将几何模型添加到场景中
scene.add(cube);

// 6.2 设置相机看向物体(拍摄对象)的位置(默认状态下相机看向的是场景的原点(0,0,0))
camera.lookAt(cube.position);

// 7. 创建光源
const spotLight = new THREE.SpotLight(0xffffff); // 创建聚光灯,颜色为白色
// 7.1 设置光源的位置
spotLight.position.set(0, 20, 20); // 调整光源位置,使其能够照亮立方体
// 7.2 设置光源照射的强度,默认值为1, 越大越亮
spotLight.intensity = 2;
// 7.3 将光源添加到场景中
scene.add(spotLight);

// 8. 为了方便观察 3D 图像,添加三维坐标系对象
const axesHelper = new THREE.AxesHelper(6); // 参数表示坐标系的大小 (x轴红色, y轴绿色, z轴蓝色)
scene.add(axesHelper); // 将坐标系添加到场景中

// 9. 渲染函数
function animate() {
  requestAnimationFrame(animate); // 请求再次执行渲染函数animate,形成循环

  // 让立方体动起来
  cube.rotation.x += 0.01; // 沿x轴旋转
  cube.rotation.y += 0.01; // 沿y轴旋转
  cube.rotation.z += 0.01;

  renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染出来
}

animate(); // 执行渲染函数,进入无限循环,完成渲染
❌
❌