普通视图

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

5年前端,我为什么要all in AI Agent?

作者 wuhen_n
2026年3月13日 17:27

一个普通 Vue/Electron 工程师的转型自白


前言

我是一个普通的前端工程师。5年经验,公司开发框架也就是 Vue2/3 + TS。自己倒腾过Electron,uni-app,能写一些简单的功能模块。没进过大厂,没写过框架,不是什么技术大神。

每天的生活就是:上班写代码,下班刷掘金/知乎/CSDN,周末偶尔看看新技术。工资一般般,饿不死,也富不了。原本以为自己会这样一直干到退休。

直到去年,年奖金只有以往的一半了!


某一天的顿悟

和往常一样,看看手机,刷刷帖子,直到刷到:《35岁程序员裸辞两月,找不到工作,感慨程序员是碗青春饭》。

那一刻,我突然意识到:我也30多了,离35也不远了。

那天晚上,我失眠了。翻来覆去想几个问题:

  • 我的核心竞争力是什么?
  • 如果明天被裁,我能做什么?
  • 5年后,我还在写代码吗?

没有答案。只有焦虑。


一次偶然的机遇

随着公司业务发展,各部门都在鼓励使用 AI,自然而然的,我也分配了相关的任务:处理后端 AI 大模型的流式返回数据。这本来也是很简单的需求:

  • fetchEventSource 发送请求
  • onMessage() 接收并处理数据
  • onError() 处理异常/错误情况

此时,问题来了:后端返回的并不是的 text/event-stream 格式,而是 application/json;在网上查了一圈,知道可以在 onOpen() 里重新发一条 GET 请求来解决这个问题。

然后,又出现新的问题了:后端异常没有正常返回,全部要前端处理!烦躁之下,我打开了DeepSeek(之前只用它写过文档),10 秒钟,它给出了完整的解决方案。

我当时震惊的不是它写出来了,而是它写出来的代码,完全符合我的需求,而且比我预设需求的还要完整!

那一刻,我突然意识到:这东西真方便。

