普通视图

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

美联储4月维持利率不变的概率为99.5%

2026年4月20日 07:26
据CME“美联储观察”:美联储4月加息25个基点的概率为0.5%,维持利率不变的概率为99.5%。美联储到6月累计降息25个基点的概率为4.5%,维持利率不变的概率为95%,累计加息25个基点的概率为0.5%。(财联社)

外围扰动正逐步钝化,机构看好科技主线热度延续

2026年4月20日 07:24
过去一周,中东局势缓和预期发酵,市场情绪逐渐回暖,创业板指走出强势独立行情。展望后市,券商策略报告认为,虽然中东局势处于拉锯博弈状态,其走向仍存诸多变数,但地缘冲击最大的阶段已经过去,市场对外围因素扰动的反应正逐步钝化。当前正值A股财报密集披露期,算力、储能、光通信等科技成长赛道龙头公司盈利确定性突出。后续资金向高景气度成长板块集中的趋势仍将持续一段时间。 (上证报)

喜宝婴幼儿食品疑遭鼠药污染,欧洲多国下架喜宝产品

2026年4月20日 07:21
当地时间4月19日,欧洲知名婴幼儿食品生产商喜宝(HiPP)公司表示,在捷克和斯洛伐克发现了疑似被鼠药污染的该品牌婴幼儿罐装食品。公司声明称“在调查过程中确认,这些食品明显被人为动过手脚”。喜宝公司表示,出于预防措施,两国的零售商已将所有该品牌婴幼儿罐装食品从货架上撤下。奥地利警方表示,消费者可以通过以下特征识别问题产品:底部贴有白色标签并带红色圆圈、瓶盖已被打开或损坏、缺少安全封口,或有异常气味。喜宝公司表示,此次事件仅涉及部分分销渠道,其他未被纳入调查的欧洲国家未受影响。(央视财经)

国投智能:公司已构建全链条AI安全能力体系

2026年4月20日 07:18
36氪获悉,国投智能4月19日在业绩说明会上表示,端侧AI Agent快速爆发带来系统性安全挑战,是规模化落地的关键瓶颈。公司已构建全链条AI安全能力体系,覆盖检测(美亚“慧眼”鉴真)、防护(“星盾”平台)、取证(OpenClaw专项取证模块)、治理(研究“龙虾证”实现AI行为责任认定)全环节。

国家发展改革委下达2026年第二批“两重”建设项目清单

2026年4月20日 07:15
19日从国家发展改革委获悉,近日,国家发展改革委会同有关部门组织下达2026年第二批“两重”建设项目清单,共安排超长期特别国债资金2168亿元支持336个重大项目。 记者了解到,相关项目涉及人工智能、城市地下管网建设改造、长江经济带交通基础设施、高标准农田、高等教育提质升级、“三北”工程等重点领域。加上此前已下达的3897亿元,今年累计安排“两重”建设资金6065亿元,占全年8000亿元的76%,下达进度明显快于去年。 国家发展改革委有关负责人表示,同时将进一步完善优化投融资等机制,加快实施“软建设”措施,强化中央投资资金监管,尽早形成更多实物工作量。(新华社)

2026中国互联网发展座谈会在京召开

2026年4月20日 07:12
36氪获悉,据工信微报,4月19日,中国互联网发展座谈会在北京召开。会议强调,有包容并蓄的格局,甘做融通发展的推动者。主动开放技术、数据和市场资源,带动产业链上下游企业,构建大中小企业融通发展的良好生态。要有扎根实业的定力,善做产业升级的赋能者。前瞻谋划6G、智能体互联网等新型基础设施建设,培育壮大新兴产业,前瞻布局未来产业,加快发展智能经济。要有造福社会的担当,争做合规向善的践行者。

内存价格可能还要涨,HBM千亿美元大市场来了

2026年4月20日 07:04
长江证券电子行业资深分析师蔡少东表示:“HBM (高带宽内存)这个新的产品在过去三年其实完成了10倍以上的增长,我们可以看到2025年整个市场规模是在350亿美元,预计2027到2028年整个市场应该会突破1000亿美元。”“我们自己的判断是,在二季度合约的价格,有希望环比上涨30%到50%,有一些部分可能会比这更高。在下半年三四季度,整个存储的合约价涨幅会趋于收敛。” (央视财经)

行业首发!线控制动量产上车,奇瑞星途 EX7 上市售价 19.99 万元起

作者 李华
2026年4月20日 00:58

今晚,奇瑞星途品牌的全新车型——星途 EX7 正式上市。星途推出了纯电与增程两种动力共 6 款车型,官方指导价为 19.99 万至 26.39 万元。

从定价和车型布局来看,这台车同样是冲着当下的主流家用市场来的。

在现在的新能源市场,谈论「豪华」似乎成了一件很容易的事。冰箱、大屏、沙发几乎成了新车的标配,但在内卷的狂潮中,星途这次想讲的故事有些不太一样。

任何豪华品牌都必须以安全、性能、品质为根基,没有这个根基,再拼搏一辈子都是不能成功的。

奇瑞汽车股份有限公司常务副总裁张国忠在现场的这句话,奠定了整场发布会的基调。比起单纯的配置叠加,星途 EX7 更想强调一台车作为交通工具的基础素质。

首先是直观的尺寸数据。

星途 EX7 采用了「星际美学」设计语言,长宽高分别达到了 4988mm、1975mm 和 1710mm,轴距长达 3000mm。在现场,高管团队将这种车身比例简单概括为「532」。

作为一台主打「大五座」的 SUV,近 5 米的车长和 3 米的轴距,主要服务于舱内的两排乘客。

除了宽裕的第二排乘坐空间,放倒座椅后还能形成一个长达 2.2 米的平整空间。搭配 1800 升的后备箱容积和 60 升的前备箱,装载能力足够应对家庭远途出行。

回到车内,星途 EX7 也没有落下当下流行的智能化体验。

中控台配备了一块 30 英寸的 6K 一体屏,内置高通骁龙 8295P 芯片,以支撑 AI 灵犀智舱 2.0 的运行。

前排给到了零重力座椅,舱内还配备了一台 9 升容量的冰箱,支持 -18°C 到 50°C 的宽温域调节。

不过,真正让人觉得好用的,反而是那些不起眼的细节设计。

开发团队并没有把所有功能都塞进中控屏里。星途 EX7 将第二排座椅的通风、加热和按摩控制按键,直接做成了物理按键,安置在后排的门板侧边。

而在音响系统的硬件思路上,星途的做法也很典型。新车搭载了 23 个扬声器的音响系统,为了减少音频信号的传输损耗,工程师在金属插接线上使用了足金材质。

