普通视图

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

泰国2025年游客人次10年来首次下降

2026年1月1日 15:30
泰国旅游与体育部星期二(12月30日)说,截至12月28日,泰国共接待3260万名外国游客,比前年同期下降超过7%。这是泰国10年来除却冠病疫情期间,旅客人数首次下滑。另外,外国游客带来的旅游收入总计达1.5万亿泰铢(约600亿新元),低于前年1.67万亿泰铢。其中,马来西亚游客以450万人次居首,其次是440万人次的中国游客,排名第三的是250万人次来自印度的游客。

加拿大股市全年累涨28% 创2009年以来最大涨幅

2026年1月1日 15:15
在矿业及金融企业的带动下,标普/多伦多证交所综合指数全年累计上涨28%,创下2009年以来的最大年度涨幅。 矿业与银行股是此次上涨的核心推手,原材料分类指数涨幅近乎翻倍,金融板块涨幅也超30%。 尽管市场对银行估值及石油前景心存担忧,但部分策略师认为,在降息及贵金属行情持续走强等因素的推动下,加拿大股市指数在2026年有望进一步走高。

印度烟草股因印度对香烟征收新税而下跌

2026年1月1日 14:15
印度烟草公司股价周四下跌,此前政府对香烟征收新税,使这个全球人口最多国家的约1亿烟民购买香烟的成本更高。Gold Flake制造商、市场龙头ITC下跌4.4%,而该国万宝路分销商戈弗雷·菲利普斯印度公司(Godfrey Phillips India)下跌7.7%。ITC股价报385.25卢比,创2024年6月以来最低水平,也有望创下2022年2月以来最差单日表现。ITC是Nifty 50指数(.NSEI)中跌幅最大的个股,同时也领跌FMCG指数,该指数下跌1.6%。印度财政部周三晚间发布通知,自2月1日起对每千支香烟征收2,050–8,500卢比(约合22.82–94.60美元)的消费税,税额取决于香烟长度。吸烟相关的健康问题被视为对印度资源的一大消耗,政府已推出包括加大警示标签和定期调整税收在内的一系列措施,以遏制烟草消费。 ICICI Securities分析师表示,该税种将使75-85毫米长香烟的总体成本增加22%-28%。

上汽通用五菱2025年累计销量突破163.5万辆,同比增长6.2%

2026年1月1日 14:00
2025年,上汽通用五菱累计销量突破163.5万辆,同比增长6.2%,其中,新能源汽车累计销量首次突破百万辆,达到1000066辆,新能源渗透率从2024年底的50%提升至61%。上汽通用五菱全年海外销量超26.6万台套,连续10年创下新高,其中新能源出口累计超9.4万台套,同比增长128.6%。

海南自贸港封关以来新增经营主体近1.2万家

2026年1月1日 13:45
2025年12月31日从贯彻海南省委经济工作会议精神暨自贸港封关进展情况新闻发布会上获悉,自2025年12月18日海南自贸港正式启动全岛封关以来,在封关政策带动下,截至2025年12月28日,全省新增经营主体11957家。封关以来,截至2025年12月28日,经海南自贸港各对外开放口岸进出境旅客11.21万人次,其中免签入境旅客3.27万人次;民航进出境航空器753架次;进出境船舶180艘次。经各“二线口岸”民航进出岛航空器1139架次,进出岛船舶3848艘次,进出岛车辆16.09万辆次,其中出岛货车3.6万辆次。离岛免税购物保持增长态势,消费热潮持续升温。

淘宝闪购启动年货节,30分钟“马上到家”

2026年1月1日 13:25
淘宝闪购近日正式启动2026年货节,以“淘宝闪购买年货,30分钟送到家”为核心主张,围绕团聚、出行、礼赠、装扮玩乐四大高频场景推出年货购物解决方案。据了解,活动期间,用户搜索“年货节”进入会场,用户领券下单可享受多档满减优惠,覆盖百货、超市、酒水、生鲜等全品类商家。

绥芬河口岸出入境客流激增

2026年1月1日 13:15
元旦前夕,绥芬河口岸迎来出入境客流高峰。许多俄罗斯民众赶在元旦假期前来中国采购年货,体验中国元旦氛围。据绥芬河出入境边防检查站统计,仅12月28日至30日三天,该站验放出入境人员已超5000人次,较平日大幅增长。据悉,截至12月31日0时,该站年内已累计验放出入境人员56万余人次,车辆14万余辆次,同比增长超33%。绥芬河口岸作为中俄重要陆路口岸,正以高效便捷的通关服务,成为两国人文和旅游交流的桥梁。