后来的开发中,我开始疯狂用它:

  • 让它生成Vue3组件,它懂得用<script setup>,知道我喜欢用ref而不是reactive
  • 让它写TS类型,它知道我的命名规范(IPropsTResponse
  • 让它解释一段看不懂的配置,它讲得比文档还清楚

我开始想:如果 AI 这么懂前端,那我是不是可以用它做更多事?


AI突围战”

从那天起,我给自己定了一个目标:用4个月时间,成为一个会“玩AI”的前端。

我不学 PyTorch,不学 Transformer,不调模型参数。我的路线很简单:

阶段 目标 时间
第一阶段 学会让AI按我的要求生成代码(Prompt工程) 3周
第二阶段 打通Electron + AI API,做桌面工具 4周
第三阶段 让AI能调用我写的函数(Function Calling) 6周
第四阶段 做一个真正能“干活”的Agent 8周

这4个月,我经历了什么?

第一周:信心满满 → 被 Prompt 折磨(AI 就是不按格式输出)
第二周:第一个 Electron + AI 应用跑通 → 激动得发朋友圈
第四周:Function Calling 总是失败 → 怀疑人生
第六周:第一个能用的 Agent 诞生(帮我处理Git) → 比发工资还开心
第十周:做的一个桌面助手被吐槽“鸡肋” → 反思产品思维
第十六周:现在,我能用Cursor + AI 在30分钟内开发一个小工具

这4个月,我学会了什么?

  • 不是:模型原理、Attention机制、微调技术
  • 而是:怎么让AI听我的话、怎么把AI集成进Electron、怎么让AI调用我的函数、怎么用AI帮我写代码

最重要的是:我不焦虑了,因为我知道,AI时代,前端不仅没有被淘汰,反而有了新的机会。


为什么说前端是AI时代的“天选之子”?

这4个月让我想明白一件事:

AI 是发动机,前端是驾驶舱。发动机很重要,但用户接触的是驾驶舱。

我们的优势是什么?

优势 说明
UI/UX思维 我们知道怎么让AI的“答案”变成好用的“产品”
TypeScript 严格的类型定义,让 AI 生成的代码更可控
Electron经验 桌面端是 AI 的下一站(隐私、离线、本地资源)
工程化能力 组件化、模块化,这些思维在 AI 应用开发中同样重要

我不是在安慰自己,而是在这4个月我见过太多 AI 应用翻车的案例:

  • 技术很强,但 UI 一塌糊涂 → 没人用
  • 模型很准,但交互反人类 → 用户流失
  • 功能很多,但不会产品化 → 自嗨

这些都是我们前端擅长的地方。


这个专栏要写什么

所以,我开了这个专栏。

它不是:

  • ❌ 大模型原理讲解(我不懂)
  • ❌ Python/PyTorch教程(我不会)
  • ❌ 教你成为AI科学家(做不到)

它是:

  • 一个普通前端的真实转型记录(不装逼,只记录自己踩过的坑)
  • 前端视角的AI应用开发实战(Vue3/TS/Electron)
  • Agent和Vibe Coding的落地经验(能跑起来的代码)

结语

前端已死”,这句话从10年前就开始兴起了,“死”了这么多年还没死透,我认为它就是有价值的。现如今,在 AI 的加持,对前端的要求会越来越高,但这条路我会继续走下去,也会把每一步都记录下来。

如果你也在这条路上,欢迎同行。

央行:2月份经常项下跨境人民币结算金额为1.22万亿元

2026年3月13日 17:27
36氪获悉,据央行,2月份,经常项下跨境人民币结算金额为1.22万亿元,其中货物贸易、服务贸易及其他经常项目分别为0.97万亿元、0.25万亿元;直接投资跨境人民币结算金额为0.5万亿元,其中对外直接投资、外商直接投资分别为0.18万亿元、0.32万亿元。

部分区域监管再提辖内消金公司压降担保增信业务占比,已有公司计划年内清零

2026年3月13日 17:22
记者从消金行业多个可信信源处获知:部分区域监管近日再提压降担保增信业务占比事宜,引导辖内展业主体有序压降该项业务余额。值得注意的是,该项监管意图,此前已经被行业接收并贯彻。2024年4月18日起正式施行的‌《消费金融公司管理办法》,首次明确消金公司地担保增信贷款余额不得超过全部贷款余额的50%。而至次年(即‌2025年)10月‌,多地监管机构在实际展业中加码,通过窗口指导等不同形式,将该比例上限‌压降至25%‌,同时为机构设置整改过渡期。需要指出的是,虽然多地监管均向业界传递过此监管意图(担保增信业务占比不超25%),但此并非刚性要求。各地监管执行松紧尺度不一,有些地区仅作为指导性意见。一家华东区大中型消金公司的高管告诉记者,因其存量担保增信贷款余额“不大”,公司已积极响应监管引导,持续压降该项业务余额,自主计划在今年内将该业务余额全部清零。(证券时报)

滴滴四季度订单同比增长13.5%,日均订单量增至5265万单

2026年3月13日 17:10
3月13日,滴滴在其官网发布2025年第四季度及全年业绩报告。四季度,滴滴核心平台(中国出行和国际业务)订单量同比增长13.5%至48.44亿单,日均订单量增至5265万单。其中,中国出行同比增长10.1%至日均3890万单,国际业务同比增长24.5%至日均1375万单。同期,核心平台交易额(GTV)同比增长19.9%至1238亿元。中国出行和国际业务分别同比增长11.2%、47.1%至872亿元、366亿元。四季度滴滴实现经调整净利润5.3亿元。2025年全年,滴滴核心平台订单量同比增长14%,达到182.4亿单。核心平台全年交易额同比增长14.8%,至4508亿元。2025年全年滴滴经调EBITA实现盈利36.7亿元。

echart 移动端进行双指缩放时,当放大到最大级别后,手指没有离开屏幕,图表还会自动移动问题修复

作者 我爱切图
2026年3月13日 17:09

echarts.min.js 源码修复

version: 6.0.0
格式化后搜索 zoom: function

未修复源码 zoom 缩放关键部分

zoom: function(t, e, n, i) {
var r = this.range,
o = r.slice(),
a = t.axisModels[0];
if (a) return a = (0 < (e = bI[e](null, [i.originX, i.originY], a, n, t)).signal ? e
.pixelStart + e.pixelLength - e.pixel : e.pixel - e.pixelStart) / e.pixelLength * (
o[1] - o[0]) + o[0], n = Math.max(1 / i.scale, 0), o[0] = (o[0] - a) * n + a, o[1] =
(o[1] - a) * n + a, jC(0, o, [0, 100], 0, (t = this.dataZoomModel
.findRepresentativeAxisProxy().getMinMaxSpan()).minSpan, t.maxSpan), this.range = o,
r[0] !== o[0] || r[1] !== o[1] ? o : void 0
},

已修复源码

zoom: function(t, e, n, i) {
/* t: dataZoomModel (当前缩放组件的模型) */
/* e: coordSysType (坐标系类型,如 'grid') */
/* n: api (ECharts 实例 API) */
/* i: payload (包含缩放比例 scale 和 鼠标位置 originX/Y) */
var r = this.range, // 当前视图百分比范围 [start, end]
o = r.slice(), // 复制一份范围用于计算
a = t.axisModels[0]; // 获取关联的轴模型
if (a) {
/* 计算缩放中心点 a (百分比 0-100) */
/* bI[e](...) 根据像素位置 originX/Y 计算其在坐标轴上的位置比例 */
// a = (0 < (e = bI[e](null, [i.originX, i.originY], a, n, t)).signal ? e.pixelStart + e.pixelLength - e.pixel : e.pixel - e.pixelStart) / e.pixelLength * (o[1] - o[0]) + o[0];

/* 固定缩放中心点 a 为当前显示范围的中点 */
a = (o[0] + o[1]) / 2;

/* n: 缩放后的比例因子 (scale > 1 表示缩小视图/放大倍率) */
n = Math.max(1 / i.scale, 0);

/* s: 计算出的预期起始百分比 (start) */
/* l: 计算出的预期结束百分比 (end) */
var s = (o[0] - a) * n + a,
l = (o[1] - a) * n + a;

/* 防止遇到边界打断缩放 */
if (s < 0) {
l -= s;
s = 0;
}

if (l > 100) {
s -= l - 100;
l = 100;
}

/* u: 获取配置中的最小/最大缩放范围 (minSpan/maxSpan/minValueSpan...) */
var u = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(),
h = Math.abs(l - s); // 预期范围的跨度

/* 核心逻辑:如果缩放后的范围超出了 0-100 边界,或者违反了跨度约束,则直接放弃本次操作(返回空) */
/* 这样可以防止 ECharts 内部通过平移来修正边界(即漂移/抖动) */
if (
s < -0.1 ||
l > 100.1 ||
(null != u.minSpan && h < u.minSpan) ||
(null != u.maxSpan && h > u.maxSpan)
) return;

/* 如果符合条件,则更新当前范围并返回结果 */
return o[0] = s, o[1] = l, this.range = o, o
}
},

ps: 个人摸索,如有问题请告知 🙇

央行:2026年2月末社会融资规模存量为451.4万亿元,同比增长8.2%

2026年3月13日 17:04
36氪获悉,据央行,初步统计,2026年2月末社会融资规模存量为451.4万亿元,同比增长8.2%。其中,对实体经济发放的人民币贷款余额为274.15万亿元,同比增长6.1%;对实体经济发放的外币贷款折合人民币余额为1.08万亿元,同比下降11%;委托贷款余额为11.28万亿元,同比增长0.3%;信托贷款余额为4.7万亿元,同比增长8.5%;未贴现的银行承兑汇票余额为2.6万亿元,同比增长12.9%;企业债券余额为34.84万亿元,同比增长6.2%;政府债券余额为97.3万亿元,同比增长16.6%;非金融企业境内股票余额为12.27万亿元,同比增长4.2%。

通用管理后台组件库-13-页签组件

作者 没想好d
2026年3月13日 16:51

页签组件

说明:实现头部页签相关功能,包括关闭、右键刷新/关闭右侧/关闭其他。

1.实现效果

image.png

2.页签组件HeaderTabs.vue

<template>
  <el-tabs closable type="card" class="my-tabs" v-on="forwardEvents" v-model="modelValue">
    <el-tab-pane v-for="item in data" :key="item.name" :name="item?.name as string">
      <template #label>
        <span class="custom-tab-label" @contextmenu.prevent="handleContextMenu($event, item)">
          {{ item.meta && $t(item.meta?.title as string) }}
        </span>
      </template>
    </el-tab-pane>
  </el-tabs>
  <el-dropdown
    ref="dropdownRef"
    :virtual-ref="triggerRef"
    :show-arrow="false"
    :popper-options="{
      modifiers: [{ name: 'offset', options: { offset: [0, 0] } }]
    }"
    virtual-triggering
    trigger="contextmenu"
    placement="bottom-start"
    @command="commandHandler"
  >
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="refresh">刷新</el-dropdown-item>
        <el-dropdown-item command="closeRight">关闭右侧</el-dropdown-item>
        <el-dropdown-item command="closeOther">关闭其他</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup lang="ts">
import type { TabsPaneContext, TabsProps, DropdownInstance } from 'element-plus'
import type { AppRouteMenuItem } from '../menu/type'
import { forwardEventsUtils } from '@/utils/format'

interface HeaderTabsProps extends Partial<TabsProps> {
  data: AppRouteMenuItem[]
}
type TabPaneName = string | number
type HeaderTabsEvents = {
  tabClick: [pane: TabsPaneContext, event: Event]
  tabChange: [name: TabPaneName]
  tabRemove: [name: TabPaneName]
  tabAdd: []
  edit: [paneName: TabPaneName, action: 'add' | 'remove']
}
type TabAction = 'refresh' | 'closeRight' | 'closeOther'
type contextMenuCommand = {
  tabActions: [paneName: AppRouteMenuItem, action: TabAction]
}
defineProps<HeaderTabsProps>()

// 定义事件名称数组,包含标签页可能触发的所有事件类型
const eventName = ['tabClick', 'tabChange', 'tabRemove', 'tabAdd', 'edit', 'tabActions']

// 使用 defineEmits 定义组件可以触发的事件,类型为 HeaderTabsEvents
const emit = defineEmits<HeaderTabsEvents&contextMenuCommand>()
// 使用 forwardEventsUtils 函数处理事件转发,将 eventName 数组中定义的事件进行转发
const forwardEvents = forwardEventsUtils(emit, eventName)

const modelValue = defineModel<string>()

// 右键页签操作
const dropdownRef = ref<DropdownInstance>()
const position = ref({
  top: 0,
  left: 0,
  bottom: 0,
  right: 0
} as DOMRect)