奇瑞汽车股份有限公司执行副总裁李学用在台上解释了原因:「我们为什么要用真黄金?是为了保证信号的稳定,让信号抗氧化、百分之百传递,让整车的音响更稳。」

这种死磕物理细节的做法,倒确实挺符合外界对奇瑞「理工男」的固有印象。

动力上,星途 EX7 提供了纯电和增程两套方案,以此来覆盖不同用户的出行半径。

增程版搭载了一台热效率接近 46% 的 1.5T 发动机作为增程器,配合 41.16 度的电池包,CLTC 纯电续航在 225 到 245 公里之间。如果是四驱版本,电机总功率可以达到 374kW。

纯电版全系基于 800V 高压平台打造。它提供了 70.01 度和 100.26 度两款宁德时代电池,对应的 CLTC 续航里程分别为 600、682 和 726 公里。

另外,李学用还提到了一个「期货」。

到今年四季度,我们就能够买到搭载半固态电池的 EX7,它的能量密度将由现在的 200Wh/kg 提升到 300Wh/kg。

在底盘硬件上,这台车也堆得足够满。

飞鱼底盘 3.0 系统的物理底子,是前全铝合金双叉臂加上后五连杆的独立悬架,另外空气弹簧和 CDC 电磁减振系统自然也没有缺席。不过,硬件堆料只是及格线,底盘的灵魂在于后期的调校。

星途这次找来了老牌工程公司西班牙 IDIADA(伊迪亚达)进行联合调校。谈到这背后的功夫,李学用在台上打了个很形象的比方。

底盘调校是一门很深刻的学科。它就像老中医一样,要有深厚的底蕴;又要像西医一样,有强大的数据库来支撑。

为了找准几块减振阀片在不同路况下的细微差别,底盘工程师需要常年在全球各地跑测试,最终呈现出来的数据确实直观。

在实测中,这台自重不轻的中大型 SUV,做出了 145km/h 的双移线成绩,麋鹿测试也达到了 76km/h。对于一台接近 5 米长的大车来说,这是一个相当不错的动态指标。

但这还不是今晚的技术重头戏。

整场发布会花去最多时间拆解的,是那套全球首搭的航空级 EMB 线控制动系统。简单来说,它彻底抛弃了汽车用了一百多年的液压刹车管路,直接用电信号控制电机来夹紧刹车盘。

这样做的好处立竿见影,没有了液压油的传递延迟,响应时间直接缩短到了 90 毫秒。体现在路面上,就是这台大车百公里刹停只需要 33 米级,并且连续高频重刹也不会出现明显的热衰减。

但纯电控刹车,大家最担心的必然是「万一断电了怎么办」。

工程师给出的答案是物理层面的「套娃」。系统内置了两路独立的电源、两颗独立的控制芯片,哪怕是遇到最极端的情况,它还留了最后一道「对角线交叉冗余」。

就算有一个对角坏了,另外一个对角可以同样制动。装 EMB 的车,只需要两个轮子(有刹车)就可以。

至于新能源车绕不开的辅助驾驶,星途 EX7 标配了基于地平线 HSD 打造的猎鹰 700 辅助驾驶系统。车顶的激光雷达和英伟达 Orin-Y 芯片,组成了核心的感知与计算中枢,支持高速和城市的领航辅助。

被动安全方面,车身使用了 88% 的高强度钢和铝合金,整车扭转刚度达到了 40000 Nm/deg。

在 20 万出头的新能源 SUV 市场,想卖好一辆车变得越来越复杂。

星途 EX7 身上保留着奇瑞一贯的固执,把大量的精力花在了底盘悬架和一套全新的电控刹车上。它其实代表了一部分传统车企的坚持,认定一台车终究还是要回归驾驶和安全本身。

带轮子的都关注,欢迎交流。 邮箱:tanjiewen@ifanr.com

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

昨天 — 2026年4月19日首页

2026 年前端工程师面试:一份来自面试官视角的真实复盘

作者 怕浪猫
2026年4月19日 23:09

前言:为什么我要写这篇文章

前两天和一个在高校和企业都面试过不少候选人的"面试官老炮"聊天,他听过太多候选人抱怨面试内容脱离实际、工作用不到。也听过面试官抱怨候选人只会背题、动手能力差。有意思的是,这两拨人的抱怨,往往都对。

今天我想换个视角——不站在候选人角度刷题,也不站在理论派角度讲八股文,而是站在有实际招聘需求、真正要带团队干活的面试官视角,聊聊 2026 年的前端工程师面试,到底在考什么、为什么这么考。

核心结论先行

2026 年的前端面试,考察维度已经发生了结构性变化:

维度 占比 变化趋势
AI 工程能力 20% 大幅上升,2024 年几乎不考
项目深度与结果 30% 持续核心,但问法变了
Coding 基本功 20% 稳定,需要证明你能写
框架原理(React 为主) 20% 稳定,但要理解本质
系统设计 10% 稳定,外企中大厂标配

一个重要变化:纯背题的通过率断崖式下跌。面试官开始问"这个方案你实际落地过吗""遇到什么问题""怎么取舍"。


一、AI 工程能力:这是 2026 年的新标配

为什么 AI 能力突然重要了?

原因很简单:团队里用 AI 的工程师,和不用的工程师,生产效率差 2-3 倍。不是 10-20%,是 2-3 倍。

任何一个正常的技术团队 Leader,只要用过了,都会想把 AI 用到团队里。所以面试 AI 能力,本质上是在判断:你能不能快速融入一个 AI-Augmented 的团队

面试怎么考?

通常分三个层次:

层次一:工具使用(基础分)

  • 你用哪些 AI 编程工具?
  • 你的日常 AI 工作流是什么?
  • 你如何保证 AI 生成代码的质量?

这三个问题几乎每场面试都会问到。如果你还在说"我就用 ChatGPT 写代码",那只能得基础分。

层次二:工程化落地(核心竞争力)

  • 你在公司里推动过 AI 工作流落地吗?
  • 团队如何统一 AI 工具配置?(Rule 文件、MCP 服务等)
  • 怎么量化 AI 提效的价值?
  • AI 生成的代码谁来 Review?流程是什么?

这部分是拉开差距的关键。很多候选人用 AI 用得很溜,但从来没想过如何让团队也用好

层次三:边界认知(加分项)

  • AI 能帮你做什么?不能帮你做什么?
  • 什么时候你选择不用 AI?
  • AI 生成的代码可能有哪些隐蔽的坑?