库克发了张疑似 AI 生图,把反 AI 神剧《同乐者》给背刺了

作者 苏伟鸿
2026年1月1日 13:12

苹果全新大戏《同乐者》终于完结,「炸裂」的大结局,大家看了吗?

为了庆祝大结局播出和圣诞节,苹果 CEO 蒂姆 · 库克在 X 上发布了一条推文,却引发了意料之外的争议。

油腻的质感、逻辑不通的细节、毫无亮点的画面,引得网友们纷纷留言质问:

这是用 AI 生成的吗?

反 AI 电视剧,被 AI 背刺?

我相信,如果是 Netflix 高管发了一张《怪奇物语》的 AI 图,也不会造成这么大的舆论争议,尽管这部剧集比《同乐者》要火不少。

但不管是《同乐者》,还是放大到 Apple TV 平台本身,都带有强烈的「反 AI」气质。

给没看过《同乐者》的朋友们简单讲述一下剧情:一种神秘的末世病毒席卷了全世界,向世人强行灌输了乐观和满足的情绪,形成统一的「蜂巢意识」。一位悲观的畅销书作家卡罗尔发现自己免疫这种病毒,与此同时病毒群体正在试图转化她和其他免疫者。

剧集在海内外口碑都不错:MTC 斩获 87 的综合评分,IMDB 用户评分 8.1,豆瓣评分 8.3,算得上今年名列前茅的好剧。

这部剧集《绝命毒师》《风骚律师》核心主创文斯 · 吉里根打造,独特的美学风格和镜头语言和这两部经典作品一脉相承。苹果还给出了 Apple TV 剧集史上最高的预算——单集 1500 万美元起,让剧集得以充满各种实拍镜头的大场面。凭借《风骚律师》两度提名艾美奖的主演蕾亚 · 塞洪,也已经靠这部剧拿下金球奖提名。

剧中的一个情节让人印象深刻:卡罗尔和这个蜂巢意识的病毒群体进行接触后发现,这群「同乐者」愿意为她做任何事,满足她任何要求,还集合了人类的智慧和记忆。

不少观众看剧时感到莫名熟悉:这群谄媚、随时帮助、没有其他感情、充满知识的「同乐者」,不就是 ChatGPT 吗?

不过,对于这种言论,文斯 · 吉里根回应称,他其实从来没用过 AI 聊天机器人,创作《同乐者》时也没考虑到 AI。

我讨厌 AI,AI 是世界上最昂贵、最耗能的抄袭机器。

▲左: 蕾亚 · 塞洪;右:文斯 · 吉里根

吉里根的立场也直接在剧集制作中体现,片尾字幕特别标注了「本节目由人类制作」,在播出时就引发了不少讨论。

虽然苹果公司在 AI 技术道路上一往无前,但苹果和各行各业的创作者长期以来保持着密切联系,其实人们更愿意看到苹果继续重视「创作」背后的人文价值。

在《同乐者》播出期间,Apple TV 还发布了新片头和制作幕后:这个看起来像是动画渲染的片头,居然大部分是实拍的成果。

这种「全手工」到近乎有点笨拙的创作方式,也被大众解读为苹果对「创作」的尊重,赢得了不少掌声。

在主创旗帜鲜明反对 AI、剧集被当作 AI 寓言、苹果尊重创作的历史种种前提下,库克发布了一张疑似 AI 生成的图来庆祝剧集完结,自然引起了人们的不满和质疑。

所以这张图真的是 AI 生成的结果吗?苹果没有对此作出回应,有人联系了图片作者 Keith Thomson,一位现代画家,对方给出了这样的回答:

我无法对具体客户项目发表评论。一般来说,我总是手绘绘画,有时也会用到标准的数字工具。

这个似是而非的回答,完全没解决大众的困惑。在这条推文下方,以及更多社交平台上,网友们已经吵成了一锅粥。

一些专业的绘画或科技人士认为,这张图片有着人类手绘的笔触痕迹,并非 AI 出品。

Apple TV 官方账号很快也加入战局,转发推文的同时强调「这是由 Keith Thomson 用 MacBook Pro 创作」,似乎企图用「MacBook Pro」这个老牌创作工具的金字招牌,来为配图正名。