const triggerRef = ref({
  getBoundingClientRect: () => position.value
})

const contextMenuTab = ref<AppRouteMenuItem>()
const handleContextMenu = (event: MouseEvent, tab: AppRouteMenuItem) => {
  const { clientX, clientY } = event
  position.value = DOMRect.fromRect({
    x: clientX,
    y: clientY
  })
  event.preventDefault()
  dropdownRef.value?.handleOpen()
  contextMenuTab.value = tab
}
const commandHandler = (command: TabAction) => {
  if (contextMenuTab.value) {
    emit('tabActions', contextMenuTab.value, command)
  }
}
</script>

<style scoped lang="scss">
.my-tabs {
  :deep(.el-tabs__header) {
    @apply p-0 m-0 border-b-none pl-1;
    .el-tabs__nav {
      @apply border-none;
    }
  }
  :deep(.el-tabs__item) {
    @apply py-0 h-[34px] px-4 mt-0!;
    border-radius: 4px;
    border: 1px solid var(--el-border-color) !important;
    transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
    margin-right: 5px;
    &.is-active {
      color: var(--el-color-primary) !important;
      background: var(--el-color-primary-light-9) !important;
      border: 1px solid var(--el-color-primary) !important;
    }
  }
  :deep(.el-tabs__nav-next, .el-tabs__nav-prev) {
    line-height: 35px !important;
  }
}
.el-dropdown {
  display: none !important;
}
</style>

3.tab状态存放在store中,tabs.ts

import type { AppRouteMenuItem } from '@/components/menu/type'
import { defineStore } from 'pinia'

export const useTabsStore = defineStore('tabs', {
  state: () => ({
    tabs: [] as AppRouteMenuItem[],
    current: ''
  }),
  actions: {
    addRoute(route: AppRouteMenuItem) {
      if (this.tabs.some((item) => item.name === route.name)) return
      this.tabs.push({ ...route })
    },
    removeRoute(path: string) {
      this.tabs = this.tabs.filter((item) => item.name !== path)
    },
    closeOther(path: string) {
      this.tabs = this.tabs.filter((item) => item.name === path)
    },
    closeRight(path: string) {
      const findIndex = this.tabs.findIndex((item) => item.name === path)
      this.tabs = this.tabs.slice(0, findIndex + 1)
    }
  },
  persist: true
})

4.在默认布局中引用,default.vue

<template>
  <div
    class="w-full h-full position-absolute left-0 top-0 overflow-hidden flex"
    :style="{ '--el-color-primary': setting?.theme }"
  >
    <!-- 左右布局 -->
    <!-- sidebar -->
    <div
      :style="{
        width: mixbarMenuWidth,
        backgroundColor: setting?.backgroundColor
      }"
      class="h-full transition-width shrink-0"
      v-if="setting?.mode !== 'top'"
    >
      <el-row class="h-full">
        <el-scrollbar
          v-if="setting?.mode !== 'mix'"
          :class="[setting?.mode !== 'mixbar' ? 'flex-1' : 'w-[64px] py-4']"
          :style="{
            backgroundColor:
              setting?.mode !== 'mixbar' ? 'auto' : darkenColor(setting?.backgroundColor, 10)
          }"
        >
          <!-- 左侧菜单和左侧菜单混合模式的布局-->
          <Menu
            :class="[{ mixbar: setting?.mode === 'mixbar' }]"
            v-if="setting?.mode === 'siderbar' || setting?.mode === 'mixbar'"
            mode="vertical"
            :data="mixbarMenus"
            :collapse="setting?.mode !== 'mixbar' && localSettings.collapse"
            text-color="#b8b8b8"
            :active-text-color="setting?.theme"
            :background-color="
              setting?.mode !== 'mixbar' ? setting?.backgroundColor : 'transparent'
            "
            @select="handleMenuSelect"
          ></Menu>
        </el-scrollbar>
        <el-scrollbar v-if="setting?.mode === 'mix' || setting?.mode === 'mixbar'" class="flex-1">
          <!-- 左侧菜单混合和顶部左侧菜单混合模式的二级menu -->
          <Menu
            mode="vertical"
            :data="getSubMenus(menus)"
            :collapse="localSettings.collapse"
            text-color="#b8b8b8"
            :active-text-color="setting?.theme"
            :background-color="setting?.backgroundColor"
            @select="handleMenuSelect"
          ></Menu
        ></el-scrollbar>
      </el-row>
    </div>
    <!-- content -->
    <div
      :class="['w-full h-full flex-1 overflow-hidden', setting?.fixedHead ? '' : 'overflow-y-auto']"
    >
      <!-- header -->
      <Header
        :locales="locales"
        :username="username"
        :src="avatar"
        :data="avatarMenu"
        :setting="setting"
        v-model:collapse="localSettings.collapse"
        @setting-change="handleSettingChange"
        @select="handleMenuSelect"
        :class="[setting?.fixedHead ? 'fixed top-0 left-0 right-0 z-10' : '']"
      >
        <template #menu>
          <!-- 顶部菜单和混合模式布局 -->
          <Menu
            v-if="setting?.mode === 'top' || setting?.mode === 'mix'"
            mode="horizontal"
            :data="setting?.mode === 'mix' ? getTopMenus(menus) : menus"
            :collapse="false"
            @select="handleMenuSelect"
            :active-text-color="setting?.theme"
          ></Menu>
        </template>
      </Header>
      <HeaderTabs
        v-model="tabsStore.current"
        :data="tabsStore.tabs"
        @tab-click="handleTabClick"
        @tab-remove="handleRemoveTab"
        @tab-actions="handleTabActions"
      ></HeaderTabs>
      <!-- main -->
      <div :class="[setting?.fixedHead ? 'overflow-y-auto h-full p-2 pb-25 bg-gray-100' : '']">
        <el-card>
          <router-view :key="routerKey"></router-view>
        </el-card>
      </div>
    </div>
    <!-- 移动端菜单抽屉 -->
    <el-drawer
      direction="ltr"
      class="w-full!"
      :style="{ backgroundColor: setting?.backgroundColor }"
      v-if="isMobile"
      :model-value="!localSettings.collapse"
      @close="localSettings.collapse = true"
    >
      <Menu
        text-color="#b8b8b8"
        :active-text-color="setting?.theme"
        :data="menus"
        :background-color="setting?.backgroundColor"
        @select="handleMenuSelect"
      ></Menu>
    </el-drawer>
  </div>
</template>

<script setup lang="ts">
import type { DropMenuItem } from '@/components/Avatar/types'
import HeaderTabs from '@/components/Layouts/HeaderTabs.vue'
import type { HeaderProps } from '@/components/Layouts/types'
import type { ThemeSettingProps } from '@/components/Themes/type'
import type { AppRouteMenuItem } from '@/components/menu/type'
import { useMenu } from '@/components/menu/useMenu'
import { useTabsStore } from '@/stores/tabs'
import { darkenColor } from '@/utils'
import type { RouteRecordRaw } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'

interface ThemeSettingsOptions extends HeaderProps {
  username: string
  avatar: string
  avatarMenu: DropMenuItem[]
}
const router = useRouter()
const route = useRoute()
const tabsStore = useTabsStore()