这部分考察的是你的判断力和工程素养。AI 不是万能的,知道它的边界在哪里,才是成熟工程师的标志。

我的 AI 工作流(可直接写在简历里的框架)

需求 → Context 构建 → AI 生成 → 质量保障 → 持续优化

1. Context 构建阶段

  • 维护项目 Rule 文件(代码规范、架构约束)
  • 配置 MCP 服务(提供项目特定上下文)
  • 沉淀 Skills(常见任务的最佳实践)
  • 持续更新 README 和 Onboarding 文档

2. AI 生成阶段

  • UI to Code:设计稿直接生成组件代码
  • 组件生成:可复用组件批量生产
  • 逻辑实现:业务逻辑 + 状态管理
  • 测试生成:单元测试 + 集成测试

3. 质量保障阶段

  • 静态检查:ESLint + TypeScript + Prettier
  • 自动测试:Jest + React Testing Library
  • CI Pipeline:自动化流水线
  • Code Review:AI 辅助 + 人工 Review 结合

4. 持续优化阶段

  • 收集 AI 生成代码的运行时反馈
  • 优化 Prompt 和上下文配置
  • 沉淀最佳实践到知识库

高质量 Prompt 的五要素

面试时经常会被问到"怎么写 Prompt",可以参考这个框架:

要素 说明 示例
目标 要实现什么功能 "帮我实现一个可复用的分页组件"
约束 技术栈、代码规范 "使用 React + TypeScript,遵循项目 ESLint 规则"
上下文 相关代码、接口定义 "已有基础的 Table 组件,路径在 src/components/Table"
输出 期望的交付物 "完整的 .tsx 组件 + 对应的单元测试"
质量要求 类型安全、错误处理、可访问性 "必须标注完整的 TypeScript 类型,处理 loading/error 状态"

二、项目深度:这是永远的压轴戏

变了什么?问法升级了

以前的项目问题:

"请介绍一下你做过的最有挑战性的项目。"

现在的问题:

"你在那个项目里遇到的最大技术挑战是什么?你尝试了几种方案?为什么最终选择了这一种?"

以前是描述题,现在是决策题

面试官不关心你做过什么,关心的是你怎么做决定

推荐的回答框架:决策链法则

每个项目准备一套"决策链":

背景 → 问题 → 约束 → 方案对比 → 最终选择 → 结果 → 复盘

背景:项目的业务背景是什么?你负责什么? 问题:核心挑战是什么?量化指标是什么? 约束:时间、技术栈、团队能力等限制条件 方案对比:你考虑了哪几种方案?各自的优劣? 最终选择:为什么选了这个?trade-off 是什么? 结果:最终的成果,用数据说话 复盘:如果重来一次,你会怎么做?

三个必杀项目类型

无论你有多少项目,建议准备这三类:

1. 性能优化项目(技术深度证明)

这是面试官的最爱,因为数据清晰、过程明确。

参考回答模板:

"我们有一个管理后台,包含 10 万条数据的表格,用户反馈滚动卡顿。

我先用 React DevTools Profiler 定位到问题是每帧渲染的行数太多,不是分页能解决的。

调研了三个方案:虚拟滚动(react-window)、分片渲染、骨架屏。最终选了虚拟滚动,因为这是唯一能满足无限滚动 + 搜索 + 排序三个需求的方案。

实现的时候遇到两个坑:动态行高滚动位置保持。行高问题通过预先测量+缓存解决,滚动位置用 key + scrollTop 记录。

结果:首屏从 3s 降到 0.3s,滚动帧率从 20fps 到 60fps,内存从 500MB 降到 50MB。"

2. 架构设计项目(系统思维证明)

可以是一次重构,也可以是新项目的架构选型。

关键不是选了什么框架,而是为什么这么选

"我们系统从 jQuery 迁移到 React,我主导了技术方案选型。

调研了三个方案:渐进式迁移(逐页替换)、微前端隔离、独立重写。

最终选了渐进式迁移,原因:

  • 独立重写风险太高,涉及 200+ 页面
  • 微前端适合多团队独立部署,我们团队就 3 个人
  • 渐进式迁移风险可控,同时能积累经验

迁移过程中设计了沙箱隔离层,让 React 和 jQuery 组件可以互相通信。

2 年时间完成了 100% 迁移,线上零事故。"

3. 失败/踩坑项目(工程成熟度证明)

面试官特别喜欢问:"你做过的项目里,有没有什么失败的经历?"

这不是送命题,这是送分题。关键是展示你如何从失败中学习。

"我曾经在一个项目里过度设计了状态管理。明明是一个简单的表单页面,我上了 Redux Toolkit。

结果:引入复杂度远超收益,团队其他成员维护成本很高。

我的复盘:状态管理方案应该由业务复杂度决定,不是技术炫技。

后来我总结了选型原则:能用 Context 就不用 Zustand,能用 Zustand 就不用 Redux。只有当团队超过 5 人、业务复杂度超过一定阈值时,才考虑引入全局状态管理库。


三、Coding 基本功:证明你能写代码

考什么?

2026 年的 Coding 环节大致分两类:

LeetCode 算法题(约 10%)

  • 高频题型:数组/字符串操作、DFS/BFS、基础动态规划
  • 难度:Medium 为主,偶尔 Easy 或 Hard
  • 时间控制:15 分钟以内,超时基本挂

手写代码(约 10%)

  • Promise 系列:Promise.all、Promise.race、Promise 并发控制
  • 数组操作:拍平、深拷贝、防抖/节流
  • 框架相关:简易版 useState、简易版 useEffect

为什么还要考算法?

这是面试官被"简历包装"坑怕了之后的保底手段。

你说你熟练使用 React,代码怎么写的?Promise 用得溜,实际能写一个 Promise.all 吗?

算法题的目的不是考你数据结构知识,而是看你在压力下思考和表达的能力。写不出来没关系,能讲清楚思路也算半个通过。

我的准备建议

  1. 刷题策略:刷 LeetCode Top 100 高频题足矣,不需要刷 500 道
  2. 手写题:一定要能讲清楚原理,不是背答案
  3. 善用 AI:用 AI 帮你理解算法思路,但一定要自己手写实现
  4. 时间管理:20 分钟内没思路,主动问面试官要提示,不丢人

四、框架原理:理解本质,而非背诵

React Fiber:必考,没有之一

关于 Fiber,我见过最离谱的候选人回答是:

"Fiber 是一个新的……React 版本。"

这是送命题。

Fiber 到底考什么?