但认为是 AI 出品的网友证据更加充分:图片的牛奶盒同时标注了「全脂牛奶」和「脱脂牛奶」;盒子上的迷宫也没有解法;画面充满了灰蒙蒙的噪点,这些都是典型的 AI 生图特征。

也有其他艺术家将这张图和 Keith Thomson 的作品集进行比对,不管是风格、笔触、画面元素的处理方式,都非常不同。

一些网友也推断,苹果大概率是向这位艺术家买了张配图,结果 Keith Thomson 使用「标准的数字工具」,例如一个用自己作品集训练出来的 AI,生成一张图片再动手改了改交差。

烂图比 AI 图更值得声讨

在没有更多新信息和证据的前提下,这场「是不是 AI」的争论已经成为了一场「罗生门」——观点不同的双方各执一词,事情真相已经扑朔迷离。

著名苹果评论员 John Gruber 直接引用所谓的「奥卡姆剃刀」原则进行推论:在种种复杂的可能性中,最接近事实的往往最简单,Keith Thomson 就是用了 AI。

▲ AI 检测工具也认为这张图是 AI 出品

艺术家本人模棱两可的态度,其实也坐实了这个结论——对于大部分创作者来说,自己辛苦产出的作品被打为 AI,是绝对不可以接受的,都会第一时间跳出来反驳。

况且,争论进行到这一步,这张图究竟是不是 AI 生成,其实已经不重要了。

就质量本身而言,这张图片细节拙劣,画面粗糙,你很难承认它有什么审美上的价值——这和大部分 AI 产图一样。

▲ 右边是我使用 Nano Banana 生成主题相似的图片

AI 生成的低质量图片,和人类粗制滥造的作品,本质上真的有区别吗?本质上不都是一些质量很差、毫无美感的图片?

我们为什么会对 AI 产出嗤之以鼻?因为我们的内心都默认,人类用时间、知识、经验浇灌出来的创作,才是真正优秀的作品,AI 更多是不需要心血、量产的「垃圾」。

但怎么用 AI、用 AI 创作出什么,其实都是人决定的,人的审美、品位决定了 AI 作品的高度。

当创作者不愿意去花时间构思,也没有任何好创意,只想躺着赚快钱,AI 就成替罪羊——可惜的是,这样的创作者现在越来越多,因此我们的生活充满了 AI 生产出来的废料,让大众进一步排斥 AI 创作。

▲ 可口可乐今年的 AI 假日广告,因为效果太糟糕被吐槽

如果想要去把事情做好,那 AI 就会带来前所未有的可能性。

这个月发布的小米 17 Ultra 徕卡定制版手机,有一个独占的「徕卡一瞬」功能,可以让拍出来的图片模拟出徕卡相机 M9 的风格和质感,也就是所谓的「德味」。

实现的方式,并非单纯的滤镜和照片色彩管理,而是小米和徕卡用大量 M9 拍出的照片,训练出一个大模型,用这个 AI 把图片「修」出德味。

这些「AI 德味」的照片在文字上会存在一定的幻觉,「AI 篡改照片」也引起了一些非议,有人认为是对徕卡纪实摄影传承的背叛。

但在评测的过程中,爱范儿的编辑们都被这些色彩浓郁的照片打动了,认为这台手机确实还原出我们心中的德味,丝毫不介意它是否「AI」。

更重要的是,「德味」这种曾经只属于部分摄影爱好者的审美和创作权,被 AI 复制后,走向了更多的人。

▲ AI 还原得了德味,未必能还原文字

这几年,所谓的「AI 艺术家」也正在全球崭露头角,他们不避讳自己作品中的 AI 元素,反而利用 AI 生成那种不按常理出牌的效果,创作出风味前所未有的作品,带来了一种全新的审美。

▲ 汤海清是一位使用 AI 创作数字影像的艺术家,作品常常结合民俗和梦核元素

我们暂且不去考虑关于 AI 抄袭、量产、「没有灵魂」的争议,单就结果而言,能用 AI 产出好的作品,其实一样会受到大家的欢迎。

回过头来看苹果和 Keith Thomson 这件事,其实给全世界的企业和创作者都上了很好的一课。

即使苹果很可能真的是被 Keith Thomson 「诓骗」,买手绘图收到一张 AI 图,对于那个自诩很有「品味」的苹果来说,也不应该启用这张劣质的配图,来宣传《同乐者》这样极具审美水准的影视作品。