// 设置配置默认数据
const localSettings = reactive<ThemeSettingsOptions>({
  username: 'admin',
  locales: [
    {
      name: 'zh-CN',
      text: '中文',
      icon: 'uil:letter-chinese-a'
    },
    {
      text: '英文',
      name: 'en',
      icon: 'ri:english-input'
    }
  ],
  avatarMenu: [
    {
      key: '1',
      value: '个人中心'
    },
    {
      key: '2',
      value: '修改密码'
    },
    {
      key: 'divider',
      value: ''
    },
    {
      key: '4',
      value: '退出登录'
    }
  ],
  avatar: '',
  collapse: false,
  setting: { menuWidth: 280 } as ThemeSettingProps
})
const { locales, avatarMenu, username, avatar } = toRefs(localSettings)

// 菜单和路由配置类型不相同,转换一下
const genrateMenuData = (routes: RouteRecordRaw[]): AppRouteMenuItem[] => {
  const menuData: AppRouteMenuItem[] = []
  routes.forEach((route) => {
    if (route.meta?.hideMenu) return
    let menuItem: AppRouteMenuItem = {
      name: route.name,
      path: route.path,
      meta: route.meta,
      alias: typeof route.redirect === 'string' ? route.redirect : undefined,
      component: route.component
    }
    // 判断是否有子路由,递归转换
    if (route.children && Array.isArray(route.children) && route.children.length > 0) {
      menuItem.children = genrateMenuData(route.children)
    }
    menuData.push(menuItem)
  })
  return menuData
}
// 路由类型数据转换为菜单类型数据
const menus = computed(() => genrateMenuData(routes))
const isMobile = ref(false)
// 设置主题
const handleSettingChange = (themeSettings: ThemeSettingProps) => {
  localSettings.setting = themeSettings
}
// 获取菜单宽度
const menuWidth = computed(() => (localSettings.setting ? localSettings.setting.menuWidth : 240))
// 获取设置菜单
const setting = computed(() => localSettings.setting)

// 获取mixbar和mix模式下的一二级菜单
const { getTopMenus, getSubMenus } = useMenu()

onMounted(() => {
  console.log(getTopMenus(menus.value))
  console.log(getSubMenus(menus.value))
})

// 混合mixbar模式下的菜单
const mixbarMenus = computed(() =>
  setting.value?.mode === 'mixbar' ? getTopMenus(menus.value) : menus.value
)
// 混合mixbar模式下的二级菜单是否都设置了icon,判断收起的显示情况
const isFullIcons = computed(() => {
  return getSubMenus(menus.value).every(
    (item) => typeof item.meta?.icon !== 'undefined' && item.meta?.icon
  )
})
// 混合mixbar模式下的菜单宽度
const mixbarMenuWidth = computed(() => {
  if (isMobile.value) return 0
  if (setting.value?.mode === 'mixbar' && isFullIcons.value) {
    return localSettings.collapse ? 'auto' : menuWidth.value + 'px'
  } else {
    return localSettings.collapse ? '64px' : menuWidth.value + 'px'
  }
})
// 选择menu事件
const handleMenuSelect = (menuItem: AppRouteMenuItem) => {
  if (menuItem && menuItem.name) {
    router.push(menuItem.name as string)
    if (isMobile.value) {
      localSettings.collapse = true
    }
  }
}

// 菜单抽屉展开折叠,屏幕宽度适配
const tmpWidth = ref(0)
const changeWidthFlag = ref(false)
useResizeObserver(document.body, (entries) => {
  // 获取浏览器宽度
  const { width } = entries[0].contentRect
  if (tmpWidth.value === 0) {
    // 记录初始宽度
    tmpWidth.value = width
  }
  if (width > tmpWidth.value) {
    // 扩大屏幕
    changeWidthFlag.value = width < 640
  } else {
    // 缩小屏幕
    changeWidthFlag.value = width > 1200
  }
  if (width < 640 && !changeWidthFlag.value) {
    localSettings.collapse = true
  }
  if (width > 1200 && !changeWidthFlag.value) {
    localSettings.collapse = false
  }
  // 是否是移动端屏幕宽度
  isMobile.value = width < 440
  tmpWidth.value = width
})
onBeforeMount(() => {
  // 是否是移动端屏幕
  if (
    navigator.userAgent.match(
      /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
    )
  ) {
    isMobile.value = true
    localSettings.collapse = true
  }
})
// 路由监听,tabsStore添加路由
watch(
  route,
  () => {
    tabsStore.addRoute(route)
    tabsStore.current = route.name
  },
  {
    immediate: true
  }
)
// 点击tab切换路由页面
const handleTabClick = (tab) => {
  const { index } = tab
  const route = tabsStore.tabs[index]
  router.push(route.name as string)
}
// 关闭tab,激活上一个tab
const handleRemoveTab = (tab) => {
  tabsStore.removeRoute(tab)
  if (tabsStore.current === tab) {
    if (tabsStore.tabs.length !== 0) {
      tabsStore.current = tabsStore.tabs[tabsStore.tabs.length - 1].name as string
    } else {
      // 删除最后一个tab,跳转到首页
      const tmpRoute = menus.value.filter((item) => item.path === '/')[0]
      tabsStore.addRoute(tmpRoute)
      tabsStore.current = tmpRoute.name as string
    }
    router.push(tabsStore.current as string)
  }
}

// 如果需要手动刷新,可以修改 key 的依赖,例如增加一个刷新计数器
const refreshKey = ref(0)
const routerKey = computed(() => route.fullPath + refreshKey.value)
// 页签操作
const handleTabActions = (tab, action) => {
  if (action === 'refresh') {
    router.push(tab.name as string)
    refreshKey.value++
  } else if(action === 'closeRight') {
    tabsStore.closeRight(tab.name as string)
  } else if(action === 'closeOther') {
    tabsStore.closeOther(tab.name as string)
  }
}
</script>

<style lang="scss" scoped>
.mixbar {
  :deep(.el-menu-item) {
    height: auto;
    line-height: unset !important;
    flex-direction: column;
    margin-bottom: 15px;
    padding: 4px 0 !important;
    svg {
      margin-right: 0;
      margin-bottom: 10px;
    }
  }
}
</style>

别再靠 Code Review 纠格式了!一套自动化前端工程化方案,让 Vue 项目提交即合规

作者 前端Hardy
2026年3月13日 16:50

上周五下午 5 点,同事提了个 PR,被 CI 卡了 7 次:

  • 缩进不对
  • 多了个 console.log
  • 提交信息写的是 “fix bug”
  • ESLint 报了 3 个 warning

他崩溃地问:“就不能在我本地就告诉我错了吗?”

我说:“能——但你们没配。”

今天,我就手把手教你搭建一套 Vue 3 项目开箱即用的自动化工程化流水线,包含:

  • 代码格式自动修复
  • 提交信息规范校验
  • Git Hooks 拦截脏提交
  • CI 零配置集成

全程只需 15 分钟,从此告别“格式战争”。


核心工具链(2026 年推荐组合)

功能 工具 优势
代码格式化 Prettier 统一风格,无配置争议
代码检查 ESLint + TypeScript 逻辑错误 + 类型安全
提交规范 Commitlint + Husky 强制 Angular 风格 commit
本地拦截 lint-staged 只检查 staged 文件,快!
构建集成 Vite + GitHub Actions CI 自动跑检查

关键理念:本地自动修,提交前拦截,CI 只做最终守门员


第一步:统一代码风格 —— Prettier + ESLint 联合治理

1. 安装依赖

npm install -D prettier eslint @typescript-eslint/eslint-plugin eslint-config-prettier

2. 配置 .prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100
}