问题层级 预期回答深度
Fiber 是什么? 一种链表结构的虚拟 DOM 描述对象
为什么要 Fiber? 解决大型应用更新时的卡顿问题,让渲染可中断
Fiber 的两个阶段? render 阶段(可中断) + commit 阶段(不可中断)
render 阶段做了什么? diff + 标记副作用(placement/update/deletion)
时间切片怎么实现? requestIdleCallback(现已被 MessageChannel 替代)
优先级调度? lanes 模型,不同更新有不同的优先级

最低要求:能说清楚 Fiber 解决了什么问题、两个阶段的区别。 加分项:能讲清楚 lanes/优先级调度的实现细节。

Hooks 原理:理解原理,而非 API

Hooks 的问题正在从"怎么用"升级到"为什么这么设计"。

问题 考察点
useState 的实现原理? 链表结构、current 指针、dispatch 闭包
为什么不能在条件语句中调用 Hook? 链表顺序对应,每次调用对应链表的一个节点
useEffect 的清理机制? 函数返回值作为清理函数,下次执行前调用
useMemo vs useCallback? 前者缓存值,后者缓存函数引用
什么时候不用 useMemo? 计算不耗时时、简单值,memo 本身的开销可能更大

性能优化:基于数据的优化

面试官最烦的答案是:

"用 React.memo 优化性能。"

面试官最喜欢的问题是:

"你在哪个场景下遇到了性能问题?怎么定位的?用的什么优化手段?效果如何?"

性能优化的正确打开方式:

Profile 定位瓶颈 → 假设原因 → 实施优化 → 数据验证效果

光优化没用,要能复现问题、定位原因、验证效果


五、系统设计:考的是权衡能力

常见题目类型

  • 设计一个支付页面
  • 设计一个实时协作编辑器
  • 设计一个图片上传和裁剪系统
  • 设计一个新闻推荐系统

答题框架:先问后画

第一步:需求澄清(必做)

"我想确认几个问题:

  1. 预期的用户规模是多少?(1 万 vs 1000 万,方案差异很大)
  2. 重点关注哪个方面?(性能、安全、可扩展性)
  3. 是纯前端系统设计还是包含后端?"

这一步做不做,差距非常大。不问就开始画的,往往答不到点子上。

第二步:从高层到细节

整体架构
    ↓
数据模型
    ↓
核心模块
    ↓
关键决策点(trade-off 讨论)

第三步:讲清楚权衡

"方案 A 的优势是 XXX,劣势是 YYY。 方案 B 的优势是 XXX,劣势是 YYY。 考虑到我们的场景是……,所以最终选了方案 B。"

面试官想听的不是"最佳方案",而是你如何权衡取舍


总结:面试的核心逻辑

2026 年的前端面试,核心考察的是四个能力层次:

层次 能力 对应的面试内容
能干活 Coding 基本功 LeetCode、手写代码
懂原理 框架深度理解 React Fiber、Hooks、性能优化
能扛事 项目落地能力 决策链、问题解决、技术选型
会协作 AI 工程能力 工作流、工具链、团队提效

这四个层次,层层递进。前两个是基础,后两个是拉开差距的关键。

一个忠告:不要把面试当成一场演技表演。真正的高手,面试时说的每一句话,都是自己真实做过的事情。与其花时间背题,不如花时间真正把项目做深、把问题想透。

面试只是开始,入职后的每一天才是真正的考验。

祝各位都能找到伯乐,也祝各位伯乐都能找到千里马。

构建无障碍组件之Window Splitter Pattern

作者 anOnion
2026年4月19日 22:02

Window Splitter Pattern 详解:构建可拖拽面板分割器

Window Splitter(窗口分割器,也称为 Resizable SplitterPane ResizerSplit PanelDivider)是一种可移动的分隔组件,用于调整两个相邻面板(pane)的相对大小。本文基于 W3C WAI-ARIA Window Splitter Pattern 规范,详解如何构建无障碍的窗口分割器组件。

一、Window Splitter 的定义与核心概念

1.1 什么是 Window Splitter

Window Splitter 是一种可移动的分隔条,位于两个面板之间,允许用户调整面板的相对大小。它具有以下特征:

  • 位于两个面板之间,作为可交互的分隔线
  • 支持拖拽调整面板大小
  • 可以是**可变(variable)固定(fixed)**类型
    • 可变分割器:可以在允许范围内调整到任意位置
    • 固定分割器:在两个固定位置之间切换
  • 具有表示**主面板(primary pane)**大小的数值

1.2 核心术语

术语 说明
Primary Pane 主面板,分割器的值表示该面板的大小
Secondary Pane 次面板,大小随主面板变化而调整
Variable Splitter 可变分割器,可在范围内任意调整
Fixed Splitter 固定分割器,只能在两个位置间切换
Value 分割器当前值,表示主面板的大小(通常为 0-100)
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  ┌──────────────────┬──────────────────────────────────────┐    │
│  │                  │                                      │    │
│  │   Primary Pane   │          Secondary Pane              │    │
│  │                  │                                      │    │
│  │  ┌────────────┐  │  ┌────────────────────────────────┐  │    │
│  │  │            │  │  │                                │  │    │
│  │  │  Content   │  │  │         Content                │  │    │
│  │  │            │  │  │                                │  │    │
│  │  └────────────┘  │  └────────────────────────────────┘  │    │
│  │                  │                                      │    │
│  └──────────────────┼──────────────────────────────────────┘    │
│                     │                                           │
│              ┌──────┴──────┐                                    │
│              │  Splitter   │  <-- draggable separator           │
│              │  (separator)│      role="separator"              │
│              └─────────────┘      aria-valuenow                 │
│                                                                 │
│  Value = 30 (Primary: 30%, Secondary: 70%)                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意:"主面板"仅表示该面板的大小由分割器控制,不表示其内容更重要。

1.3 典型应用场景

  • 代码编辑器:左侧文件树,右侧代码编辑区
  • 阅读应用:左侧目录,右侧正文内容
  • 邮件客户端:左侧邮件列表,右侧邮件详情
  • 设计工具:左侧工具栏,右侧画布

二、WAI-ARIA 角色与属性

2.1 基本角色

Window Splitter 使用 role="separator" 标记。从 ARIA 1.1 开始,当 separator 元素可聚焦时,它被视为一个控件(widget)

<div
  role="separator"
  aria-label="目录"
  aria-valuenow="30"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-controls="primary-pane"
  tabindex="0">
</div>

2.2 必需属性

属性 说明 示例值
role="separator" 标记为分隔符角色 -
aria-valuenow 当前值,表示主面板大小 "30"
aria-valuemin 最小值,主面板最小时的位置 "0"
aria-valuemax 最大值,主面板最大时的位置 "100"
aria-controls 指向主面板元素 "primary-pane"
aria-labelaria-labelledby 可访问标签,应与主面板名称匹配 "目录"