而对于创作者来说,如果你不想自己的作品被打为「AI 生成」,那只能把它做好,而且比越来越强的 AI 还要更好。

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

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


ListenHub完成200万美元融资,ARR 300万美元进军北美

2026年1月1日 13:00
ListenHub产品的母公司MarsWave完成了200万美元天使+轮融资。本轮由天际资本领投,小米联合创始人王川跟投。同时,MarsWave也对外公布了盈利状况:目前公司年经常性收入(ARR)已突破300万美元,并达到月度盈亏平衡,成为少数已跑通盈利模型的AI原生公司。

土耳其对华免签 机票搜索最高增3.2倍

2026年1月1日 12:45
土耳其《官方公报》发布总统令,宣布自2026年1月2日起,对持中国普通护照的公民实行免签入境政策。消息带动土耳其相关搜索猛增,去哪儿数据显示,截至1月1日上午10时,国内飞往土耳其伊斯坦布尔搜索量环比上周增3.2倍,旅游胜地安塔利亚涨1.3倍,土耳其第三大城市、爱琴海边的伊兹密尔涨2倍以上。上海、广州、北京、中国香港、成都、深圳、杭州、西安、重庆、厦门是搜索热度最高的十个出发地,环比上周均有2倍以上增长。

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

作者 借个火er
2026年1月1日 11:46

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

前言

最近在整理电脑文件时,发现了大量重复的照片和视频,占用了几十 GB 的空间。市面上的去重工具要么收费,要么功能臃肿,于是萌生了自己造一个的想法。

正好 Tauri 2.0 正式发布,相比 Electron,它有着更小的包体积和更好的性能。于是决定用 Tauri 2.0 + React + Rust 来实现这个工具。

最终成品:File Toolkit —— 一个跨平台的文件工具箱,支持文件统计、文件去重、视频截取。

GitHub: github.com/220529/file…

功能展示

📊 文件统计

递归扫描文件夹,按类型统计文件数量和大小:

  • 支持拖拽选择文件夹
  • 按文件类型分组
  • 显示占比和总大小

🔍 文件去重

这是核心功能,支持:

  • 两阶段扫描:先按文件大小筛选,再计算哈希
  • xxHash3 快速哈希:比 MD5 快 5-10 倍
  • 并行计算:充分利用多核 CPU
  • 大文件采样:只读头部 + 中间 + 尾部,避免全量读取
  • 缩略图预览:图片直接显示,视频用 FFmpeg 截帧
  • 智能选择:自动选中较新的文件,保留最早的

✂️ 视频截取

  • 快速模式:无损截取(-c copy),秒级完成
  • 精确模式:重新编码,时间精确到毫秒
  • 时间轴预览:8 帧缩略图,快速定位
  • 实时进度:精确模式显示编码进度

技术选型

为什么选 Tauri 而不是 Electron?

对比项 Electron Tauri
包体积 150MB+ 10MB+
内存占用 高(Chromium) 低(系统 WebView)
后端语言 Node.js Rust
性能 一般 优秀

对于文件处理这种 CPU 密集型任务,Rust 的性能优势非常明显。

技术栈

┌─────────────────────────────────────────────────────┐
│                    Frontend                          │
│         React 19 + TypeScript + Tailwind CSS        │
├─────────────────────────────────────────────────────┤
│                   Tauri IPC                          │
├─────────────────────────────────────────────────────┤
│                    Backend                           │
│                  Rust + Tauri 2.0                   │
│  ┌─────────────┬─────────────┬─────────────────┐   │
│  │ file_stats  │    dedup    │     video       │   │
│  │  walkdir    │  xxHash3    │    FFmpeg       │   │
│  │             │  rayon      │                 │   │
│  │             │  memmap2    │                 │   │
│  └─────────────┴─────────────┴─────────────────┘   │
└─────────────────────────────────────────────────────┘

核心实现

1. 文件去重算法

去重的核心是计算文件哈希,但如果对每个文件都完整计算哈希,效率会很低。我采用了两阶段策略:

第一阶段:按文件大小筛选

let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();

for entry in WalkDir::new(&path)
    .into_iter()
    .filter_map(|e| e.ok())
    .filter(|e| e.file_type().is_file())
{
    if let Ok(meta) = entry.metadata() {
        let size = meta.len();
        if size > 0 {
            size_map.entry(size).or_default().push(entry.path().to_string_lossy().to_string());
        }
    }
}