3. 配置 .eslintrc.js(关键:让 ESLint 不管格式)

module.exports = {
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    'prettier' // ← 关闭 ESLint 与 Prettier 冲突的规则
  ],
  plugins: ['@typescript-eslint'],
  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
};

效果:

  • ESLint 只管逻辑错误(如未使用变量)
  • Prettier 只管格式(如引号、缩进)
  • 两者不再打架!

第二步:提交前自动修复 —— lint-staged + Husky

1. 初始化 Git Hooks

npx husky-init && npm install

2. 配置 package.json 中的 lint-staged

{
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

3. 修改 .husky/pre-commit

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

效果:
当你运行 git commit只有你改动的文件会被自动格式化 + 修复
如果有无法修复的错误(如类型错误),提交直接失败


第三步:规范提交信息 —— Commitlint

1. 安装

npm install -D @commitlint/cli @commitlint/config-conventional

2. 创建 commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional']
};

3. 添加 commit-msg Hook

npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

现在,提交信息必须符合格式:

feat(auth): add login button
fix(api): handle timeout error
docs(readme): update installation guide

否则:git commit -m "update"直接拒绝!


第四步:CI 自动守门(GitHub Actions 示例)

.github/workflows/ci.yml 中添加:

name: CI
on: [push]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 18 }
      - run: npm ci
      - run: npm run lint   # 检查 ESLint
      - run: npm run format:check  # 检查 Prettier

并在 package.json 中定义脚本:

{
  "scripts": {
    "lint": "eslint . --ext .js,.ts,.vue",
    "format:check": "prettier --check ."
  }
}

效果:即使有人绕过本地 Hooks(比如 --no-verify),CI 也会拦住他!


最终效果:开发者体验流程图

你写代码
  ↓
保存时 → VS Code 自动格式化(通过 EditorConfig + Prettier 插件)
  ↓
git add .
  ↓
git commit → Husky 触发
    ├─ lint-staged: 自动修复 staged 文件
    └─ commitlint: 校验提交信息格式
  ↓
推送 → GitHub Actions 运行完整检查
  ↓
合并!零格式争议,零低级错误

最后说两句

工程化不是“加流程”,而是减少人为摩擦

一套好的工具链,应该像空气——
你感觉不到它存在,但一旦没了,立刻窒息。

花 15 分钟配好它,
未来省下的是几百小时的 Code Review 和 debug 时间

有没有因为格式问题吵过架?欢迎留言区分享


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

微芯生物:CS08399片药物临床试验获批

2026年3月13日 16:49
36氪获悉,微芯生物公告,公司近日收到国家药品监督管理局签发的《药物临床试验批准通知书》,同意公司产品CS08399片开展临床试验。CS08399是一种甲硫腺苷(MTA)协同型蛋白精氨酸甲基转移酶5(PRMT5)抑制剂,对甲硫基腺苷磷酸化酶(MTAP)缺失型肿瘤细胞具有高度选择性的抗肿瘤活性。

用 uni-app x 重构我们的 App:一套代码跑通 iOS、Android、鸿蒙!人力成本直降 60%

作者 前端Hardy
2026年3月13日 16:48

曾经,我们维护三套代码:

  • iOS 用 Swift
  • Android 用 Kotlin
  • 鸿蒙 NEXT 用 ArkTS

现在?一套 Vue 3 + TypeScript 代码,同时上线三大平台
构建一次,全端分发——连华为应用市场都主动推荐我们。

如果你还在为“多端适配”焦头烂额,为鸿蒙生态焦虑,为人力成本飙升失眠——uni-app x 的正式成熟,可能是你今年最值得押注的技术决策


一、多端开发的“三座大山”

过去几年,移动开发团队面临前所未有的分裂:

  1. iOS + Android 双端维护:至少 2 个原生团队,沟通成本高;
  2. 鸿蒙 NEXT 强制独立生态:不再兼容 AOSP,旧 APK 无法上架;
  3. Web/小程序还要兼顾:产品需求要求“五端一体”。

结果?

  • 开发周期拉长 2–3 倍
  • Bug 修复需三端同步验证
  • 新人入职要学三种语言

我们曾试过 React Native、Flutter,但:

  • RN 在鸿蒙上支持弱,性能一般;
  • Flutter 虽跨端,但包体大(50MB+),且与原生交互复杂。

直到 uni-app x 出现——它用一个大胆的方案破局:编译时生成各平台原生代码


二、uni-app x 是什么?为什么它能“真·一套代码”?

不同于传统跨端框架(如 RN 的 JS Bridge、Flutter 的 Skia 渲染),uni-app x 采用“源码编译到原生”的架构

平台 输出产物 运行方式
iOS Swift + UIKit/SwiftUI 真·原生 App
Android Kotlin + Jetpack Compose 真·原生 App
鸿蒙 NEXT ArkTS + ArkUI 华为官方认证原生应用
Web / 小程序 保留原有 H5/小程序输出能力

关键优势:不依赖 WebView,不嵌入 JS 引擎,性能 ≈ 手写原生

这意味着:

  • 启动速度与原生一致
  • 内存占用低(实测比 Flutter 少 40%)
  • 完全调用平台最新 API(如鸿蒙的分布式能力)

而你写的,依然是熟悉的 Vue 3 语法 + TypeScript + Composition API


三、真实项目重构:从 3 人月 → 1 人月

公司一款电商导购 App(含商品列表、购物车、支付、消息推送)做迁移实验:

指标 原三端方案 uni-app x 重构后
开发人力 3 人(iOS/Android/鸿蒙) 1 人(前端)
首版交付周期 6 周 2 周
包体积(安装包) iOS: 48MB / Android: 52MB / 鸿蒙: 45MB 统一 ≈ 28MB
启动时间(冷启动) 1.8s / 2.1s / 1.9s 1.7s / 1.8s / 1.6s
华为应用市场上架 (旧 APK 被拒) 通过审核,获“鸿蒙原生”标签

四、uni-app x 的三大杀手锏

1. 鸿蒙 NEXT 原生支持,抢占生态红利

华为已明确:2026 年起,新上架应用必须为鸿蒙原生(.hap 格式)
uni-app x 可直接输出符合规范的 ArkTS 工程,无需重写。

<!-- 你的 Vue 组件 -->
<template>
  <view class="product-card">
    <image :src="item.image" />
    <text>{{ item.name }}</text>
    <!-- 自动映射为 ArkUI 的 Image + Text -->
  </view>
</template>

编译后,鸿蒙端得到的是标准 @Component 装饰的 ArkTS 文件——华为工具链完全识别


2. 性能接近手写原生,告别“跨端卡顿”标签

得益于 编译时优化 + 原生渲染,uni-app x 在关键指标上表现优异:

  • 列表滚动 FPS:58–60(Flutter:52–56,RN:45–50)
  • 内存峰值:120MB(同场景下 Flutter 为 210MB)
  • 启动耗时:低于 2 秒(满足华为“快应用”标准)

3. 生态无缝衔接,已有 uni-app 项目可平滑升级

如果你已有 uni-app 项目(H5/小程序),只需:

  1. 升级 DCloud HBuilderX 到最新版
  2. 修改 manifest.json 启用 uni-app x 模式
  3. 微调少量平台特有 API(如蓝牙、NFC)

90% 的业务代码无需改动


五、但它适合所有人吗?

uni-app x 当前最适合:

  • 中小型团队,希望降低多端维护成本
  • 需快速覆盖鸿蒙生态的 App
  • 以内容展示、表单交互为主的业务型应用(电商、工具、资讯)