2.3 属性详解

aria-valuenow

表示分割器的当前位置,通常映射为主面板的百分比大小:

  • 0:主面板完全折叠(最小)
  • 100:主面板完全展开(最大)
  • 30:主面板占 30%,次面板占 70%
aria-controls

指向主面板元素,让辅助技术知道分割器控制哪个面板:

<div id="primary-pane" role="region" aria-label="目录">
  <!-- 主面板内容 -->
</div>

<div
  role="separator"
  aria-controls="primary-pane"
  ...>
</div>
aria-label

标签应与主面板名称匹配,帮助用户理解分割器的作用:

<!-- 好的示例 -->
<div role="region" aria-label="目录" id="toc-pane">...</div>
<div role="separator" aria-label="目录" aria-controls="toc-pane">...</div>

<!-- 不好的示例 -->
<div role="separator" aria-label="分割器">...</div>

三、键盘交互规范

3.1 基本键盘交互

按键 功能
← Left Arrow 垂直分割器向左移动
→ Right Arrow 垂直分割器向右移动
↑ Up Arrow 水平分割器向上移动
↓ Down Arrow 水平分割器向下移动
Enter 切换主面板的展开/折叠状态
Home(可选) 将分割器移到最小位置(可能完全折叠主面板)
End(可选) 将分割器移到最大位置(可能完全展开主面板)
F6(可选) 在窗口面板之间循环切换焦点

3.2 Enter 键行为详解

Enter 键用于切换主面板的折叠状态

  • 如果主面板未折叠:折叠主面板(分割器移到最小值)
  • 如果主面板已折叠:恢复分割器到之前的位置
function handleEnter(splitter) {
  const currentValue = parseInt(splitter.getAttribute('aria-valuenow'));
  const minValue = parseInt(splitter.getAttribute('aria-valuemin'));
  
  if (currentValue > minValue) {
    // 主面板未折叠,保存当前位置并折叠
    splitter.dataset.previousValue = currentValue;
    setSplitterValue(splitter, minValue);
  } else {
    // 主面板已折叠,恢复到之前的位置
    const previousValue = parseInt(splitter.dataset.previousValue || '50');
    setSplitterValue(splitter, previousValue);
  }
}

3.3 固定分割器的键盘交互

固定分割器只支持 Enter 键,不支持方向键:

  • 在两个固定位置之间切换
  • 例如:折叠/展开侧边栏

四、鼠标交互规范

4.1 拖拽行为

  • 鼠标按下:开始拖拽,记录起始位置
  • 鼠标移动:实时更新分割器位置和面板大小
  • 鼠标释放:结束拖拽,保存最终位置