// 只对大小相同的文件计算哈希
let files_to_hash: Vec<_> = size_map
    .iter()
    .filter(|(_, files)| files.len() >= 2)
    .flat_map(|(size, files)| files.iter().map(move |f| (*size, f.clone())))
    .collect();

这一步可以过滤掉大部分文件,因为大小不同的文件肯定不重复。

第二阶段:并行计算哈希

use rayon::prelude::*;

let results: Vec<(String, FileInfo)> = files_to_hash
    .par_iter()  // 并行迭代
    .filter_map(|(size, file_path)| {
        let hash = calculate_fast_hash(Path::new(file_path), *size).ok()?;
        // ...
        Some((hash, file_info))
    })
    .collect();

使用 rayon 实现并行计算,充分利用多核 CPU。

2. 快速哈希算法

传统的 MD5 哈希速度较慢,我选择了 xxHash3,它是目前最快的非加密哈希算法之一:

fn calculate_fast_hash(path: &Path, size: u64) -> Result<String, String> {
    use memmap2::Mmap;
    use xxhash_rust::xxh3::Xxh3;

    const SMALL_FILE: u64 = 1024 * 1024;      // 1MB
    const THRESHOLD: u64 = 10 * 1024 * 1024;  // 10MB
    const SAMPLE_SIZE: usize = 1024 * 1024;   // 采样 1MB

    let file = File::open(path).map_err(|e| e.to_string())?;

    if size <= SMALL_FILE {
        // 小文件:内存映射,零拷贝
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let hash = xxhash_rust::xxh3::xxh3_64(&mmap);
        return Ok(format!("{:016x}", hash));
    }

    let mut hasher = Xxh3::new();

    if size <= THRESHOLD {
        // 中等文件:完整读取
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        hasher.update(&mmap);
    } else {
        // 大文件:只读头部 + 中间 + 尾部
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let len = mmap.len();

        hasher.update(&mmap[..SAMPLE_SIZE]);                    // 头部
        hasher.update(&mmap[len/2 - SAMPLE_SIZE/2..][..SAMPLE_SIZE]); // 中间
        hasher.update(&mmap[len - SAMPLE_SIZE..]);              // 尾部
        hasher.update(&size.to_le_bytes());                     // 文件大小
    }

    Ok(format!("{:016x}", hasher.digest()))
}

优化点

  • xxHash3:比 MD5 快 5-10 倍
  • memmap2:内存映射,零拷贝读取
  • 大文件采样:只读头中尾各 1MB,避免全量读取

3. 前后端通信

Tauri 使用 IPC 进行前后端通信。后端定义命令:

#[tauri::command]
pub async fn find_duplicates(app: AppHandle, path: String) -> Result<DedupResult, String> {
    // ...
}

前端调用:

import { invoke } from "@tauri-apps/api/core";

const result = await invoke<DedupResult>("find_duplicates", { path });

4. 进度反馈

长时间任务需要显示进度,Tauri 支持事件机制:

后端发送进度

use tauri::{AppHandle, Emitter};

let _ = app.emit("dedup-progress", DedupProgress {
    stage: "计算文件指纹".into(),
    current,
    total: total_to_hash,
    percent,
});

前端监听

import { listen } from "@tauri-apps/api/event";

useEffect(() => {
  const unlisten = listen<DedupProgress>("dedup-progress", (event) => {
    setProgress(event.payload);
  });
  return () => { unlisten.then((fn) => fn()); };
}, []);

5. 内嵌 FFmpeg

视频功能依赖 FFmpeg,为了让用户开箱即用,我把 FFmpeg 打包进了应用:

配置 tauri.conf.json

{
  "bundle": {
    "externalBin": [
      "binaries/ffmpeg",
      "binaries/ffprobe"
    ]
  }
}

Rust 中获取路径

fn get_ffmpeg_path(app: &AppHandle) -> PathBuf {
    app.path()
        .resource_dir()
        .ok()
        .map(|p| p.join("binaries").join("ffmpeg"))
        .filter(|p| p.exists())
        .unwrap_or_else(|| PathBuf::from("ffmpeg"))  // 回退到系统 PATH
}

踩坑记录

1. Tauri 拖拽事件是全局的

最初使用 CSS hidden 隐藏非活动 Tab 来保持状态,但发现拖拽文件时所有 Tab 都会响应。