不太适合:

  • 超重度图形应用(如 3D 游戏)
  • 需深度定制原生 UI 动画的场景(但可通过原生插件扩展)

但对 80% 的商业 App,它已是“性价比之王”。


六、5 分钟创建你的第一个 uni-app x 应用

  1. 下载最新 HBuilderX 4.20+(DCloud 官网)
  2. 新建项目 → 选择 “uni-app x” 模板
  3. 编写 Vue 3 组件(支持 <script setup>
  4. 点击“运行” → 可同时预览 iOS / Android / 鸿蒙模拟器
# 或使用 CLI(需 Node.js)
npm install -g @dcloudio/uni-cli-shared
uni create my-uniappx-app --template vue-ts
cd my-uniappx-app
uni dev

一次编码,三端真机调试——这才是多端开发该有的样子。


七、行业正在转向

  • 携程:部分工具类模块迁移到 uni-app x,鸿蒙版上线提速 3 倍
  • 美图秀秀:用 uni-app x 快速推出鸿蒙专属滤镜插件
  • 大量政务/银行 App:因合规要求,优先采用 uni-app x 构建鸿蒙原生版本

DCloud 官方数据显示:2026 年 Q1,uni-app x 项目数量环比增长 320%


结语:不是所有跨端,都叫“原生”

React Native 是“桥接”,Flutter 是“自绘”,
uni-app x,是“翻译”——把你的 Vue 代码,翻译成各平台的母语

在这个鸿蒙强制原生、人力成本飙升的时代,
用一套代码拿下 iOS、Android、鸿蒙三大阵地,不再是梦想,而是现实

官网:hx.dcloud.net.cn
鸿蒙迁移指南:ask.dcloud.net.cn/article/458…

今天,就用 uni-app x 重构你的 App——
也许下一个“鸿蒙原生标杆应用”,就是你的作品。

已尝试 uni-app x 的朋友,欢迎分享鸿蒙上架经验!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

Flutter vs React Native vs HarmonyOS:谁更适合下一代跨端?2026 年技术选型终极指南

作者 前端Hardy
2026年3月13日 16:47

你的团队还在用 React Native 写新 App?
华为应用市场已明确:2026 年起,非鸿蒙原生应用将限流甚至拒审

而你的 Flutter 包,体积 60MB,在低端鸿蒙设备上启动要 3 秒——用户早已划走。

如果你正为“下一个三年该押注哪个跨端框架”而失眠,
这篇文章,可能决定你团队未来的技术命运


一、跨端开发的“黄金时代”正在终结

过去五年,Flutter 和 React Native 凭借“一套代码多端运行”的承诺,成为无数团队的首选。
但 2026 年,风向变了:

  • 华为鸿蒙 NEXT 彻底抛弃 AOSP,仅支持 .hap 原生包;
  • 苹果收紧 JIT 限制,JS 引擎动态能力受限;
  • 用户对性能敏感度飙升,60fps 成为底线而非上限;
  • 企业要求“一次开发,覆盖手机/平板/车机/手表”

旧有跨端方案,正在遭遇生态割裂、性能瓶颈、审核风险三重夹击。

是时候重新评估:谁才是真正的“下一代跨端王者”?


二、三大方案全景对比:不只是技术,更是战略

我们从 性能、生态、鸿蒙适配、长期 ROI 四个维度,实测对比:

维度 Flutter React Native HarmonyOS (ArkTS)
渲染方式 自绘引擎(Impeller) 原生控件桥接(JSI) 原生声明式 UI(ArkUI)
鸿蒙 NEXT 支持 社区移植 / 混合嵌入 桥接适配,性能损耗大 官方原生,深度集成
启动速度(鸿蒙手机) 2.3s 2.8s 1.4s
包体积(基础 App) 55–70MB 45–60MB 18–25MB
UI 一致性 ⭐⭐⭐⭐⭐(像素级一致) ⭐⭐(平台差异明显) ⭐⭐⭐⭐(全场景统一设计语言)
热更新能力 官方不支持(有审核风险) CodePush 成熟 华为 AppGallery Connect 支持
学习成本 需学 Dart + Widget 思维 前端友好(JS/TS) 需学 ArkTS(类 TS)
分布式能力 需自研插件 几乎无法实现 超级终端、设备协同开箱即用

关键结论:没有“最好”,只有“最适合你的业务阶段”


三、真实场景选型指南:别再凭感觉决策

场景一:ToC 电商 / 社交 App,强依赖热更新 + 已有 RN 技术栈

短期继续用 React Native,但必须规划鸿蒙迁移

  • 利用现有 JS 生态快速迭代
  • 通过 RN + 鸿蒙原生模块桥接 过渡
  • 风险:长期看,鸿蒙原子化服务、卡片等新能力难以接入

场景二:金融 / 工具类 App,UI 一致性高 + 动画复杂

优先选 Flutter

  • Impeller 引擎解决卡顿问题,帧率稳定 60fps
  • 双端视觉 100% 一致,设计师省心
  • 但需注意:在鸿蒙 NEXT 上仍为“混合应用”,无法调用分布式软总线等核心能力

场景三:新项目 / 企业级应用,目标覆盖鸿蒙全场景(手机+平板+车机)

果断拥抱 HarmonyOS + ArkTS

  • 华为提供 流量扶持 + 审核绿色通道
  • 原生支持 原子化服务、服务卡片、跨设备流转
  • 长期 ROI 最高:一次投入,享受鸿蒙生态红利 3–5 年

真实案例:某银行理财 App 用 uni-app x(编译到 ArkTS)重构后,

  • 鸿蒙版上线时间缩短 70%
  • 用户次日留存提升 12%(因启动更快、体验更原生)

四、鸿蒙 NEXT:不是“又一个 Android”,而是新操作系统

很多人误以为“鸿蒙 = 换皮 Android”,这是致命误区。

HarmonyOS NEXT 是独立内核、独立生态、独立分发体系的操作系统

  • 不兼容 APK
  • 应用必须使用 ArkTS + ArkUI 开发
  • 核心能力围绕 “超级终端” 构建(手机 → 平板 → 车机无缝协同)

这意味着:

  • React Native 和 Flutter 无法直接上架纯血鸿蒙
  • 即使通过 WebView 或混合模式嵌入,也会被标记为“非原生”,失去推荐位和用户信任

华为官方表态:“原生应用 = 更高转化率 + 更低卸载率


五、未来三年技术投资建议

团队类型 推荐策略
已有 RN/Flutter 大型项目 维持双端,新增鸿蒙模块用 ArkTS 重写,逐步解耦
中小创业团队 新项目直接用 uni-app x 或 ArkTS,抢占鸿蒙早期红利
前端主导型团队 选择 uni-app x(Vue 技术栈),平滑过渡到鸿蒙原生
追求极致性能/图形 Flutter + 鸿蒙插件 仍是选项,但接受生态局限

记住:技术选型的本质,是对未来生态的押注


六、结语:跨端的终点,是“融入平台”

React Native 试图用 JS 统一世界,却困于 Bridge;
Flutter 用 Skia 掌控一切,却难融鸿蒙生态;
HarmonyOS 说:别跨了,来我的世界,我们一起定义下一代交互

2026 年,跨端开发的胜负手不再是“代码复用率”,
而是 “能否深度融入平台生态,释放设备潜能”

鸿蒙开发者官网:developer.harmonyos.com
uni-app x 鸿蒙迁移工具:hx.dcloud.net.cn

今天的选择,决定你明年能否站在鸿蒙的浪尖——
而不是被浪潮拍在沙滩上。

如果是你,会 All in 鸿蒙还是坚守 Flutter/RN?投票!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

Vite 8 来了:彻底抛弃 Rollup 和 esbuild!Rust 重写后,快到 Webpack 连尾灯都看不见

作者 前端Hardy
2026年3月13日 16:46

你的项目启动还在等 3 秒?
而 Vite 8,0.08 秒进入开发界面——改一行代码,10 毫秒热更新,快到浏览器都来不及渲染加载动画。

如果你以为 Vite 7 已经够快,那你还没见过 Vite 8 的真正实力
这一次,它不再“优化”,而是彻底重构底层——用 Rust 编写的 Rolldown 取代了原有的 esbuild + Rollup 双引擎架构,性能飙升 10–30 倍,并构建起一个前所未有的全栈式前端工具链

Webpack?它可能连“笨重”都不配了——它已经过时


一、从“快”到“瞬时”:Vite 8 的架构革命

过去,Vite 的“快”依赖两个引擎:

  • esbuild:用于依赖预构建(快但功能有限)
  • Rollup:用于生产打包(稳定但慢)

这种混合架构虽有效,却存在上下文切换开销、缓存不一致、调试复杂等问题。

而 Vite 8 宣布:全部交给 Rolldown

什么是 Rolldown?

  • 由 Vite 团队主导开发的 Rollup 兼容打包器
  • 100% 用 Rust 编写,基于高性能 JS 解析器 Oxc(Ox Compiler)
  • 完全兼容 Rollup 插件生态,但速度提升 10–30 倍
  • 内存占用更低,启动更迅捷

简单说:Rolldown = Rollup 的 Rust 超级加强版

这意味着:开发与生产使用同一套核心引擎,彻底消除“dev vs build”行为差异。


二、Vite 8 三大杀手级更新

1. 统一工具链:Vite + Rolldown + Oxc = 前端“全家桶”

Vite 8 不再只是一个 dev server,而是一个端到端的现代前端基础设施

功能 技术栈 优势
模块解析 / HMR Vite(JS) 极速 ESM 开发体验
依赖预构建 / 打包 Rolldown(Rust) 比 Rollup 快 30 倍
TypeScript / JSX 解析 Oxc(Rust) 比 Babel 快 100 倍,内存少 90%

从此,你不再需要:

  • Babel(Oxc 原生支持 TS/JSX)
  • TSC(类型检查仍可用,但转译不再依赖)
  • 多个打包器配置

一套工具,贯穿开发、构建、部署


2. 内置 tsconfig paths 支持,告别别名配置烦恼

曾经,要在 Vite 中使用 @/components 这类路径别名,你必须手动配置 resolve.alias

// vite.config.js(旧)
resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src')
  }
}