4.2 视觉反馈

  • 悬停状态:鼠标悬停时显示可拖拽的视觉提示(如改变光标为 col-resizerow-resize
  • 拖拽状态:拖拽过程中显示视觉反馈(如半透明遮罩)
  • 焦点状态:键盘聚焦时显示清晰的焦点指示器
[role="separator"] {
  cursor: col-resize; /* 垂直分割器 */
}

[role="separator"][aria-orientation="horizontal"] {
  cursor: row-resize; /* 水平分割器 */
}

[role="separator"]:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

五、实现方式

5.1 基础 Window Splitter 结构

<!-- 窗口容器 -->
<div class="window-container">
  <!-- 主面板 -->
  <div
    id="primary-pane"
    class="primary-pane"
    role="region"
    aria-label="目录">
    <!-- 主面板内容 -->
    <nav>
      <h2>目录</h2>
      <ul>
        <li><a href="#ch1">第一章</a></li>
        <li><a href="#ch2">第二章</a></li>
      </ul>
    </nav>
  </div>

  <!-- 分割器 -->
  <div
    role="separator"
    class="splitter"
    aria-label="目录"
    aria-valuenow="30"
    aria-valuemin="0"
    aria-valuemax="100"
    aria-controls="primary-pane"
    tabindex="0">
  </div>

  <!-- 次面板 -->
  <div
    class="secondary-pane"
    role="region"
    aria-label="内容">
    <!-- 次面板内容 -->
    <article>
      <h1>文章标题</h1>
      <p>文章内容...</p>
    </article>
  </div>
</div>

5.2 CSS 样式

.window-container {
  display: flex;
  height: 100vh;
}

.primary-pane {
  width: 30%; /* 初始宽度对应 aria-valuenow="30" */
  min-width: 0;
  overflow: auto;
}

.splitter {
  width: 4px;
  background-color: #e5e7eb;
  cursor: col-resize;
  transition: background-color 0.2s;
}

.splitter:hover,
.splitter:focus {
  background-color: #3b82f6;
}

.splitter:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

.secondary-pane {
  flex: 1;
  overflow: auto;
}

5.3 JavaScript 实现

class WindowSplitter {
  constructor(splitterElement) {
    this.splitter = splitterElement;
    this.primaryPane = document.getElementById(
      splitterElement.getAttribute('aria-controls')
    );
    this.container = this.splitter.parentElement;
    
    this.isDragging = false;
    this.startX = 0;
    this.startWidth = 0;
    
    this.init();
  }

  init() {
    // 鼠标事件
    this.splitter.addEventListener('mousedown', this.handleMouseDown.bind(this));
    document.addEventListener('mousemove', this.handleMouseMove.bind(this));
    document.addEventListener('mouseup', this.handleMouseUp.bind(this));
    
    // 键盘事件
    this.splitter.addEventListener('keydown', this.handleKeyDown.bind(this));
  }

  handleMouseDown(e) {
    this.isDragging = true;
    this.startX = e.clientX;
    this.startWidth = this.primaryPane.offsetWidth;
    this.container.style.userSelect = 'none';
  }

  handleMouseMove(e) {
    if (!this.isDragging) return;
    
    const delta = e.clientX - this.startX;
    const newWidth = this.startWidth + delta;
    const containerWidth = this.container.offsetWidth;
    const percentage = Math.round((newWidth / containerWidth) * 100);
    
    this.setValue(percentage);
  }

  handleMouseUp() {
    this.isDragging = false;
    this.container.style.userSelect = '';
  }

  handleKeyDown(e) {
    const currentValue = parseInt(this.splitter.getAttribute('aria-valuenow'));
    const minValue = parseInt(this.splitter.getAttribute('aria-valuemin'));
    const maxValue = parseInt(this.splitter.getAttribute('aria-valuemax'));
    const step = 5; // 每次移动 5%

    switch (e.key) {
      case 'ArrowLeft':
        e.preventDefault();
        this.setValue(Math.max(minValue, currentValue - step));
        break;
      case 'ArrowRight':
        e.preventDefault();
        this.setValue(Math.min(maxValue, currentValue + step));
        break;
      case 'Home':
        e.preventDefault();
        this.setValue(minValue);
        break;
      case 'End':
        e.preventDefault();
        this.setValue(maxValue);
        break;
      case 'Enter':
        e.preventDefault();
        this.toggleCollapse();
        break;
    }
  }

  setValue(value) {
    const minValue = parseInt(this.splitter.getAttribute('aria-valuemin'));
    const maxValue = parseInt(this.splitter.getAttribute('aria-valuemax'));
    
    // 限制在范围内
    value = Math.max(minValue, Math.min(maxValue, value));
    
    // 更新 ARIA 属性
    this.splitter.setAttribute('aria-valuenow', value);
    
    // 更新视觉
    this.primaryPane.style.width = value + '%';
  }

  toggleCollapse() {
    const currentValue = parseInt(this.splitter.getAttribute('aria-valuenow'));
    const minValue = parseInt(this.splitter.getAttribute('aria-valuemin'));
    
    if (currentValue > minValue) {
      // 保存当前值并折叠
      this.splitter.dataset.previousValue = currentValue;
      this.setValue(minValue);
    } else {
      // 恢复之前的位置
      const previousValue = parseInt(this.splitter.dataset.previousValue || '30');
      this.setValue(previousValue);
    }
  }
}

// 初始化
const splitter = document.querySelector('[role="separator"]');
new WindowSplitter(splitter);

5.4 固定分割器实现

固定分割器只支持 Enter 键切换:

class FixedWindowSplitter {
  constructor(splitterElement) {
    this.splitter = splitterElement;
    this.primaryPane = document.getElementById(
      splitterElement.getAttribute('aria-controls')
    );
    
    this.positions = [0, 30]; // 两个固定位置:折叠、展开
    this.currentIndex = 1; // 默认展开
    
    this.init();
  }

  init() {
    this.splitter.addEventListener('keydown', this.handleKeyDown.bind(this));
  }

  handleKeyDown(e) {
    if (e.key === 'Enter') {
      e.preventDefault();
      this.togglePosition();
    }
  }

  togglePosition() {
    this.currentIndex = (this.currentIndex + 1) % this.positions.length;
    const value = this.positions[this.currentIndex];
    
    this.splitter.setAttribute('aria-valuenow', value);
    this.primaryPane.style.width = value + '%';
  }
}

六、最佳实践

6.1 提供清晰的标签

分割器的标签应与主面板名称匹配:

<!-- 好的示例 -->
<div role="region" aria-label="文件树" id="file-tree">...</div>
<div role="separator" aria-label="文件树" aria-controls="file-tree">...</div>

<!-- 不好的示例 -->
<div role="separator" aria-label="拖拽调整">...</div>

6.2 确保键盘可访问

  • 分割器必须可聚焦(tabindex="0"
  • 支持方向键调整位置
  • 支持 Enter 键折叠/展开

6.3 提供视觉反馈

  • 悬停时改变光标样式
  • 焦点状态清晰可见
  • 拖拽过程中实时更新面板大小

6.4 限制调整范围

设置合理的 aria-valueminaria-valuemax,防止面板过小或过大:

<!-- 主面板最小 15%,最大 50% -->
<div
  role="separator"
  aria-valuemin="15"
  aria-valuemax="50"
  ...>
</div>

6.5 保存用户偏好

记住用户调整后的面板大小,下次访问时恢复:

// 保存
localStorage.setItem('splitter-value', splitter.getAttribute('aria-valuenow'));

// 恢复
const savedValue = localStorage.getItem('splitter-value');
if (savedValue) {
  splitter.setAttribute('aria-valuenow', savedValue);
  primaryPane.style.width = savedValue + '%';
}

6.6 响应式设计考虑

在小屏幕上,考虑禁用分割器或提供替代方案:

@media (max-width: 768px) {
  [role="separator"] {
    display: none; /* 小屏幕隐藏分割器 */
  }
  
  .primary-pane {
    width: 100% !important; /* 全宽显示 */
  }
}

七、常见错误

7.1 忘记设置 aria-controls

<!-- 错误 -->
<div role="separator" aria-label="目录"></div>

<!-- 正确 -->
<div role="separator" aria-label="目录" aria-controls="primary-pane"></div>

7.2 标签与主面板不匹配

<!-- 错误 -->
<div role="region" aria-label="目录">...</div>
<div role="separator" aria-label="调整大小">...</div>

<!-- 正确 -->
<div role="region" aria-label="目录">...</div>
<div role="separator" aria-label="目录">...</div>

7.3 忽略键盘交互

只实现鼠标拖拽,不实现键盘支持,导致键盘用户无法调整面板大小。

八、总结

构建无障碍的 Window Splitter 组件需要关注:

  1. 正确的角色:使用 role="separator"
  2. 必需的属性aria-valuenow aria-valuemin aria-valuemax aria-controls aria-label
  3. 完整的键盘支持:方向键调整、Enter 键折叠、Home/End 快捷键
  4. 鼠标拖拽支持:mousedown/mousemove/mouseup 事件
  5. 清晰的标签:标签与主面板名称匹配
  6. 视觉反馈:悬停、焦点、拖拽状态的视觉提示

遵循 W3C Window Splitter Pattern 规范,我们能够创建既实用又无障碍的面板分割器,提升所有用户的操作体验。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

PDF无限制预览!Jit-Viewer V1.5.0开源文档预览神器正式发布

作者 徐小夕
2026年4月19日 21:30

下面和大家分享一下最近我们开源的文档预览SDK——Jit-Viewer,昨天刚发布 1.5.0 版本,和大家分享一下最新的功能更新。

图片

如果你是开发文档预览功能的开发者,一定经历过这种崩溃:txt文档预览乱码、PDF只能看前5页、大文件加载卡顿,代码文件预览毫无章法。

为了帮大家解决这些真实的使用痛点,提升开发体验,我们这段时间优化了 Jit-Viewer 开源文档预览SDK。上周刚帮不少开发者解决了PDF预览受限的问题——终于能完整查看所有PDF文档了。

今天,Jit-Viewer V1.5.0 正式发布,4大核心更新,让文档预览开发更高效、更省心。

文档地址:jitword.com/jit-viewer.…

开源地址:github.com/jitOffice/j…

这次更新,我们重点带来了以下功能:

1. 支持txt多编码格式预览兼容  

图片

之前很多开发者反馈,txt文档预览经常出现乱码,尤其是非UTF-8编码的文件,调试起来特别麻烦,浪费大量时间。

这次更新,我们优化了txt文档解析逻辑,全面兼容ANSI、UTF-8、GBK等多种常见编码格式,不管你导入的txt文件是什么编码,都能正常显示,再也不用手动转换编码、反复调试,帮大家节省更多开发时间。

2. 支持PDF文件完整预览,告别5页限制  

图片

这是本次更新最受期待的功能!之前版本的Jit-Viewer,PDF文件只能预览前5页,对于需要完整预览长文档的开发者来说,实用性大打折扣,很多场景下根本无法满足需求。

图片

这次我们彻底突破了这个限制,底层重构了PDF渲染能力,支持PDF文件全页完整预览,不管是几页的PDF,都能一次性加载完成,搭配原有缩放、翻页功能,完美适配各类PDF预览场景,再也不用为了查看完整PDF额外集成其他工具。

3. 优化SDK预览性能,搭载高性能文件预览引擎  

我们知道,开发者在集成文档预览SDK时,最在意的就是性能——大文件加载慢、切换页面卡顿,都会影响产品体验。这次更新,我们重新设计了文件预览引擎,优化了文件加载、渲染的全流程,大幅提升了预览速度和稳定性,即使是大文档,也能快速加载、流畅切换,不会出现卡顿、崩溃的情况,同时降低了资源占用,让你的应用运行更流畅。

4. 支持代码文件高亮预览  

针对开发类场景,我们新增了代码文件高亮预览功能。不管是Java、Python、JavaScript,还是HTML、CSS等常见编程语言,导入后都能自动识别语言类型,实现语法高亮,代码结构清晰可见,再也不用看着杂乱无章的纯文本代码发愁,尤其适合需要在应用中集成代码预览功能的开发者,大幅提升使用体验。

市面上很多商业文档预览SDK,只解决“能预览”的问题,而 Jit-Viewer 想解决的是“好用、省心、适配多场景”。

这次V1.5.0的更新,本质上是在“轻量高效”的核心定位上,进一步突破场景限制、优化使用体验——让复杂的文档预览开发,变得更简单,让不同需求的开发者,都能快速集成、高效使用,不用再为各类预览问题额外消耗精力。

简单来说,Jit-Viewer 是一个纯前端的文件预览引擎。不需要后端转换服务,不需要安装任何插件,几行代码就能让浏览器具备"专业软件"的预览能力。图片目前 jit-viewer 已经支持了:

  • docx / ppt / pdf / excel
  • csv
  • html
  • markdown
  • txt
  • 代码文件(如js,css, java, go, c#, php, ts等)
  • 音频 / 视频
  • CAD
  • 3D模型
  • OFD(国产格式)

同时我们还在持续迭代优化,帮助大家仅通过几行代码,就能让自己的web系统轻松拥有多种文档预览的能力。

github:github.com/jitOffice/j…

《SwiftUI 高级特性第1章:自定义视图》

作者 90后晨仔
2026年4月19日 19:37

Snip20260419_12.png

1.1 自定义视图概述

在 SwiftUI 中,自定义视图是构建复杂用户界面的基础。通过创建可重用的自定义视图组件,我们可以:

  • 提高代码的可维护性和可重用性
  • 封装复杂的 UI 逻辑
  • 保持主视图代码的简洁性
  • 实现统一的设计风格

1.2 自定义视图的创建方法

1.2.1 基本结构

创建自定义视图的基本步骤:

  1. 定义一个遵循 View 协议的结构体
  2. 实现 body 计算属性,返回一个视图
  3. 为视图添加必要的属性和初始化方法

1.2.2 示例代码结构

// 自定义视图结构体
struct CustomView: View {
    // 属性定义
    let title: String
    let subtitle: String
    
    // 初始化方法
    init(title: String, subtitle: String) {
        self.title = title
        self.subtitle = subtitle
    }
    
    // 视图体
    var body: some View {
        VStack {
            Text(title)
            Text(subtitle)
        }
    }
}

1.3 自定义组件示例

1.3.1 自定义按钮

功能说明:创建一个具有不同样式的自定义按钮组件。

代码实现

// 自定义按钮组件
struct CustomButton: View {
    let title: String
    let style: ButtonStyle
    let action: () -> Void
    
    // 按钮样式枚举
    enum ButtonStyle {
        case primary
        case secondary
    }
    
    // 初始化方法
    init(title: String, style: ButtonStyle = .primary, action: @escaping () -> Void) {
        self.title = title
        self.style = style
        self.action = action
    }
    
    var body: some View {
        Button(action: action) {
            Text(title)
                .padding()
                .background(style == .primary ? .blue : .gray)
                .foregroundColor(.white)
                .cornerRadius(10)
                .font(.headline)
        }
    }
}

使用示例

CustomButton(title: "点击我") {
    print("自定义按钮被点击")
}

CustomButton(title: "次要按钮", style: .secondary) {
    print("次要按钮被点击")
}

1.3.2 自定义卡片

功能说明:创建一个带有标题、副标题和图标的卡片组件。

代码实现

// 自定义卡片组件
struct CustomCard: View {
    let title: String
    let subtitle: String
    let imageName: String
    
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Image(systemName: imageName)
                    .font(.largeTitle)
                    .foregroundColor(.blue)
                Spacer()
            }
            Text(title)
                .font(.headline)
                .fontWeight(.bold)
            Text(subtitle)
                .font(.subheadline)
                .foregroundColor(.gray)
        }
        .padding()
        .background(.white)
        .cornerRadius(10)
        .shadow(radius: 5)
        .padding(.horizontal)
    }
}