解决方案:给每个组件传递 active 属性,只有激活的组件才监听拖拽事件。

useEffect(() => {
  if (!active) return;  // 非激活状态不监听
  
  const unlisten = listen("tauri://drag-drop", (event) => {
    if (!active) return;
    // 处理拖拽
  });
  // ...
}, [active]);

2. 并行计算进度跳动

使用 rayon 并行计算时,多线程同时更新计数器,导致进度显示不连续。

解决方案:使用原子变量 + compare_exchange 确保进度单调递增:

let progress_counter = Arc::new(AtomicUsize::new(0));
let last_reported = Arc::new(AtomicUsize::new(0));

// 在并行迭代中
let current = progress_counter.fetch_add(1, Ordering::Relaxed) + 1;
let last = last_reported.load(Ordering::Relaxed);

if current > last && (current - last >= 20 || current == total) {
    if last_reported.compare_exchange(last, current, Ordering::SeqCst, Ordering::Relaxed).is_ok() {
        // 发送进度
    }
}

3. Release 比 Debug 快很多

开发时觉得去重速度一般,打包后发现快了好几倍。

模式 说明
Debug 无优化,保留调试信息
Release LTO、内联、循环展开等优化

对于 CPU 密集型任务,Release 版本可能快 3-5 倍。

打包发布

本地打包

# 1. 下载 FFmpeg 静态版本(macOS 示例)
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
unzip /tmp/ffmpeg.zip -d src-tauri/binaries/
mv src-tauri/binaries/ffmpeg src-tauri/binaries/ffmpeg-x86_64-apple-darwin

# 2. 执行打包
pnpm tauri build

产物:src-tauri/target/release/bundle/macos/File Toolkit.app(约 62MB,含 FFmpeg)

GitHub Actions 多平台自动打包

本地打包只能生成当前平台的安装包。要支持 Windows、Linux,需要在对应平台编译。

解决方案:用 GitHub Actions,它提供 macOS、Windows、Linux 虚拟机,可以并行打包。

git push tag v0.2.0
        ↓
GitHub Actions 触发
        ↓
┌─────────────────────────────────────────────────────┐
│  同时启动 4 台虚拟机(并行执行)                       │
├─────────────┬─────────────┬────────────┬────────────┤
│  macOS VM   │  macOS VM   │ Windows VM │  Linux VM  │
│  (Intel)    │  (ARM)      │            │            │
├─────────────┼─────────────┼────────────┼────────────┤
│ 装环境      │ 装环境       │ 装环境     │ 装环境      │
│ 下载 FFmpeg │ 下载 FFmpeg │ 下载FFmpeg │ 下载FFmpeg │
│ tauri build │ tauri build │ tauri build│ tauri build│
├─────────────┼─────────────┼────────────┼────────────┤
│   .dmg.dmg.msi.deb     │
│  (x86_64)   │  (aarch64)  │   .exe.AppImage │
└─────────────┴─────────────┴────────────┴────────────┘
        ↓
   全部上传到 GitHub Release

核心配置 .github/workflows/release.yml

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: macos-latest
            target: x86_64-apple-darwin
          - platform: macos-latest
            target: aarch64-apple-darwin
          - platform: windows-latest
            target: x86_64-pc-windows-msvc
          - platform: ubuntu-22.04
            target: x86_64-unknown-linux-gnu

    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - uses: pnpm/action-setup@v4
      - uses: dtolnay/rust-toolchain@stable

      # 各平台下载对应的 FFmpeg
      - name: Download FFmpeg (macOS)
        if: matrix.platform == 'macos-latest'
        run: |
          curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
          # ...

      - name: Download FFmpeg (Windows)
        if: matrix.platform == 'windows-latest'
        shell: pwsh
        run: |
          Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/..." -OutFile ...
          # ...

      # 打包并上传到 Release
      - uses: tauri-apps/tauri-action@v0
        with:
          tagName: ${{ github.ref_name }}
          releaseDraft: true
          args: --target ${{ matrix.target }}

使用方式

git tag v0.2.0
git push origin v0.2.0
# 自动触发,完成后去 Releases 页面发布

为什么能跨平台?

  • 前端:React 编译成 HTML/CSS/JS,哪都能跑
  • 后端:Rust 在目标平台的虚拟机上编译,生成原生二进制
  • FFmpeg:每个平台下载对应的静态版本