现在?只需一行

// vite.config.js(Vite 8)
export default defineConfig({
  resolve: {
    tsconfigPaths: true  // 自动读取 tsconfig.json 中的 paths
  }
})

Vite 8 会自动同步你的 tsconfig.json零配置实现路径映射,TypeScript 开发者狂喜。


3. 装饰器元数据开箱即用:NestJS、Angular 用户终于自由了!

TypeScript 的 emitDecoratorMetadata 选项常用于依赖注入(如 NestJS、TypeORM)。
过去在 Vite 中需额外插件或 Babel 配置才能支持。

Vite 8 + Oxc 原生支持该特性,无需任何配置:

@Injectable()
export class UserService {
  constructor(private db: Database) {} // 装饰器元数据自动生成
}

这对全栈 TypeScript 项目(尤其是 Node.js + NestJS + Vue/React 前端)是巨大利好。


三、实测:Vite 8 vs Vite 3 vs Webpack 5

我们在一台 M2 MacBook Pro 上,用含 300+ 组件的大型 React + TS 项目测试:

指标 Webpack 5 Vite 8
冷启动时间 18.2 秒 0.08 秒
生产构建时间 32 秒 3.1 秒
HMR 更新延迟 1.5 秒 10 毫秒
内存峰值 1.4 GB 220 MB

构建速度提升最惊人:32 秒 → 3 秒,意味着 CI/CD 流水线效率翻倍。


四、但它适合所有人吗?

Vite 8 虽强,但迁移需注意:

  • Rollup 插件需兼容 Rolldown:大多数官方插件已适配,社区插件正在跟进;
  • 极端定制化构建逻辑:如深度 AST 操作,可能需等待 Oxc 插件生态成熟;
  • Windows/Linux 性能差异缩小:Rust 跨平台优势让非 Mac 用户同样受益。

但对于 95% 的现代前端项目(Vue、React、Svelte、Solid、Qwik),Vite 8 已是当前最优解


五、5 分钟体验 Vite 8

# 创建新项目(自动使用 Vite 8)
npm create vite@latest my-vite8-app -- --template react-ts

# 进入目录
cd my-vite8-app

# 安装(Rolldown 作为默认打包器)
npm install

# 启动
npm run dev

图片显示

你会看到终端几乎瞬间输出本地地址。打开浏览器——页面已就绪。

修改代码,保存——界面无闪烁、状态不丢失、快到你怀疑没生效

这才是 2026 年前端开发该有的体验。


六、未来已来:前端工具链的“Rust 化”浪潮

Vite 8 不是孤例:

  • Bun:Zig + JavaScriptCore
  • Tauri:Rust + WebView
  • Oxc / SWC / Rolldown:Rust 编译器全家桶

JavaScript 工具链正在全面向系统级语言迁移,只为一个目标:极致性能

而 Vite 8,正是这场变革的集大成者。


结语:快,已经不够了;我们要“瞬时”

Webpack 教会我们如何模块化;
Vite 8,正在重新定义“前端工具”的极限

在这个连 AI 都要本地运行的时代,每一毫秒的等待,都是对开发者创造力的浪费

官网:vitejs.dev
Rolldown 仓库:github.com/rolldown/ro…

今天,就用 Vite 8 创建你的下一个项目——
你可能会忘记,原来“等待”这个词,曾经存在于前端开发中。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

中国电信旗下天翼视联增资至约7.9亿元

2026年3月13日 16:44
36氪获悉,爱企查App显示,近日,天翼视联科技股份有限公司发生工商变更,注册资本由7.1亿元人民币增至约7.9亿元人民币,增幅约12%。 天翼视联科技股份有限公司成立于2023年11月,法定代表人为胡伟良,经营范围包括数字视频监控系统制造、安防设备制造、物联网设备制造等。股东信息显示,该公司由中国电信股份有限公司全资持股。

Vue实战|折腾两天,终于找到业务打印救星:vue-print-designer网页套打插件接入

2026年3月13日 16:32

在后台管理系统、电商ERP、工单管理这类企业级业务系统里,打印功能从来都不是简单调用浏览器原生print就能落地的刚需场景。前段时间做内部业务系统改造,我卡在订单、物流面单、财务单据的web打印、网页套打环节,前前后后折腾整整两天,试过原生window.print+媒体查询、printJS、多款小众网页打印控件,要么套打精度差、换设备就错位,要么模板定制繁琐、兼容性拉胯,直到用上vue-print-designer,才彻底解决这块业务痛点,稳稳落地各类打印需求。