使用示例

CustomCard(
    title: "欢迎使用 SwiftUI",
    subtitle: "这是一个自定义卡片视图",
    imageName: "star.fill"
)

CustomCard(
    title: "学习 SwiftUI",
    subtitle: "从基础到高级",
    imageName: "book.fill"
)

1.3.3 自定义进度条

功能说明:创建一个可自定义颜色和进度的进度条组件。

代码实现

// 自定义进度条组件
struct CustomProgressBar: View {
    let progress: Double
    let color: Color
    
    init(progress: Double, color: Color = .red) {
        self.progress = min(max(progress, 0), 1) // 确保进度在0-1之间
        self.color = color
    }
    
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // 背景
                Rectangle()
                    .fill(.gray.opacity(0.3))
                    .cornerRadius(5)
                
                // 进度条
                Rectangle()
                    .fill(color)
                    .frame(width: geometry.size.width * CGFloat(progress))
                    .cornerRadius(5)
            }
            .frame(height: 20)
        }
        .padding(.horizontal)
    }
}

使用示例

CustomProgressBar(progress: 0.3)
CustomProgressBar(progress: 0.7, color: .green)
CustomProgressBar(progress: 1.0, color: .blue)

1.3.4 自定义徽章

功能说明:创建一个可自定义颜色的徽章组件。