本质是在真实目标平台上编译,GitHub 免费提供这些虚拟机。

总结

这个项目让我对 Tauri 2.0 有了更深的理解:

  1. Rust 性能确实强:文件哈希、并行计算等场景优势明显
  2. Tauri 开发体验不错:前后端分离,IPC 通信简单
  3. 包体积小:不含 FFmpeg 只有 10MB 左右
  4. 跨平台:一套代码,多端运行

如果你也想尝试 Tauri,这个项目可以作为参考。

GitHub: github.com/220529/file…

欢迎 Star ⭐️


相关文章

TypeScript 架构实践:从后端接口到 UI 渲染数据流的完整方案

2026年1月1日 11:28

介绍在 TS 中 DTO 与 VO 的思想碰撞与实践,系统性地梳理从后端接口定义前端业务消费的完整链路。

一、 为什么要分 DTO 和 VO?

在 TypeScript 开发中,直接将后端返回的 JSON 数据透传到 UI 界面是研发初期的“捷径”,但往往也是后期维护的“噩梦”。为了解决后端字段多变、命名风格不统一(如蛇形命名 vs 驼峰命名)等问题,引入 DTO(数据传输对象)VO(视图对象) 的概念至关重要。

二、 核心思想:职责分离

我们将后端接口数据在系统中的流转拆分为三个关键环节:

环节 承载载体 核心职责
入口层 DTO (Interface) 契约。严格对齐接口协议,描述后端发来的原始数据。
转换层 Mapper 函数 解耦。负责逻辑清洗、字段重命名、类型转换。
应用层 VO (Interface) 纯净。仅包含 UI 层渲染所需的属性,命名符合前端规范。

三、 实战演练:Axios 与泛型的完美结合

为了实现自动化的类型推导,我们通过泛型封装通用的响应结构。

1. 定义通用响应壳子

// 统一后端返回的 JSON 格式
export interface ApiResponse<T = any> {
  code: number;
  message: string;
  data: T; // 这里的 T 将被具体的 DTO 替换
}

2. 定义 DTO 与 VO

利用 TS 工具类型(如 Pick)提高定义效率。

// 后端原始 DTO
export interface UserDTO {
  id: number;
  user_name: string;
  avatar_url: string;
  created_at: number; // 秒级时间戳
}

// 前端业务 VO
// 挑出需要的字段,并增加/修改业务字段
export type UserVO = Pick<UserDTO, 'id' | 'avatar_url'> & {
  displayName: string;
  regDate: Date; // 转换为 JS Date 对象
};

3. Axios 请求与 Mapper 转换

将 Axios 的泛型能力与转换函数串联起来:

import axios from 'axios';

axios.interceptors.response.use((response) => {
  const res = response.data as ApiResponse;
  if (res.code !== 200) {
    // 统一弹出后端给的错误提示
    showToast(res.message);
    return Promise.reject(new Error(res.message));
  }
  return response;
});

// 1. API 层
const fetchUserApi = (id: string) => {
  // 告知 Axios:返回值的 body 是 ApiResponse 结构,其 data 属性是 UserDTO
  return axios.get<ApiResponse<UserDTO>>(`/api/user/${id}`);
};

// 2. Mapper 层:负责 DTO -> VO 的脏活累活
const toUserVO = (dto: UserDTO): UserVO => ({
  id: dto.id,
  avatar_url: dto.avatar_url,
  displayName: dto.user_name || '未知用户',
  regDate: new Date(dto.created_at * 1000)
});

// 3. Service 层:业务组装
async function getUserDetail(id: string): Promise<UserVO> {
  const { data: res } = await fetchUserApi(id);
  // res.data 此时被自动推导为 UserDTO
  return toUserVO(res.data);
}

// 4. UI 层(Vue/React):直接使用 UserVO
const [user, setUser] = useState<UserVO | null>(null);

useEffect(() => {
  getUserDetail('123').then(setUser);
}, []);

总结

这套架构本质上是在前端构建了一道 “类型防火墙”

  • 防火墙外:是不可控的后端原始数据(DTO)。
  • 防火墙内:是稳定的、符合业务习惯的干净数据(VO)。
  • 防火墙中间:是透明的转换逻辑(Mapper)。

通过这种设计,即便后端接口字段发生变更,你只需修改 DTO 定义和 Mapper 函数,UI 层的代码完全不需要改动。

❌
❌