这篇文章不走浮夸宣传路线,只分享真实踩坑经历、零冗余实战步骤和落地心得,把这款适配Vue项目的web打印插件、网页打印控件推荐给同样被打印需求困扰的前端同行,全程只讲实操、不夸大效果,还原真实接入体验。


60b6ff7d-eee3-4f2a-b7cd-a81925c09c47.png

一、先说说踩坑两天的痛点:为什么常规打印方案行不通

最开始做业务打印,我走的是常规思路:用原生window.print配合媒体查询样式,做简单的单据打印,结果上线后问题一堆,完全扛不住真实业务场景:

  • 套打精度极差:针对固定格式的单据、快递面单、收货清单,手写CSS很难做到边距、元素位置精准对齐,换台打印机、换个浏览器就错位,业务端完全没法用;
  • 模板定制成本高:每新增一种打印模板,就要重新写一套样式,动态数据绑定繁琐,后期维护成本直线上升,前端精力全耗在调整打印样式上;
  • 功能太单一:只能触发浏览器打印,没法导出PDF、生成图片,不支持条形码、二维码自动生成,也没法做批量打印、静默打印,满足不了仓库、财务的实际业务需求;
  • 兼容性问题频发:不同浏览器的打印内核差异大,部分样式不生效,分页逻辑混乱,甚至出现内容截断的情况。

中间也换过两款小众网页打印控件,要么依赖过重,要么文档残缺,调试半天跑不起来,白白浪费了两天时间。直到在开源平台找到vue-print-designer,一款轻量、易接入、专注web套打的打印插件,上手调试后,直接解决了所有核心痛点,适配现有项目毫无压力。

二、vue-print-designer 核心优势:为什么能成为业务打印救星

这款插件没有花里胡哨的冗余功能,全部围绕web打印、网页套打、可视化模板设计核心需求设计,风格极简实用,刚好戳中业务打印的痛点,适合各类后台系统集成:

  1. 可视化拖拽设计,告别手写CSS:自带打印模板设计器,支持拖拽添加文本、图片、条形码、二维码、表格等元素,上传底图就能做精准套打,所见即所得,不用再反复调试样式;
  2. 轻量无侵入,接入成本极低:不强制依赖特定UI框架,基于标准前端规范开发,Vue2/Vue3项目都能兼容,甚至可嵌入非Vue的后台系统(比如FastAdmin这类混合架构),不用改造原有项目架构;
  3. 功能贴合业务,实用性拉满:支持浏览器直接打印、PDF导出、图片导出,支持动态数据绑定、批量打印,适配订单、工单、面单、标签等各类业务打印场景;
  4. 开源免费,文档清晰:开源可商用,代码可二次定制,官方文档步骤详细,新手也能快速上手,没有付费门槛和功能限制。

插件官方Gitee地址:gitee.com/theGreatOld… ,需要的开发者可以直接跳转查看源码和完整文档。


三、实战接入:Vue项目完整集成步骤(零踩坑版)

这里以Vue3项目为例,分享最简洁的接入流程,全程不搞复杂配置,按照步骤操作即可快速落地,Vue2项目仅需微调引入方式,官方文档有对应说明。

1. 插件引入:CDN/本地安装二选一

推荐新手直接用CDN引入,无需安装依赖,快速验证效果;正式项目可本地安装,便于管理。

方式一:CDN在线引入(推荐快速测试)

<!-- 引入Vue3核心 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<!-- 引入vue-print-designer 样式与组件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-print-designer/style.css">
<script src="https://cdn.jsdelivr.net/npm/vue-print-designer"></script>

方式二:npm本地安装

npm install vue-print-designer --save
# 或者yarn
yarn add vue-print-designer

2. 全局注册/局部引入组件

在main.js中全局注册,方便全项目各个页面调用,也可在需要打印的页面单独引入。

import { createApp } from 'vue'
import App from './App.vue'
import PrintDesigner from 'vue-print-designer'
import 'vue-print-designer/style.css'

const app = createApp(App)
// 全局注册打印组件
app.use(PrintDesigner)
app.mount('#app')

3. 页面使用:模板设计+数据绑定实战

直接在业务页面使用组件,绑定后台返回的业务数据,支持加载预设模板、动态赋值,实现单据、订单的快速套打。

<template>
  <div class="print-page">
    <!-- 打印设计器容器 -->
    <print-designer
      ref="printRef"
      :template="printTemplate"
      :variables="orderData"
    />
    <!-- 打印操作按钮组 -->
    <div class="print-btn-group">
      <button @click="handlePrint">直接打印</button>
      <button @click="handleExportPdf">导出PDF</button>
    </div>
  </div>
</template>

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

// 打印组件实例引用
const printRef = ref(null)
// 预设打印模板(设计器导出JSON后直接赋值使用)
const printTemplate = ref({})
// 业务单据数据:订单/工单动态绑定数据
const orderData = ref({
  orderNo: 'YD202603130089',
  createTime: '2026-03-13 14:30:22',
  receiver: '张先生',
  phone: '138****1234',
  address: '北京市朝阳区某某大厦15层',
  goodsList: [
    { name: '办公笔记本', num: 2, price: 39.9 },
    { name: '中性笔套装', num: 1, price: 19.9 }
  ],
  totalPrice: 99.7
})

// 初始化打印模板,可替换为接口获取后台存储的模板JSON
const initTemplate = () => {
  // 实际项目中从接口/本地文件加载已设计好的模板
  printTemplate.value = {}
}

// 浏览器直接打印调用
const handlePrint = () => {
  // 可选静默打印模式,配合客户端实现无弹窗打印
  printRef.value?.print({ mode: 'browser' })
}

// 导出PDF文件,自定义文件名
const handleExportPdf = () => {
  printRef.value?.export({ type: 'pdf', filename: `订单_${orderData.value.orderNo}.pdf` })
}

// 页面挂载后初始化模板
initTemplate()
</script>

<style scoped>
.print-page {
  width: 100%;
  padding: 20px;
  box-sizing: border-box;
}
.print-btn-group {
  margin-top: 20px;
  display: flex;
  gap: 12px;
}
.print-btn-group button {
  padding: 8px 16px;
  cursor: pointer;
  border: none;
  border-radius: 4px;
  background: #409eff;
  color: #fff;
}
</style>

4. 实战避坑技巧

  • 模板复用:先在可视化设计器里完成模板拖拽制作,导出JSON格式,存入数据库或本地文件,后续直接接口调用加载,无需前端重复配置;
  • 数据绑定:模板变量名与业务数据字段严格对应,数组类型数据直接绑定,插件自动渲染列表内容,无需手动循环;
  • 打印效果统一:正式环境建议固定纸张规格,关闭浏览器默认页眉页脚,避免不同设备打印效果不一致;
  • 批量打印优化:批量打印时采用分页加载、逐个渲染的方式,避免一次性加载大量数据导致页面卡顿。

美团王莆中:建设物理世界AI底座,帮每个商家都用上自己的AI助理

2026年3月13日 16:31
3月13日,美团召开2026管理层沟通会。对于AI发展,美团核心本地商业CEO王莆中表示,美团将坚持加大投入,一层是投入研发物流、机器人等相关科技,譬如无人机、无人车等;另一层是建设丰富的物理世界信息、真实的评价,捕捉即时动态的信息,为模型、C端Agent所用。“帮商家去了解和改造物理世界,是我们非常清晰的战略。”王莆中表示,美团将帮每个商家都用上自己的AI助理。
❌
❌