代码实现

// 自定义徽章组件
struct CustomBadge: View {
    let text: String
    let color: Color
    
    init(text: String, color: Color = .blue) {
        self.text = text
        self.color = color
    }
    
    var body: some View {
        Text(text)
            .padding(.horizontal, 12)
            .padding(.vertical, 6)
            .background(color)
            .foregroundColor(.white)
            .cornerRadius(15)
            .font(.subheadline)
            .fontWeight(.bold)
    }
}

使用示例

CustomBadge(text: "新")
CustomBadge(text: "热门", color: .red)
CustomBadge(text: "优惠", color: .green)

1.3.5 自定义开关

功能说明:创建一个带有动画效果的自定义开关组件。

代码实现

// 自定义开关组件
struct CustomToggle: View {
    @Binding var isOn: Bool
    
    var body: some View {
        Button(action: {
            isOn.toggle()
        }) {
            HStack {
                Text(isOn ? "开启" : "关闭")
                    .font(.headline)
                Spacer()
                RoundedRectangle(cornerRadius: 20)
                    .fill(isOn ? .green : .gray)
                    .frame(width: 50, height: 30)
                    .overlay(
                        Circle()
                            .fill(.white)
                            .frame(width: 24, height: 24)
                            .offset(x: isOn ? 10 : -10)
                            .animation(.spring(), value: isOn)
                    )
            }
            .padding()
            .background(.white)
            .cornerRadius(10)
            .shadow(radius: 2)
        }
    }
}

使用示例

@State private var isEnabled = true

CustomToggle(isOn: $isEnabled)

1.3.6 自定义列表项

功能说明:创建一个带有图标、标题和副标题的列表项组件。

代码实现

// 自定义列表项组件
struct CustomListItem: View {
    let title: String
    let subtitle: String
    let imageName: String
    
    var body: some View {
        HStack(spacing: 15) {
            Image(systemName: imageName)
                .font(.title)
                .foregroundColor(.blue)
            VStack(alignment: .leading, spacing: 5) {
                Text(title)
                    .font(.headline)
                Text(subtitle)
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
            Spacer()
            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
        }
        .padding()
        .background(.white)
        .cornerRadius(10)
        .shadow(radius: 2)
    }
}

使用示例

CustomListItem(
    title: "项目1",
    subtitle: "这是第一个项目",
    imageName: "circle.fill"
)

CustomListItem(
    title: "项目2",
    subtitle: "这是第二个项目",
    imageName: "square.fill"
)

1.4 自定义视图的最佳实践

  1. 命名规范:使用清晰、描述性的名称
  2. 参数设计:合理设计初始化参数,提供默认值
  3. 布局考虑:使用适当的布局容器和间距
  4. 可访问性:确保视图对所有用户可访问
  5. 性能优化:避免不必要的计算和重绘
  6. 文档注释:为组件添加清晰的文档注释

1.5 综合示例

功能说明:展示所有自定义组件的综合示例。

代码实现

import SwiftUI

struct CustomViewsDemo: View {
    @State private var toggleState = false
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // 标题
                Text("自定义视图")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 1. 自定义按钮
                VStack {
                    Text("1. 自定义按钮")
                        .font(.headline)
                    CustomButton(title: "主要按钮") {
                        print("主要按钮被点击")
                    }
                    CustomButton(title: "次要按钮", style: .secondary) {
                        print("次要按钮被点击")
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 2. 自定义卡片
                VStack {
                    Text("2. 自定义卡片")
                        .font(.headline)
                    CustomCard(
                        title: "SwiftUI 教程",
                        subtitle: "学习现代 UI 开发",
                        imageName: "swift"
                    )
                    CustomCard(
                        title: "高级特性",
                        subtitle: "自定义视图与动画",
                        imageName: "star.fill"
                    )
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 3. 自定义进度条
                VStack {
                    Text("3. 自定义进度条")
                        .font(.headline)
                    CustomProgressBar(progress: 0.3)
                    CustomProgressBar(progress: 0.7, color: .green)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 4. 自定义徽章
                VStack {
                    Text("4. 自定义徽章")
                        .font(.headline)
                    HStack {
                        CustomBadge(text: "新")
                        CustomBadge(text: "热门", color: .red)
                        CustomBadge(text: "推荐", color: .green)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 5. 自定义开关
                VStack {
                    Text("5. 自定义开关")
                        .font(.headline)
                    CustomToggle(isOn: $toggleState)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 6. 自定义列表项
                VStack {
                    Text("6. 自定义列表项")
                        .font(.headline)
                    CustomListItem(
                        title: "设置",
                        subtitle: "应用偏好设置",
                        imageName: "gear"
                    )
                    CustomListItem(
                        title: "个人资料",
                        subtitle: "查看和编辑个人信息",
                        imageName: "person"
                    )
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .padding()
        }
    }
}

#Preview {
    CustomViewsDemo()
}

1.6 总结

自定义视图是 SwiftUI 中非常强大的功能,通过创建可重用的组件,我们可以构建更加模块化、可维护的用户界面。在本章节中,我们学习了如何创建各种类型的自定义视图,包括按钮、卡片、进度条、徽章、开关和列表项等。

通过合理的设计和组织,自定义视图可以大大提高开发效率,同时保持代码的清晰性和可维护性。


参考资料


本内容为《SwiftUI 高级特性》第一章,欢迎关注后续更新。

联翔股份:预计2025年净亏损1080万元至1550万元 公司股票可能在年报披露后被实施退市风险警示

2026年4月19日 18:15
4月19日,联翔股份公告称,经财务部门初步测算,公司预计2025年度实现净利润为-1,080万元至-1,550万元,扣非后净利润为-1,500万元至-1,800万元;扣除与主营业务无关及不具备商业实质的收入后的营业收入为1.2亿元至1.58亿元,低于3亿元。根据相关规定,若公司2025年年报披露后的利润总额、净利润或者扣除非经常性损益后的净利润孰低者为负值且营收低于3亿元,公司股票将在年报披露后被实施退市风险警示(简称前冠以“*ST”字样)。目前年报审计工作正在进行中,具体数据以正式披露为准,敬请投资者注意投资风险。
❌
❌