普通视图

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

tauri2+vue+vite实现基于webview视图渲染的桌面端开发

2025年12月11日 17:33

创建应用

pnpm create tauri-app

应用程序更新

1.安装依赖

安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用

pnpm tauri add updater

2.配置

密钥生成,在package.json文件中添加,如下命令生成更新公钥和私钥

"@description:updater": "Tauri CLI 提供了 signer generate 命令 生成更新密钥",
"updater": "tauri signer generate -w ~/.tauri/myapp.key"

在windows环境变量配置私钥,输入cmd 命令行执行 win cmd

set TAURI_PRIVATE_KEY="content of the generated key"
set TAURI_KEY_PASSWORD="password"

powershell

$env:TAURI_PRIVATE_KEY="content of the generated key"
$env:TAURI_KEY_PASSWORD="password"

在 src-tauri\tauri.conf.json 文件中开启自动升级,并将公钥添加到里面,设置你的升级信息json文件获取的url路径

{
  "app": {},
  "bundle": {
    "createUpdaterArtifacts": true,
    "icon": []
  },
  "plugins": {
    "updater": {
      "active": true,
      "windows": {
        "installMode": "passive"
      },
      "pubkey": "公钥",
      "endpoints": ["https://xxx/download/latest.json"]
    }
  }
}

更新 latest.json 内容

{
  "version": "v1.0.0",
  "notes": "Test version",
  "pub_date": "2020-06-22T19:25:57Z",
  "platforms": {
    "darwin-x86_64": {
      "signature": "Content of app.tar.gz.sig",
      "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz"
    },
    "darwin-aarch64": {
      "signature": "Content of app.tar.gz.sig",
      "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz"
    },
    "linux-x86_64": {
      "signature": "Content of app.AppImage.tar.gz.sig",
      "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz"
    },
    "windows-x86_64": {
      "signature": "Content of app.msi.sig",
      "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64.msi.zip"
    }
  }
}

tauri 权限配置 src-tauri\capabilities\default.json

{
  "permissions": [
    "updater:default",
    "updater:allow-check",
    "updater:allow-download",
    "updater:allow-install"
  ]
}

3.封装hooks

src\hooks\updater.ts

import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
export default () => {
  const message = window.$message;
  const dialog = window.$dialog;

  const checkV = async () => {
    return await check()
      .then((e: any) => {
        if (!e?.available) {
          return;
        }
        return {
          version: e.version,
          meg: `新版本 ${e.version} ,发布时间: ${e.date} 升级信息: ${e.body}`,
        };
      })
      .catch((e) => {
        console.error("检查更新错误,请稍后再试 " + e);
      });
  };

  const updater = async () => {
    dialog.success({
      title: "系统提示",
      content: "您确认要更新吗 ?",
      positiveText: "更新",
      negativeText: "不更新",
      maskClosable: false,
      closable: false,
      onPositiveClick: async () => {
        message.success("正在下载更新,请稍等");

        await check()
          .then(async (e: any) => {
            if (!e?.available) {
              return;
            }
            await e.downloadAndInstall((event: any) => {
              switch (event.event) {
                case "Started":
                  message.success(
                    "文件大小:" + event.data.contentLength
                      ? event.data.contentLength
                      : 0
                  );
                  break;
                case "Progress":
                  message.success("正在下载" + event.data.chunkLength);
                  break;
                case "Finished":
                  message.success("安装包下载成功,10s后重启并安装");
                  setTimeout(async () => {
                    await relaunch();
                  }, 10000);
                  break;
              }
            });
          })
          .catch((e) => {
            console.error("检查更新错误,请稍后再试 " + e);
          });
      },
      onNegativeClick: () => {
        message.info("您已取消更新");
      },
    });
  };

  return {
    checkV,
    updater,
  };
};

4.调用示例

<template>
  <div>
    {{ meg }}
    <n-button type="primary" @click="updateTask">检查更新</n-button>
  </div>
</template>

<script setup lang="ts">
import { message } from "@tauri-apps/plugin-dialog";
import pkg from "../../package.json";
import useUpdater from "@/hooks/updater";
import { ref } from "vue";
const meg = ref("版本检测 ");
const { checkV, updater } = useUpdater();
const state = ref(false);
const updateTask = async () => {
  if (state.value) {
    await updater();
  } else {
    let res = await checkV();
    if (res) {
      meg.value = "发现新版本:" + res.meg;
      state.value = pkg.version !== res.version;
    }
  }
};
</script>

自定义系统托盘

前端方式(hooks函数)【推荐】

1.配置

添加自定义图标权限 src-tauri\Cargo.toml

[dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] }

2.封装hooks

src\hooks\tray.ts

// 获取当前窗口
import { getCurrentWindow } from "@tauri-apps/api/window";
// 导入系统托盘
import { TrayIcon, TrayIconOptions, TrayIconEvent } from "@tauri-apps/api/tray";
// 托盘菜单
import { Menu } from "@tauri-apps/api/menu";
// 进程管理
import { exit } from "@tauri-apps/plugin-process";

// 定义闪烁状态
let isBlinking: boolean = false;
let blinkInterval: NodeJS.Timeout | null = null;
let trayInstance: TrayIcon | any | null = null;
let originalIcon: string | any;
/**
 * 在这里你可以添加一个托盘菜单,标题,工具提示,事件处理程序等
 */
const options: TrayIconOptions = {
  // icon 项目根目录/src-tauri/
  icon: "icons/32x32.png",
  tooltip: "zero",
  menuOnLeftClick: false,
  action: (event: TrayIconEvent) => {
    if (
      event.type === "Click" &&
      event.button === "Left" &&
      event.buttonState === "Down"
    ) {
      // 显示窗口
      winShowFocus();
    }
  },
};

/**
 * 窗口置顶显示
 */
async function winShowFocus() {
  try {
    // 获取窗体实例
    const win = getCurrentWindow();
    // 检查窗口是否见,如果不可见则显示出来
    if (!(await win.isVisible())) {
      await win.show();
    } else {
      // 检查是否处于最小化状态,如果处于最小化状态则解除最小化
      if (await win.isMinimized()) {
        await win.unminimize();
      }
      // 窗口置顶
      await win.setFocus();
    }
  } catch (error) {
    console.error("Error in winShowFocus:", error);
  }
}

/**
 * 创建托盘菜单
 */
async function createMenu() {
  try {
    return await Menu.new({
      // items 的显示顺序是倒过来的
      items: [
        {
          id: "show",
          text: "显示窗口",
          action: () => {
            winShowFocus();
          },
        },
        {
          id: "quit",
          text: "退出",
          action: () => {
            exit(0);
          },
        },
      ],
    });
  } catch (error) {
    console.error("Error in createMenu:", error);
    return null;
  }
}

/**
 * 创建系统托盘
 */
export async function createTray() {
  try {
    const menu = await createMenu();
    if (menu) {
      options.menu = menu;
      const tray = await TrayIcon.new(options);
      trayInstance = tray;
      originalIcon = options.icon; // 保存原始图标
      return tray;
    }
  } catch (error) {
    console.error("Error in createTray:", error);
  }
}

/**
 * 开启图标闪烁
 * @param icon1 图标1路径(可选,默认原始图标)
 * @param icon2 图标2路径(可选,默认alt图标)
 * @param interval 闪烁间隔(默认500ms)
 */
export async function startBlinking(
  icon1?: string,
  icon2?: string,
  interval: number = 500
) {
  if (!trayInstance) {
    console.error("Tray not initialized");
    return;
  }

  // 如果正在闪烁,先停止
  stopBlinking();

  // 设置图标路径
  const targetIcon1 = icon1 || originalIcon;
  const targetIcon2 = icon2 || "icons/32x32_alt.png"; // 备用图标路径

  isBlinking = true;
  let currentIcon = targetIcon1;

  blinkInterval = setInterval(async () => {
    try {
      currentIcon = currentIcon === targetIcon1 ? targetIcon2 : targetIcon1;
      await trayInstance!.setIcon(currentIcon);
    } catch (error) {
      console.error("Blinking error:", error);
      stopBlinking();
    }
  }, interval);
}

/**
 * 停止闪烁并恢复原始图标
 */
export function stopBlinking() {
  if (blinkInterval) {
    clearInterval(blinkInterval);
    blinkInterval = null;
    isBlinking = false;

    // 恢复原始图标
    if (trayInstance) {
      trayInstance
        .setIcon(originalIcon)
        .catch((error) => console.error("恢复图标失败:", error));
    }
  }
}

/**
 * 销毁托盘(自动停止闪烁)
 */
export async function destroyTray() {
  try {
    stopBlinking();
    if (trayInstance) {
      await trayInstance.destroy();
      trayInstance = null;
    }
  } catch (error) {
    console.error("Error destroying tray:", error);
  }
}

3.调用示例

结合不同场景引入 hooks 函数,调用对应方法,其中 createTray函数 可以放到 main.ts 中在系统启动时创建

// 场景示例:即时通讯应用
class ChatApp {
  async init() {
    // 应用启动时初始化托盘
    await createTray();
  }

  onNewMessage() {
    // 收到新消息时启动红色提醒闪烁
    startBlinking("icons/msg_new.png", "icons/msg_alert.png");
  }

  onMessageRead() {
    // 用户查看消息后停止闪烁
    stopBlinking();
  }

  async shutdown() {
    // 退出时清理资源
    await destroyTray();
  }
}

// 场景示例:下载管理器
class DownloadManager {
  onDownloadProgress() {
    // 下载时使用蓝色图标呼吸灯效果
    startBlinking("icons/download_active.png", "icons/download_idle.png", 1000);
  }

  onDownloadComplete() {
    // 下载完成停止闪烁并显示完成图标
    stopBlinking();
    trayInstance?.setIcon("icons/download_done.png");
  }
}

前后端结合方式(Rust函数)

1.配置

添加自定义图标权限 src-tauri\Cargo.toml

[dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] }

添加配置 src-tauri\tauri.conf.json 自定义图标

"app": {
  "windows": [
  ],
  "trayIcon": {
    "iconPath": "icons/icon.ico",
    "iconAsTemplate": true,
    "title": "时间管理器",
    "tooltip": "时间管理器"
  }
},

2.Rust 封装

托盘事件定义,新建 tray.rs 文件 src-tauri\src\tray.rs

use tauri::{
    menu::{Menu, MenuItem, Submenu},
    tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
    Manager, Runtime,
};

pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
    let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
    let show_i = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
    let hide_i = MenuItem::with_id(app, "hide", "隐藏", true, None::<&str>)?;
    let edit_i = MenuItem::with_id(app, "edit_file", "编辑", true, None::<&str>)?;
    let new_i = MenuItem::with_id(app, "new_file", "添加", true, None::<&str>)?;
    let a = Submenu::with_id_and_items(app, "File", "文章", true, &[&new_i, &edit_i])?;
    // 分割线
    let menu = Menu::with_items(app, &[&quit_i, &show_i, &hide_i, &a])?;
    // 创建系统托盘 let _ = TrayIconBuilder::with_id("icon")
    let _ = TrayIconBuilder::with_id("tray")
        // 添加菜单
        .menu(&menu)
        // 添加托盘图标
        .icon(app.default_window_icon().unwrap().clone())
        .title("zero")
        .tooltip("zero")
        .show_menu_on_left_click(false)
        // 禁用鼠标左键点击图标显示托盘菜单
        // .show_menu_on_left_click(false)
        // 监听事件菜单
        .on_menu_event(move |app, event| match event.id.as_ref() {
            "quit" => {
                app.exit(0);
            }
            "show" => {
                let window = app.get_webview_window("main").unwrap();
                let _ = window.show();
            }
            "hide" => {
                let window = app.get_webview_window("main").unwrap();
                let _ = window.hide();
            }
            "edit_file" => {
                println!("edit_file");
            }
            "new_file" => {
                println!("new_file");
            }
            // Add more events here
            _ => {}
        })
        // 监听托盘图标发出的鼠标事件
        .on_tray_icon_event(|tray, event| {
            // 左键点击托盘图标显示窗口
            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                let app = tray.app_handle();
                if let Some(window) = app.get_webview_window("main") {
                    let _ = window.show();
                    let _ = window.set_focus();
                }
            }
        })
        .build(app);

    Ok(())
}

lib.rs 使用,注册函数暴露给前端调用

#[cfg(desktop)]
mod tray;

// 自定义函数声明
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_log::Builder::new().build())
        // 添加自定义托盘
        .setup(|app| {
            #[cfg(all(desktop))]
            {
                let handle: &tauri::AppHandle = app.handle();
                tray::create_tray(handle)?;
            }
            Ok(())
        })
        // Run the app
        // 注册 Rust 后端函数,暴露给前端调用
        .invoke_handler(tauri::generate_handler![
            greet
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

3.前端调用Rust暴露函数

<template>
  <div>
    <button class="item" @click="flashTray(true)">开启图标闪烁</button>
    <button class="item" @click="flashTray(false)">关闭图标闪烁</button>
  </div>
</template>
<script setup lang="ts">
import { TrayIcon } from "@tauri-apps/api/tray";

const flashTimer = ref<Boolean | any>(false);
const flashTray = async (bool: Boolean) => {
  let flag = true;
  if (bool) {
    TrayIcon.getById("tray").then(async (res: any) => {
      clearInterval(flashTimer.value);
      flashTimer.value = setInterval(() => {
        if (flag) {
          res.setIcon(null);
        } else {
          // res.setIcon(defaultIcon)
          // 支持把自定义图标放在默认icons文件夹,通过如下方式设置图标
          // res.setIcon('icons/msg.png')
          // 支持把自定义图标放在自定义文件夹tray,需要配置tauri.conf.json参数 "bundle": {"resources": ["tray"]}
          res.setIcon("tray/tray.png");
        }
        flag = !flag;
      }, 500);
    });
  } else {
    clearInterval(flashTimer.value);
    let tray: any = await TrayIcon.getById("tray");
    tray.setIcon("icons/icon.png");
  }
};
</script>

窗口工具栏自定义

1. 配置

配置文件开启权限 src-tauri\capabilities\default.json

 "permissions": [
    "core:window:default",
    "core:window:allow-start-dragging",
    "core:window:allow-minimize",
    "core:window:allow-maximize",
    "core:window:allow-unmaximize",
    "core:window:allow-toggle-maximize",
    "core:window:allow-show",
    "core:window:allow-set-focus",
    "core:window:allow-hide",
    "core:window:allow-unminimize",
    "core:window:allow-set-size",
    "core:window:allow-close",
  ]

关闭默认窗口事件 src-tauri\tauri.conf.json

"app": {
  "windows": [
    {
      "decorations": false,
    }
  ],
},

2. 自定义实现

前端调用

<script setup lang="ts">
import { getCurrentWindow } from "@tauri-apps/api/window";

const appWindow = getCurrentWindow();
onMounted(() => {
  windowCustomize();
});
const windowCustomize = () => {
  let minimizeEle = document.getElementById("titlebar-minimize");
  minimizeEle?.addEventListener("click", () => appWindow.minimize());

  let maximizeEle = document.getElementById("titlebar-maximize");
  maximizeEle?.addEventListener("click", () => appWindow.toggleMaximize());

  let closeEle = document.getElementById("titlebar-close");
  closeEle?.addEventListener("click", () => appWindow.close());
};
</script>

<template>
  <div data-tauri-drag-region class="titlebar">
    <div class="titlebar-button" id="titlebar-minimize">
      <img src="@/assets/svg/titlebar/mdi_window-minimize.svg" alt="minimize" />
    </div>
    <div class="titlebar-button" id="titlebar-maximize">
      <img src="@/assets/svg/titlebar/mdi_window-maximize.svg" alt="maximize" />
    </div>
    <div class="titlebar-button" id="titlebar-close">
      <img src="@/assets/svg/titlebar/mdi_close.svg" alt="close" />
    </div>
  </div>
</template>

<style scoped>
.titlebar {
  height: 30px;
  background: #329ea3;
  user-select: none;
  display: flex;
  justify-content: flex-end;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}
.titlebar-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 30px;
  height: 30px;
  user-select: none;
  -webkit-user-select: none;
}
.titlebar-button:hover {
  background: #5bbec3;
}
</style>

webview 多窗口创建

1. 配置

配置文件开启权限 src-tauri\capabilities\default.json

 "permissions": [
    "core:webview:default",
    "core:webview:allow-create-webview-window",
    "core:webview:allow-create-webview",
    "core:webview:allow-webview-close",
    "core:webview:allow-set-webview-size",
  ]

2. hooks 函数封装

import { nextTick } from "vue";
import {
  WebviewWindow,
  getCurrentWebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { emit, listen } from "@tauri-apps/api/event";

export interface WindowsProps {
  label: string;
  url?: string;
  title: string;
  minWidth: number;
  minHeight: number;
  width: number;
  height: number;
  closeWindLabel?: string;
  resizable: boolean;
}

export default () => {
  // 窗口事件类型
  type WindowEvent = "closed" | "minimized" | "maximized" | "resized";

  // 创建窗口
  const createWindows = async (
    args: WindowsProps = {
      label: "main",
      title: "主窗口",
      minWidth: 800,
      minHeight: 600,
      width: 800,
      height: 600,
      resizable: true,
    }
  ) => {
    if (!(await isExist(args.label))) {
      const webview = new WebviewWindow(args.label, {
        title: args.title,
        url: args.url,
        fullscreen: false,
        resizable: args.resizable,
        center: true,
        width: args.width,
        height: args.height,
        minWidth: args.minWidth,
        minHeight: args.minHeight,
        skipTaskbar: false,
        decorations: false,
        transparent: false,
        titleBarStyle: "overlay",
        hiddenTitle: true,
        visible: false,
      });

      // 窗口创建成功
      await webview.once("tauri://created", async () => {
        webview.show();
        if (args.closeWindLabel) {
          const win = await WebviewWindow.getByLabel(args.closeWindLabel);
          win?.close();
        }
      });

      // 窗口创建失败
      await webview.once("tauri://error", async (e) => {
        console.error("Window creation error:", e);
        if (args.closeWindLabel) {
          await showWindow(args.closeWindLabel);
        }
      });

      // 监听窗口事件
      setupWindowListeners(webview, args.label);
      return webview;
    } else {
      showWindow(args.label);
    }
  };

  // 设置窗口监听器
  const setupWindowListeners = (webview: WebviewWindow, label: string) => {
    // 关闭请求处理
    webview.listen("tauri://close-requested", async (e) => {
      await emit("window-event", {
        label,
        event: "closed",
        data: { timestamp: Date.now() },
      });
      console.log("label :>> ", label);
      const win = await WebviewWindow.getByLabel(label);
      win?.close();

      // const win = label ? await WebviewWindow.getByLabel(label) : await getCurrentWebviewWindow();
      // win?.close();
    });

    // 最小化事件
    webview.listen("tauri://minimize", async (e) => {
      await emit("window-event", {
        label,
        event: "minimized",
        data: { state: true },
      });
    });

    // 最大化事件
    webview.listen("tauri://maximize", async (e) => {
      await emit("window-event", {
        label,
        event: "maximized",
        data: { state: true },
      });
    });

    // 取消最大化
    webview.listen("tauri://unmaximize", async (e) => {
      await emit("window-event", {
        label,
        event: "maximized",
        data: { state: false },
      });
    });
  };

  // 窗口间通信 - 发送消息
  const sendWindowMessage = async (
    targetLabel: string,
    event: string,
    payload: any
  ) => {
    const targetWindow = await WebviewWindow.getByLabel(targetLabel);
    if (targetWindow) {
      targetWindow.emit(event, payload);
    }
  };

  // 监听窗口消息
  const onWindowMessage = (event: string, callback: (payload: any) => void) => {
    return listen(event, ({ payload }) => callback(payload));
  };

  // 窗口控制方法
  const windowControls = {
    minimize: async (label?: string) => {
      const win = label
        ? await WebviewWindow.getByLabel(label)
        : await getCurrentWebviewWindow();
      await win?.minimize();
    },
    maximize: async (label?: string) => {
      const win = label
        ? await WebviewWindow.getByLabel(label)
        : await getCurrentWebviewWindow();
      await win?.maximize();
    },
    close: async (label?: string) => {
      const win = label
        ? await WebviewWindow.getByLabel(label)
        : await getCurrentWebviewWindow();
      win?.close();
    },
    toggleMaximize: async (label?: string) => {
      const win = label
        ? await WebviewWindow.getByLabel(label)
        : await getCurrentWebviewWindow();
      const isMaximized = await win?.isMaximized();
      isMaximized ? await win?.unmaximize() : await win?.maximize();
    },
  };
  //  获取当前窗口
  const nowWindow = async () => {
    const win = await getCurrentWebviewWindow();
    return win;
  };
  // 关闭窗口
  const closeWindow = async (label?: string) => {
    if (label) {
      const win = await WebviewWindow.getByLabel(label);
      win?.close();
    } else {
      const win = await getCurrentWebviewWindow();
      win?.close();
    }
  };
  // 显示窗口
  const showWindow = async (label: string, isCreated: boolean = false) => {
    const isExistsWinds = await WebviewWindow.getByLabel(label);
    if (isExistsWinds) {
      nextTick().then(async () => {
        // 检查是否是隐藏
        const hidden = await isExistsWinds.isVisible();
        if (!hidden) {
          await isExistsWinds.show();
        }
        // 如果窗口已存在,首先检查是否最小化了
        const minimized = await isExistsWinds.isMinimized();
        if (minimized) {
          // 如果已最小化,恢复窗口
          await isExistsWinds.unminimize();
        }
        // 如果窗口已存在,则给它焦点,使其在最前面显示
        await isExistsWinds.setFocus();
      });
    } else {
      if (!isCreated) {
        return createWindows();
      }
    }
  };
  //窗口是否存在
  const isExist = async (label: string) => {
    const isExistsWinds = await WebviewWindow.getByLabel(label);
    if (isExistsWinds) {
      return true;
    } else {
      return false;
    }
  };

  return {
    createWindows,
    sendWindowMessage,
    onWindowMessage,
    ...windowControls,
    nowWindow,
    showWindow,
    isExist,
    closeWindow,
  };
};

3. 调用

window 父级

<template>
  <div class="window-controls">
    <n-button @click="minimizeWindow">最小化</n-button>
    <n-button @click="toggleMaximizeWindow">{{
      isMaximized ? "恢复" : "最大化"
    }}</n-button>
    <n-button @click="maximizeWindow">最大化</n-button>
    <n-button @click="closeWindow">关闭</n-button>
    <n-button @click="openChildWindow">打开子窗口</n-button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import useWindowManager from "@/hooks/windowManager";
const {
  createWindows,
  minimize,
  maximize,
  toggleMaximize,
  close,
  onWindowMessage,
} = useWindowManager();

const isMaximized = ref(false);

const openChildWindow = () => {
  createWindows({
    label: "child",
    title: "子窗口",
    url: "/child",
    minWidth: 400,
    minHeight: 300,
    width: 600,
    height: 400,
    resizable: true,
  });
};
// 监听子窗口消息
onWindowMessage("child-message", (payload) => {
  console.log("Received from child:", payload);
});

// 窗口控制方法
const minimizeWindow = async () => {
  await minimize("child"); // 最小化窗口
};

const maximizeWindow = async () => {
  await maximize("child"); // 最大化窗口
};

const toggleMaximizeWindow = async () => {
  await toggleMaximize("child"); // 切换最大化/还原
};

const closeWindow = async () => {
  await close("child"); // 关闭窗口
};
</script>

childView.vue 子组件

<template>
  <div class="child">
    <h1>Child Window</h1>
    <n-button @click="sendToMain">Send Message to Main</n-button>
    <n-button @click="close">Close</n-button>
  </div>
</template>

<script setup lang="ts">
import useWindowManager from "@/hooks/windowManager";

const { sendWindowMessage, close } = useWindowManager();
// const {close} = windowControls
// 向主窗口发送消息
const sendToMain = () => {
  sendWindowMessage("main", "child-message", {
    timestamp: Date.now(),
    content: "Hello from child!",
  });
};
</script>

系统通知 notification

1.安装依赖

安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用

pnpm tauri add notification

2.配置

tauri 权限配置 src-tauri\capabilities\default.json

{
  "permissions": [
    "notification:default",
    "notification:allow-get-active",
    "notification:allow-is-permission-granted"
  ]
}

3.封装hooks

src\hooks\notification.ts

import {
  isPermissionGranted,
  requestPermission,
  sendNotification,
} from "@tauri-apps/plugin-notification";

export default () => {
  const checkPermission = async () => {
    const permission = await isPermissionGranted();
    if (!permission) {
      const permission = await requestPermission();
      return permission === "granted";
    } else {
      return true;
    }
  };

  const sendMessage = async (title: string, message: string) => {
    const permission = await checkPermission();
    if (permission) {
      await sendNotification({
        title,
        body: message,
        // 这里演示,你可以作为参数传入 win11 测试没效果
        attachments: [
          {
            id: "image-1",
            url: "F:\\tv_task\\public\\tauri.png",
          },
        ],
      });
    }
  };

  return { sendMessage };
};

4.调用示例

<template>
  <div>
    <n-button @click="sendNot">notification 通知</n-button>
  </div>
</template>

<script setup lang="ts">
import useNotification from "@/hooks/notification";
const { sendMessage } = useNotification();
const sendNot = async () => {
  await sendMessage("提示", "您当前有代办的任务需要处理!");
};
</script>

日志

1.安装依赖

安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用

pnpm tauri add log

2.配置

tauri 权限配置 src-tauri\capabilities\default.json

{
  "permissions": ["log:default"]
}

3.封装hooks

src\hooks\log.ts

import {
  trace,
  info,
  debug,
  error,
  attachConsole,
} from "@tauri-apps/plugin-log";

// 启用 TargetKind::Webview 后,这个函数将把日志打印到浏览器控制台
const detach = await attachConsole();

export default () => {
  // 将浏览器控制台与日志流分离
  detach();
  return {
    debug,
    trace,
    info,
    error,
  };
};

4.调用示例

<template>
  <div>
    <h1>控制台效果</h1>
    <div class="console">
      <div
        class="console-line"
        v-for="(line, index) in consoleLines"
        :key="index"
        :class="{
          'animate__animated animate__fadeIn':
            index === consoleLines.length - 1,
        }"
      >
        {{ line }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import TauriLog from "@/hooks/log";
import { ref } from "vue";

const { info } = TauriLog();
info("我来了");
const consoleLines = ref([
  "Welcome to the console!",
  "This is a cool console interface.",
  "You can type commands here.",
  "Press Enter to execute.",
]);
</script>

程序启动监听

hooks 函数封装

src\hooks\start.ts

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

function sleep(seconds: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}

async function setup() {
  console.log("前端应用启动..");
  await sleep(3);
  console.log("前端应用启动完成");
  // 调用后端应用
  invoke("set_complete", { task: "frontend" });
}

export default () => {
  // Effectively a JavaScript main function
  window.addEventListener("DOMContentLoaded", () => {
    setup();
  });
};

调用日志打印

src\main.ts

import start from "@/hooks/start";
start();

Http 封装

axios 请求,会在打包后存在跨域问题,所以使用 tauri 插件,进行http封装

1.安装依赖

安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用

pnpm tauri add http

2.配置

tauri 权限配置 src-tauri\capabilities\default.json

{
  "permissions": [
    {
      "identifier": "http:default",
      "allow": [
        {
          "url": "http://**"
        },
        {
          "url": "https://**"
        },
        {
          "url": "http://*:*"
        },
        {
          "url": "https://*:*"
        }
      ]
    }
  ]
}

3.封装hooks

src\utils\exception.ts

export enum ErrorType {
  Network = "NETWORK_ERROR",
  Authentication = "AUTH_ERROR",
  Validation = "VALIDATION_ERROR",
  Server = "SERVER_ERROR",
  Client = "CLIENT_ERROR",
  Unknown = "UNKNOWN_ERROR",
}

export interface ErrorDetails {
  type: ErrorType;
  code?: number;
  details?: Record<string, any>;
}

export class AppException extends Error {
  public readonly type: ErrorType;
  public readonly code?: number;
  public readonly details?: Record<string, any>;

  constructor(message: string, errorDetails?: Partial<ErrorDetails>) {
    super(message);
    this.name = "AppException";
    this.type = errorDetails?.type || ErrorType.Unknown;
    this.code = errorDetails?.code;
    this.details = errorDetails?.details;

    // Show error message to user if window.$message is available
    if (window.$message) {
      window.$message.error(message);
    }
  }

  public toJSON() {
    return {
      name: this.name,
      message: this.message,
      type: this.type,
      code: this.code,
      details: this.details,
    };
  }
}

src\utils\http.ts

import { fetch } from "@tauri-apps/plugin-http";
import { AppException, ErrorType } from "./exception";

/**
 * @description 请求参数
 * @property {"GET"|"POST"|"PUT"|"DELETE"} method 请求方法
 * @property {Record<string, string>} [headers] 请求头
 * @property {Record<string, any>} [query] 请求参数
 * @property {any} [body] 请求体
 * @property {boolean} [isBlob] 是否为Blob
 * @property {boolean} [noRetry] 是否禁用重试
 * @return HttpParams
 */
export type HttpParams = {
  method: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  query?: Record<string, any>;
  body?: any;
  isBlob?: boolean;
  retry?: RetryOptions; // 新增重试选项
  noRetry?: boolean; // 新增禁用重试选项
};

/**
 * @description 重试选项
 */
export type RetryOptions = {
  retries?: number;
  retryDelay?: (attempt: number) => number;
  retryOn?: number[];
};

/**
 * @description 自定义错误类,用于标识需要重试的 HTTP 错误
 */
class FetchRetryError extends Error {
  status: number;
  type: ErrorType;
  constructor(message: string, status: number) {
    super(message);
    this.status = status;
    this.name = "FetchRetryError";
    this.type = status >= 500 ? ErrorType.Server : ErrorType.Network;
  }
}

/**
 * @description 等待指定的毫秒数
 * @param {number} ms 毫秒数
 * @returns {Promise<void>}
 */
function wait(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * @description 判断是否应进行下一次重试
 * @returns {boolean} 是否继续重试
 */
function shouldRetry(
  attempt: number,
  maxRetries: number,
  abort?: AbortController
): boolean {
  return attempt + 1 < maxRetries && !abort?.signal.aborted;
}

/**
 * @description HTTP 请求实现
 * @template T
 * @param {string} url 请求地址
 * @param {HttpParams} options 请求参数
 * @param {boolean} [fullResponse=false] 是否返回完整响应
 * @param {AbortController} abort 中断器
 * @returns {Promise<T | { data: T; resp: Response }>} 请求结果
 */
async function Http<T = any>(
  url: string,
  options: HttpParams,
  fullResponse: boolean = false,
  abort?: AbortController
): Promise<{ data: T; resp: Response } | T> {
  // 打印请求信息
  console.log(`🚀 发起请求 → ${options.method} ${url}`, {
    body: options.body,
    query: options.query,
  });

  // 默认重试配置
  const defaultRetryOptions: RetryOptions = {
    retries: options.noRetry ? 0 : 3, // 如果设置了noRetry,则不进行重试
    retryDelay: (attempt) => Math.pow(2, attempt) * 1000, // 指数退避策略
    retryOn: [500, 502, 503, 504],
  };

  // 合并默认重试配置与用户传入的重试配置
  const retryOptions: RetryOptions = {
    ...defaultRetryOptions,
    ...options.retry,
  };

  const { retries = 3, retryDelay, retryOn } = retryOptions;

  // 获取token和指纹
  const token = localStorage.getItem("TOKEN");
  //const fingerprint = await getEnhancedFingerprint()

  // 构建请求头
  const httpHeaders = new Headers(options.headers || {});

  // 设置Content-Type
  if (!httpHeaders.has("Content-Type") && !(options.body instanceof FormData)) {
    httpHeaders.set("Content-Type", "application/json");
  }

  // 设置Authorization
  if (token) {
    httpHeaders.set("Authorization", `Bearer ${token}`);
  }

  // 设置浏览器指纹
  //if (fingerprint) {
  //httpHeaders.set('X-Device-Fingerprint', fingerprint)
  //}

  // 构建 fetch 请求选项
  const fetchOptions: RequestInit = {
    method: options.method,
    headers: httpHeaders,
    signal: abort?.signal,
  };

  // 获取代理设置
  // const proxySettings = JSON.parse(localStorage.getItem('proxySettings') || '{}')
  // 如果设置了代理,添加代理配置 (BETA)
  // if (proxySettings.type && proxySettings.ip && proxySettings.port) {
  //   // 使用 Rust 后端的代理客户端
  //   fetchOptions.proxy = {
  //     url: `${proxySettings.type}://${proxySettings.ip}:${proxySettings.port}`
  //   }
  // }

  // 判断是否需要添加请求体
  if (options.body) {
    if (
      !(
        options.body instanceof FormData ||
        options.body instanceof URLSearchParams
      )
    ) {
      fetchOptions.body = JSON.stringify(options.body);
    } else {
      fetchOptions.body = options.body; // 如果是 FormData 或 URLSearchParams 直接使用
    }
  }

  // 添加查询参数
  if (options.query) {
    const queryString = new URLSearchParams(options.query).toString();
    url += `?${queryString}`;
  }

  // 拼接 API 基础路径
  //url = `${import.meta.env.VITE_SERVICE_URL}${url}`

  // 定义重试函数
  async function attemptFetch(
    currentAttempt: number
  ): Promise<{ data: T; resp: Response } | T> {
    try {
      const response = await fetch(url, fetchOptions);
      // 若响应不 OK 并且状态码属于需重试列表,则抛出 FetchRetryError
      if (!response.ok) {
        const errorType = getErrorType(response.status);
        if (!retryOn || retryOn.includes(response.status)) {
          throw new FetchRetryError(
            `HTTP error! status: ${response.status}`,
            response.status
          );
        }
        // 如果是非重试状态码,则抛出带有适当错误类型的 AppException
        throw new AppException(`HTTP error! status: ${response.status}`, {
          type: errorType,
          code: response.status,
          details: { url, method: options.method },
        });
      }

      // 解析响应数据
      const responseData = options.isBlob
        ? await response.arrayBuffer()
        : await response.json();

      // 打印响应结果
      console.log(`✅ 请求成功 → ${options.method} ${url}`, {
        status: response.status,
        data: responseData,
      });

      // 若有success === false,需要重试
      if (responseData && responseData.success === false) {
        const errorMessage = responseData.errMsg || "服务器返回错误";
        window.$message?.error?.(errorMessage);
        throw new AppException(errorMessage, {
          type: ErrorType.Server,
          code: response.status,
          details: responseData,
        });
      }

      // 若请求成功且没有业务错误
      if (fullResponse) {
        return { data: responseData, resp: response };
      }
      return responseData;
    } catch (error) {
      console.error(`尝试 ${currentAttempt + 1} 失败的 →`, error);

      // 检查是否仍需重试
      if (!shouldRetry(currentAttempt, retries, abort)) {
        console.error(
          `Max retries reached or aborted. Request failed → ${url}`
        );
        if (error instanceof FetchRetryError) {
          window.$message?.error?.(error.message || "网络请求失败");
          throw new AppException(error.message, {
            type: error.type,
            code: error.status,
            details: { url, attempts: currentAttempt + 1 },
          });
        }
        if (error instanceof AppException) {
          window.$message?.error?.(error.message || "请求出错");
          throw error;
        }
        const errorMessage = String(error) || "未知错误";
        window.$message?.error?.(errorMessage);
        throw new AppException(errorMessage, {
          type: ErrorType.Unknown,
          details: { url, attempts: currentAttempt + 1 },
        });
      }

      // 若需继续重试
      const delayMs = retryDelay ? retryDelay(currentAttempt) : 1000;
      console.warn(
        `Retrying request → ${url} (next attempt: ${currentAttempt + 2}, waiting ${delayMs}ms)`
      );
      await wait(delayMs);
      return attemptFetch(currentAttempt + 1);
    }
  }

  // 辅助函数:根据HTTP状态码确定错误类型
  function getErrorType(status: number): ErrorType {
    if (status >= 500) return ErrorType.Server;
    if (status === 401 || status === 403) return ErrorType.Authentication;
    if (status === 400 || status === 422) return ErrorType.Validation;
    if (status >= 400) return ErrorType.Client;
    return ErrorType.Network;
  }

  // 第一次执行,attempt=0
  return attemptFetch(0);
}

export default Http;

src\utils\request.ts

import Http, { HttpParams } from "./http.ts";
import { ServiceResponse } from "@/enums/types.ts";
const { VITE_SERVICE_URL } = import.meta.env;
const prefix = VITE_SERVICE_URL;
function getToken() {
  let tempToken = "";
  return {
    get() {
      if (tempToken) return tempToken;
      const token = localStorage.getItem("TOKEN");
      if (token) {
        tempToken = token;
      }
      return tempToken;
    },
    clear() {
      tempToken = "";
    },
  };
}

export const computedToken = getToken();

// fetch 请求响应拦截器
const responseInterceptor = async <T>(
  url: string,
  method: "GET" | "POST" | "PUT" | "DELETE",
  query: any,
  body: any,
  abort?: AbortController
): Promise<T> => {
  let httpParams: HttpParams = {
    method,
  };

  if (method === "GET") {
    httpParams = {
      ...httpParams,
      query,
    };
  } else {
    url = `${prefix}${url}?${new URLSearchParams(query).toString()}`;
    httpParams = {
      ...httpParams,
      body,
    };
  }

  try {
    const data = await Http(url, httpParams, true, abort);
    const serviceData = (await data.data) as ServiceResponse;
    //检查服务端返回是否成功,并且中断请求
    if (!serviceData.success) {
      window.$message.error(serviceData.errMsg);
      return Promise.reject(`http error: ${serviceData.errMsg}`);
    }
    return Promise.resolve(serviceData.result);
  } catch (err) {
    return Promise.reject(`http error: ${err}`);
  }
};

const get = async <T>(
  url: string,
  query: T,
  abort?: AbortController
): Promise<T> => {
  return responseInterceptor(url, "GET", query, {}, abort);
};

const post = async <T>(
  url: string,
  params: any,
  abort?: AbortController
): Promise<T> => {
  return responseInterceptor(url, "POST", {}, params, abort);
};

const put = async <T>(
  url: string,
  params: any,
  abort?: AbortController
): Promise<T> => {
  return responseInterceptor(url, "PUT", {}, params, abort);
};

const del = async <T>(
  url: string,
  params: any,
  abort?: AbortController
): Promise<T> => {
  return responseInterceptor(url, "DELETE", {}, params, abort);
};

export default {
  get,
  post,
  put,
  delete: del,
};

src\api\manage.ts

import request from "@/utils/request";
export const getAction = <T>(
  url: string,
  params?: any,
  abort?: AbortController
) => request.get<T>(url, params, abort);
export const postAction = <T>(
  url: string,
  params?: any,
  abort?: AbortController
) => request.post<T>(url, params, abort);
export const putAction = <T>(
  url: string,
  params?: any,
  abort?: AbortController
) => request.put<T>(url, params, abort);
export const deleteAction = <T>(
  url: string,
  params?: any,
  abort?: AbortController
) => request.delete<T>(url, params, abort);

4.调用示例

<template>
  <div>
    <n-button @click="postTest">测试POST</n-button>
  </div>
</template>
<script setup lang="ts">
import { postAction } from "@/api/manage";

const postTest = () => {
  let url = `/sys/login`;
  postAction(url, {
    username: "admin",
    password: "tick20140513",
  }).then((res) => {
    text.value = res.token;
  });
};
</script>

zero-admin后台管理模板

2025年12月11日 17:24

zero-admin 管理后台模板

zero-admin 是一个后台前端解决方案,它基于 vue3 和 ant-design-vue 实现。它使用了最新的前端技术栈【vue3+vue-router+typescript+axios+ant-design-vue+pinia+mockjs+plopjs+vite+Vitest】实现了动态路由、权限验证;自定义 Vue 指令封装;规范项目代码风格;项目内置脚手架解决文件创建混乱,相似业务模块需要频繁拷贝代码或文件问题;Echarts 图形库进行封装;axios 请求拦截封装,请求 api 统一管理;通过 mockjs 模拟数据;对生产环境构建进行打包优化,实现了打包 gzip 压缩、代码混淆,去除 console 打印,打包体积分析等;提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型

image.png

image.png

image.png

推荐 VsCode 编辑器插件

VSCode + Volar (并禁用 Vetur 扩展插件) + TypeScript Vue Plugin (Volar).

TS 中.vue导入的类型支持

默认情况下,TypeScript 无法处理“.vue”导入的类型信息,因此我们将“tsc”CLI 替换为“vue-tsc”进行类型检查。在编辑中,我们需要 TypeScript Vue Plugin (Volar) 以使 TypeScript 语言服务了解“.vue”类型。

如果你觉得独立的 TypeScript 插件不够快,Volar 还实现了一个 Take Over Mode that 这更有表现力。您可以通过以下步骤启用它:

  1. Disable the built-in TypeScript Extension
    1. Run Extensions: Show Built-in Extensions from VSCode's command palette
    2. Find TypeScript and JavaScript Language Features, right click and select Disable (Workspace)
  2. Reload the VSCode window by running Developer: Reload Window from the command palette.

项目获取

git clone gitee.com/zmmlet/zero…

自定义 Vite 配置

Vite 配置参考.

项目依赖安装

pnpm install

开发环境运行(编译和热重新加载)

pnpm dev

打包部署运行(生产打包添加类型检查、编译)

pnpm build

运行单元测试 Vitest

pnpm test:unit

语法规则和代码风格检查 ESLint

pnpm lint

功能列表

  • 项目创建
  • 配置项目代码风格 .prettierrc.json
  • 配置.vscode setting.json 文件,配置保存格式化
  • 添加 SCSS 到项目进行 CSS 预处理
  • 配置 vscode 别名跳转规则
  • 安装 ant design vue 并配置自动加载
  • 配置 less 预处理,并自定义 ant Design Vue UI 主题
  • 解决 vite 首屏加载缓慢问题
  • pinia 数据持久化
  • 解决 pinia 使用报错问题
  • Layout 布局
  • Axios 封装
  • 菜单图标动态绑定
  • Vitest 单元测试
  • 集成打印插件
  • import.meta.glob 批量导入文件夹下文件
  • 配置 .env
  • 自定义按钮权限指令
  • 动态路由
  • 路由权限
  • 按钮权限(目前和登录账号有关,和具体页面无关)
  • Echarts 集成
  • 路由懒加载
  • 项目打包优化
    • 打包 gzip 压缩
    • 代码混淆
    • 去除生产环境 console
    • 打包体积分析插件
    • 代码拆包,将静态资源分类
    • 传统浏览器兼容性支持
    • CDN 内容分发网络(Content Delivery Network)
  • 项目集成自定义 cli 解决项目重复复制代码问题
  • 集成 mockjs
  • 读取 makdown 文档,编写组件说明文档
  • maptalks + threejs demo 示例
  • 使用 postcss-pxtorem、autoprefixer 插件 px 自动转换为 rem 和自动添加浏览器兼容前缀
  • Monorepo
  • 基于 sh 脚本对项目进行一键部署
  • 图形编辑器
    • 流程图
  • 国际化
  • CI/CD
  • 自动化部署
  • 使用 commitizen 规范 git 提交,存在 plopfile.js 和 commitlint 提交规范 导入模式问题"type": "module"冲突问题,导致目前 commitizen 规范 git 提交验证暂时不可用
  • husky

项目创建

  1. 项目创建命令 pnpm create vite
  2. 选择对应初始配置项
Progress: resolved 1, reused 1, downloaded 0, added 1, done
√ Project name: ... zero-admin
√ Select a framework: » Vue
√ Select a variant: » Customize with create-vue ↗
Packages: +1

Vue.js - The Progressive JavaScript Framework

√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... No / Yes
√ Add Prettier for code formatting? ... No / Yes

Scaffolding project in D:\learningSpace\code\vue-project\zero-admin...

Done. Now run:

cd zero-admin
pnpm install
pnpm lint
pnpm dev

初始项目依赖安装

  1. pnpm add ant-design-vue --save
  2. pnpm add unplugin-vue-components -D
  3. pnpm add axios
  4. pnpm add sass-loader@7.2.0 sass@1.22.10 -D
  5. pnpm add less -D

配置项目代码风格 .prettierrc.json

{
  "stylelintIntegration": true,
  "eslintIntegration": true,
  "printWidth": 80, //单行长度
  "tabWidth": 2, //缩进长度
  "useTabs": false, //使用空格代替tab缩进
  "semi": true, //句末使用分号
  "singleQuote": false, //使用单引号
  "endOfLine": "auto"
}

配置保存(Ctrl + s)自动格式化代码

在项目中创建.vscode 文件夹中创建 setting.json 文件

{
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  },
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  } // 默认格式化工具选择prettier
}

添加 SCSS 到项目进行 CSS 预处理

  1. pnpm add sass-loader@7.2.0 sass@1.22.10 -D

  2. 新建styles/scss 文件夹,新建 index.scss文件

  3. vite.config.ts 文件中配置

css: {
  preprocessorOptions: {
    // 配置 scss 预处理
    scss: {
      additionalData: '@import "@/style/scss/index.scss";',
    },
  },
},

项目根目录新建 jsconfig.json 文件

配置 vscode 别名跳转规则

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "baseUrl": ".",
    "jsx": "react",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@setting/*": ["./src/setting/*"],
      "@views/*": ["./src/views/*"],
      "@assets/*": ["./src/assets/*"],
      "@config/*": ["./src/config/*"],
      "@api/*": ["./src/api/*"],
      "@utils/*": ["./src/utils/*"],
      "@styles/*": ["./src/styles/*"],
      "@store/*": ["./src/store/*"]
    }
  },
  "exclude": ["node_modules", "dist"]
}

vite.config.ts 文件中配置对应文件夹别名

resolve: {
  alias: {
    "@": fileURLToPath(new URL("./src", import.meta.url)),
    "@comp": path.resolve(__dirname, "./src/components"),
  },
  extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
},

安装 ant design vue 并配置自动加载

安装 UI 和自动加载插件 pnpm add ant-design-vue --save pnpm add unplugin-vue-components -D 在 vite.config.ts 引入配置

// 引入 ant design vue 按需加载
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
  // 插件
  plugins: [
    vue(),
    // ant design vue 按需加载
    Components({
      resolvers: [AntDesignVueResolver({ importStyle: "less" })],
    }),
  ],
});

配置 less 预处理,并自定义 ant Design Vue UI 主题

安装 pnpm add less -D 在 vite.config.ts 引入配置

export default defineConfig({
  // 插件
  plugins: [
    vue(),
    // ant design vue 按需加载
    Components({
      resolvers: [AntDesignVueResolver({ importStyle: "less" })],
    }),
  ],

  css: {
    preprocessorOptions: {
      // 自定义 ant desing vue 主题样式
      less: {
        modifyVars: {
          "@primary-color": "red",
          "@border-radius-base": "0px", // 组件/浮层圆角
        },
        javascriptEnabled: true,
      },
    },
  },
});

pinia 数据持久化

pnpm add pinia-plugin-persist --save

解决 pinia 使用报错问题

使用

import { userStore } from "@/stores/modules/user";
const usersto = userStore();
console.log("store :>> ", usersto);

报错 转存失败,建议直接上传图片文件

解决方法

import store from "@/stores/index";
import { userStore } from "@/stores/modules/user";
const usersto = userStore(store);
console.log("store :>> ", usersto);

Layout 布局

Axios 封装

pnpm add axios --save

菜单图标动态绑定

  1. 动态创建
// ICON.ts
import { createVNode } from "vue";
import * as $Icon from "@ant-design/icons-vue";

export const Icon = (props: { icon: string }) => {
  const { icon } = props;
  return createVNode($Icon[icon]);
};
  1. 引入使用
<template>
  <div class="about">about <Icon :icon="icon" /></div>
</template>
<script lang="ts" setup>
import { Icon } from "@/setting/ICON";
import { ref } from "vue";
const icon = ref("AppstoreOutlined");
</script>

打包 gzip 压缩

// 引入 gzip 压缩
import viteCompression from "vite-plugin-compression";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    // 打包压缩,主要是本地gzip,如果服务器配置压缩也可以
    viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240,
      algorithm: "gzip",
      ext: ".gz",
    }),
  ],
});
server {
  #端口号,不同的程序,复制时,需要修改其端口号
        listen      3031;
  #服务器地址,可以为IP地址,本地程序时,可以设置为localhost
        server_name  localhost;
        client_max_body_size 2G;

    # 开启gzip
        gzip on;
    # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
        gzip_min_length 1k;
    # gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间,后面会有详细说明
        gzip_comp_level 1;
    # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。
        gzip_types text/html text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
    # 是否在http header中添加Vary: Accept-Encoding,建议开启
        gzip_vary on;
    # 禁用IE 6 gzip
        gzip_disable "MSIE [1-6]\.";
    # 设置压缩所需要的缓冲区大小
        gzip_buffers 32 4k;
    # 设置gzip压缩针对的HTTP协议版本
        gzip_http_version 1.0;

  #程序所在目录
        root D:/learningSpace/code/vue-project/zero-admin/dist;
        charset utf-8;
            index index.html;

        location / {
            try_files $uri $uri/ /index.html;
        }

        location @rewrites {
            rewrite ^(.+)$ /index.html last;
        }

  #程序映射地址,将【zero-service】改为你程序名称,将【proxy_pass】 改为你自己的后台地址
        location /zero-service {
            proxy_pass http://localhost:9099/zero-service;
            proxy_cookie_path / /zero-service;
        }
    }

代码混淆

pnpm add terser -D

export default defineConfig({
  // 打包配置
  build: {
    chunkSizeWarningLimit: 500, // hunk 大小警告的限制(以 kbs 为单位)
    minify: "terser", // 代码混淆  boolean | 'terser' | 'esbuild' ,当设置为 'terser' 时必须先安装 Terser pnpm add terser -D
  },
});

去除生产环境 console

export default defineConfig({
  // 打包配置
  build: {
    terserOptions: {
      compress: {
        // warnings: false,
        drop_console: true, // 打包时删除console
        drop_debugger: true, // 打包时删除 debugger
        pure_funcs: ["console.log", "console.warn"],
      },
      output: {
        comments: true, // 去掉注释内容
      },
    },
  },
});

打包体积分析插件

  1. 安装 pnpm add rollup-plugin-visualizer -D
  2. vite.config.ts 配置

传统浏览器兼容性支持

  1. 安装 pnpm add @vitejs/plugin-legacy -D
  2. 在 vite.config.ts 中配置
import legacyPlugin from "@vitejs/plugin-legacy";
export default ({ command, mode }: ConfigEnv): UserConfig => {
  return {
    plugins: [
      legacyPlugin({
        targets: ["chrome 52"], // 需要兼容的目标列表,可以设置多个
        // additionalLegacyPolyfills: ["regenerator-runtime/runtime"], // 面向IE11时需要此插件
      }),
    ],
  };
};
  1. 添加传统浏览器兼容性支持,打包后在 dist 文件夹下 index.html 文件中确认 转存失败,建议直接上传图片文件

CDN 内容分发网络(Content Delivery Network)

  1. 插件安装pnpm add vite-plugin-cdn-import -D -w
  2. vite.config.ts 配置

Vitest 单元测试

vitest 参考文章:juejin.cn/post/714837…

  1. Vitest 测试已经在项目初始化的时候添加
  2. vue 组件测试 pnpm add @vue/test-utils -D
  3. 测试规则,添加查看 vite.config.ts 文件
  4. 编写 vue 组件
<template>
  <div>
    <div>Count: {{ count }}</div>
    <div>name: {{ props.name }}</div>
    <h2 class="msg">{{ msg }}</h2>
    <button @click="handle">点击事件</button>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";

const props = defineProps({
  name: {
    type: String,
    default: "1111",
  },
});

const count = ref<number>(0);

let msg = ref<string>("hello");

const handle = () => {
  count.value++;
};

onMounted(() => {
  console.log("props.message==", props.name);
});
</script>

<style scoped lang="scss"></style>
  1. 编写测试文件
import { test, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Count from "../index.vue";

test.concurrent("基础js测试", () => {
  expect(1 + 1).toBe(2);
});

const Component = {
  template: "<div>44Hello world</div>",
};

// mount 的第二个参数,可以传一些配置项,比如props。这在测试组件时,很好用
test("mounts a component", () => {
  const wrapper = mount(Component, {});

  expect(wrapper.html()).toContain("Hello world");
});

// 测试 props 组件传参
test("测试 props 组件传参", () => {
  // 测试props 传参
  const wrapper = mount(Count, {
    props: {
      name: "Hello world",
    },
  });
  expect(wrapper.text()).toContain("Hello world");
  // 测试 ref指定初始值
  expect(wrapper.vm.count).toBe(0);
  // 测试点击事件
  const button = wrapper.find("button");
  button.trigger("click");
  expect(wrapper.vm.count).toBe(1);
  // 测试msg渲染
  expect(wrapper.find(".msg").text()).toBe("hello");
});
  1. 安装 vscode 插件,配置测试 debug 环境

    • 插件商店搜索 Vitest 安装
    • 点击 debug 选择 node 配置 .vscode 文件夹下 launch.json 文件
    {
      // Use IntelliSense to learn about possible attributes.
      // Hover to view descriptions of existing attributes.
      // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
      "version": "0.2.0",
      "configurations": [
        {
          "type": "node",
          "request": "launch",
          "name": "Debug Current Test File",
          "autoAttachChildProcesses": true,
          "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
          "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
          "args": ["run", "${relativeFile}"],
          "smartStep": true,
          "console": "integratedTerminal"
        }
      ]
    }
    
  2. 点击 debug 运行 转存失败,建议直接上传图片文件转存失败,建议直接上传图片文件

集成打印插件

  1. 官网:printjs.crabbly.com/#documentat…
  2. 安装 pnpm add print-js

配置 .env

  1. vite.config.ts 读取
const root = process.cwd();
const env = loadEnv(process.argv[process.argv.length - 1], root);
// 读取值 env.VITE_APP_SERVICE;
  1. ts 文件读取import.meta.env.VITE_APP_TITLE

import.meta.glob 批量导入文件夹下文件

function addRouter(list) {
  const modules = import.meta.glob("./views/**.vue");
  for (const path in modules) {
    modules[path]().then((mod) => {
      const file = mod.default;
      if (list.map((a) => a.name).includes(file.name)) {
        router.addRoute({
          path: "/" + file.name,
          name: file.name,
          component: file,
        });
      }
    });
  }
}

解决 vite 首屏加载缓慢问题

参考文章:

采用判断是否为生产环境,生产环境打包,自动加载,开发环境全局引入,修改 vite.config.ts 文件

export default ({ command, mode }: ConfigEnv): UserConfig => {
  // 读取环境变量配置
  const root = process.cwd();
  const env = loadEnv(process.argv[process.argv.length - 1], root);
  // 判断是否为打包环境
  const isBuild = command === "build";

  return {
    plugins: [
      vue(),
      vueJsx(),
      // ant design vue 按需加载
      Components({
        resolvers: [
          AntDesignVueResolver({ importStyle: isBuild ? "less" : false }),
        ],
      }),
    ],
  };
};

全局引入在 main.ts 文件中,目前未实现环境变量的判断,如需打包,请手动注释掉全局引入的 ant-design-vue 样式

// 生产环境下,注释掉下面的全局样式引入
import "ant-design-vue/dist/antd.less";

利用 plop,自定义脚手架

Plop 是一个小而美的脚手架工具,它主要用于创建项目中特定类型的文件,Plop 主要集成在项目中使用,帮助我们快速生成一定规范的初始模板文件

  1. 安装 pnpm add plop -D
  2. 在项目根目录下创建 plopfile.js 文件
#!/usr/bin/env node
import componentsSetting from "./plop-templates/components/prompt.js";
import pageSetting from "./plop-templates/pages/prompt.js";

export default function (plop) {
  plop.setWelcomeMessage("请选择需要创建的模式:");
  plop.setGenerator("components", componentsSetting);
  plop.setGenerator("page", pageSetting);
}
  1. 在项目根目录下创建plop-templates文件夹

    • 新建 components 文件夹,添加文件prompt.js指令文件和index.hbs模板文件
    • prompt.js 指令内容
    import fs from "fs";
    function getFolder(path) {
      const components = [];
      const files = fs.readdirSync(path);
      files.forEach((item) => {
        const stat = fs.lstatSync(`${path}/${item}`);
        if (stat.isDirectory() === true && item !== "components") {
          components.push(`${path}/${item}`);
          components.push(...getFolder(`${path}/${item}`));
        }
      });
      return components;
    }
    
    const componentsSetting = {
      description: "创建组件",
      // 提示数组
      prompts: [
        {
          type: "confirm",
          name: "isGlobal",
          message: "是否为全局组件",
          default: false,
        },
        {
          type: "list",
          name: "path",
          message: "请选择组件创建目录",
          choices: getFolder("src/components"),
          when: (answers) => {
            return !answers.isGlobal;
          },
        },
        {
          type: "input",
          name: "name",
          message: "请输入组件名称",
          validate: (v) => {
            if (!v || v.trim === "") {
              return "组件名称不能为空";
            } else {
              return true;
            }
          },
        },
      ],
      // 行为数组
      actions: (data) => {
        let path = "";
        if (data.isGlobal) {
          path = "src/components/{{properCase name}}/index.vue";
        } else {
          path = `${data.path}/components/{{properCase name}}/index.vue`;
        }
        const actions = [
          {
            type: "add",
            path,
            templateFile: "plop-templates/components/index.hbs",
          },
        ];
        return actions;
      },
    };
    
    export default componentsSetting;
    
    • index.hbs 模板内容
    <template>
      <div>
        <!-- 布局 -->
      </div>
    </template>
    
    <script lang="ts" setup{{#if isGlobal}} name="{{ properCase name }}"{{/if}}>
    // 逻辑代码
    </script>
    
    <style lang="scss" scoped>
    // 样式
    </style>
    
  2. 在项目 package.json 添加 "cli": "plop", 命令


"scripts": {
    "cli": "plop",
  },
  1. 通过 pnpm cli 选择创建项目代码模板 转存失败,建议直接上传图片文件

集成 mockjs 模拟后台接口

  1. 安装依赖

    • pnpm add mockjs
    • pnpm add @types/mockjs -D
    • pnpm add vite-plugin-mock -D
  2. 配置 vite.config.ts 文件

    • 引入插件 import { viteMockServe } from "vite-plugin-mock";
    • 在 数组中进行配置
    export default ({ command, mode }: ConfigEnv): UserConfig => {
      // 读取环境变量配置
      const root = process.cwd();
      const env = loadEnv(process.argv[process.argv.length - 1], root);
    
      const isBuild = command === "build";
    
      return {
        plugins: [
          //....
          viteMockServe({
            mockPath: "src/mock",
            localEnabled: !isBuild,
            prodEnabled: isBuild,
            injectCode: `
          import { setupProdMockServer } from './mockProdServer';
          setupProdMockServer();
          `,
          }),
        ],
      };
    };
    
  3. 新建 src\mockProdServer.ts 文件与 main.ts 文件同级

import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";

const mocks: any[] = [];
const mockContext = import.meta.glob("./mock/*.ts", {
  eager: true,
});
Object.keys(mockContext).forEach((v) => {
  mocks.push(...(mockContext[v] as any).default);
});

export function setupProdMockServer() {
  createProdMockServer(mocks);
}
  1. 新建 src\mock 文件夹,mock 文件下,新建业务模块 login.ts
export default [
  {
    url: "/api/sys/login", // 模拟登录接口
    method: "POST", // 请求方式
    timeout: 3000, // 超时事件
    statusCode: 200, // 返回的http状态码
    response: (option: any) => {
      // 返回的结果集
      return {
        code: 200,
        message: "登录成功",
        data: {
          failure_time: Math.ceil(new Date().getTime() / 1000) + 24 * 60 * 60,
          account: option.body.account,
          token: "@string",
        },
      };
    },
  },
];
  1. 利用封装的 api 调用 /api/sys/login 接口
postAction("/sys/login", { userName: userName, password: password }).then(
  (res: any) => {}
);

自定义按钮权限指令

  1. 在 directive 文件夹下,新建 permission.ts 文件。添加权限指令代码
// 引入vue中定义的指令对应的类型定义
import type { Directive } from "vue";
const permission: Directive = {
  // mounted是指令的一个生命周期
  mounted(el, binding) {
    // value 获取用户使用自定义指令绑定的内容
    const { value } = binding;
    // 获取用户所有的权限按钮
    // const permissionBtn: any = sessionStorage.getItem("permission");
    const permissionBtn: any = ["admin", "dashboard.admin"];
    // 判断用户使用自定义指令,是否使用正确了
    if (value && value instanceof Array && value.length > 0) {
      const permissionFunc = value;
      //判断传递进来的按钮权限,用户是否拥有
      //Array.some(), 数组中有一个结果是true返回true,剩下的元素不会再检测
      const hasPermission = permissionBtn.some((role: any) => {
        return permissionFunc.includes(role);
      });
      // 当用户没有这个按钮权限时,返回false,使用自定义指令的钩子函数,操作dom元素删除该节点
      if (!hasPermission) {
        // el.style.display = "none";
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
      throw new Error(`传入关于权限的数组,如 v-permission="['admin','user']"`);
    }
  },
};

export default permission;
  1. 在 directive 文件夹下,新建 index.ts 批量注册指令
import type { Directive } from "vue";
import permission from "./permission";
// 自定义指令
const directives = { permission };

export default {
  install(app: any) {
    Object.keys(directives).forEach((key) => {
      // Object.keys() 返回一个数组,值是所有可遍历属性的key名
      app.directive(key, (directives as { [key: string]: Directive })[key]); //key是自定义指令名字;后面应该是自定义指令的值,值类型是string
    });
  },
};
  1. 在 main.ts 文件引入,注册自定义指令
import { createApp } from "vue";
import App from "./App.vue";
import directive from "./directive";

const app = createApp(App);
app.use(directive);

app.mount("#app");

处理 px 转 rem,和 css 自动添加浏览器前缀

  1. 安装pnpm add postcss-pxtorem autoprefixer -D
  2. vite.config.ts 配置
import postCssPxToRem from "postcss-pxtorem";
import autoprefixer from "autoprefixer";
export default ({ command, mode }: ConfigEnv): UserConfig => {
  return {
    css: {
      postcss: {
        plugins: [
          postCssPxToRem({
            // 自适应,px>rem转换
            rootValue: 16, // 1rem的大小
            propList: ["*"], // 需要转换的属性,这里选择全部都进行转换
          }),
          autoprefixer({
            // 自动添加前缀
            overrideBrowserslist: [
              "Android 4.1",
              "iOS 7.1",
              "Chrome > 31",
              "ff > 31",
              "ie >= 8",
              //'last 2 versions', // 所有主流浏览器最近2个版本
            ],
            grid: true,
          }),
        ],
      },
    },
  };
};

Monorepo

Monorepo 是一种项目管理方式,就是把多个项目放在一个仓库里面 juejin.cn/post/696432…

  1. 项目根目录新建 pnpm-workspace.yaml 文件
packages:
  # all packages in subdirs of packages/ and components/
  - "packages/**"
  1. @zero-admin/utils 安装 到 @zero-admin/chart 执行命令pnpm i @zero-admin/utils -r --filter @zero-admin/chart
  2. @zero-admin/chart 安装到根项目 package.json 文件中,执行命令 pnpm i @zero-admin/chart -w

图像编辑器

流程图

安装依赖
  1. 流程图核心包pnpm add @logicflow/core -w
  2. 流程图扩展包pnpm add @logicflow/extension -w
  3. 格式化展示 json 数据 pnpm add vue-json-pretty -w
初始化容器及 LogicFlow 对象
准备容器

国际化

  1. 安装pnpm add vue-i18n

读取 makdown 文档,编写组件说明文档

  1. 安装依赖 pnpm add @kangc/v-md-editor@next -D pnpm add prismjs -S pnpm add @types/prismjs -D
  2. 在 setting 文件夹下新建 mdEditor.ts 文件
import VueMarkdownEditor from "@kangc/v-md-editor";
import "@kangc/v-md-editor/lib/style/base-editor.css";
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";

import Prism from "prismjs";

VueMarkdownEditor.use(vuepressTheme, {
  Prism,
});
export default VueMarkdownEditor;
  1. 在 main.ts 文件中引入挂载
import { createApp } from "vue";
import App from "./App.vue";

import VueMarkdownEditor from "@/setting/mdEditor";
const app = createApp(App);

app.use(VueMarkdownEditor);
app.mount("#app");
  1. 在组件中使用
<template>
  <v-md-editor
    v-model="markdownTable"
    height="calc(100vh - 293px)"
    mode="preview"
  ></v-md-editor>
</template>
<script setup lang="ts">
  import markdownTable from "./README.md?raw";
</script>

将资源引入为字符串:资源可以使用 ?raw 后缀声明作为字符串引入 官网:ckang1229.gitee.io/vue-markdow…

maptalks + threejs demo 示例

  1. 项目依赖 pnpm add three maptalks maptalks.three --save
  2. Demo 源码文件:文件路径: src\views\charts\smartCity.vue
  3. 访问 Demo
    • 启动项目 pnpm dev
    • 浏览器访问路径 http://localhost:3030/city

使用 commitizen 规范 git 提交

  1. 安装依赖 pnpm install commitizen @commitlint/config-conventional @commitlint/cli commitlint-config-cz cz-git -D

  2. 配置 package.json

{
  ...
  "scripts": {
    "git:comment": "引导设置规范化的提交信息",
    "git": "git pull && git add . && git-cz && git push",
  },

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "src/**/*.{js,ts,vue}": [
      "prettier --write --ignore-unknown --no-error-on-unmatched-pattern",
      "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
    ],
    "package.json": [
      "prettier --write"
    ]
  },
  "config": {
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  }
  ...
}
  1. 项目根目录新建 commitlint.config.js 添加配置
module.exports = {
  // 继承的规则
  extends: ["@commitlint/config-conventional", "cz"],
  // 定义规则类型
  rules: {
    // type 类型定义,表示 git 提交的 type 必须在以下类型范围内
    "type-enum": [
      2,
      "always",
      [
        "feature", // 新功能(feature)
        "bug", // 此项特别针对bug号,用于向测试反馈bug列表的bug修改情况
        "fix", // 修补bug
        "ui", // 更新 ui
        "docs", // 文档(documentation)
        "style", // 格式(不影响代码运行的变动)
        "perf", // 性能优化
        "release", // 发布
        "deploy", // 部署
        "refactor", // 重构(即不是新增功能,也不是修改bug的代码变动)
        "test", // 增加测试
        "chore", // 构建过程或辅助工具的变动
        "revert", // feat(pencil): add ‘graphiteWidth’ option (撤销之前的commit)
        "merge", // 合并分支, 例如: merge(前端页面): feature-xxxx修改线程地址
        "build", // 打包
      ],
    ],
    // <type> 格式 小写
    "type-case": [2, "always", "lower-case"],
    // <type> 不能为空
    "type-empty": [2, "never"],
    // <scope> 范围不能为空
    "scope-empty": [2, "never"],
    // <scope> 范围格式
    "scope-case": [0],
    // <subject> 主要 message 不能为空
    "subject-empty": [2, "never"],
    // <subject> 以什么为结束标志,禁用
    "subject-full-stop": [0, "never"],
    // <subject> 格式,禁用
    "subject-case": [0, "never"],
    // <body> 以空行开头
    "body-leading-blank": [1, "always"],
    "header-max-length": [0, "always", 72],
  },
  prompt: {
    alias: { fd: "docs: fix typos" },
    messages: {
      type: "选择你要提交的类型 :",
      scope: "选择一个提交范围(可选):",
      customScope: "请输入自定义的提交范围 :",
      subject: "填写简短精炼的变更描述 :\n",
      body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
      breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
      footerPrefixesSelect: "选择关联issue前缀(可选):",
      customFooterPrefix: "输入自定义issue前缀 :",
      footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
      confirmCommit: "是否提交或修改commit ?(y/n/e/h)",
    },
    types: [
      { value: "feat", name: "feat:     新增功能 | A new feature" },
      { value: "fix", name: "fix:      修复缺陷 | A bug fix" },
      {
        value: "docs",
        name: "docs:     文档更新 | Documentation only changes",
      },
      {
        value: "style",
        name: "style:    代码格式 | Changes that do not affect the meaning of the code",
      },
      {
        value: "refactor",
        name: "refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature",
      },
      {
        value: "perf",
        name: "perf:     性能提升 | A code change that improves performance",
      },
      {
        value: "test",
        name: "test:     测试相关 | Adding missing tests or correcting existing tests",
      },
      {
        value: "build",
        name: "build:    构建相关 | Changes that affect the build system or external dependencies",
      },
      {
        value: "ci",
        name: "ci:       持续集成 | Changes to our CI configuration files and scripts",
      },
      { value: "revert", name: "revert:   回退代码 | Revert to a commit" },
      {
        value: "chore",
        name: "chore:    其他修改 | Other changes that do not modify src or test files",
      },
    ],
    allowCustomScopes: true,
    skipQuestions: ["body", "footer"],
  },
};

Git hooks 工具

vue3 使用 husky + commitlint 强制码提交规范

  1. 安装依赖 pnpm add lint-staged husky -D -w
  2. 添加 package.json 脚本
"prepare": "husky install"
  1. 初始化 husky 将 git hooks 钩子交由 husky 执行pnpm run prepare
  2. npx husky add .husky/pre-commit "pnpm run eslint"
  3. pnpm husky add .husky/commit-msg 'pnpm commitlint --edit $1'

git 使用命令

  1. 克隆远程仓库代码 git clone https://gitee.com/zmmlet/zero-admin.git

  2. 第 1 步:同步远程仓库代码:git pull git add / git commit 代码之前首先 git pull,需先从服务器上面拉取代码,以防覆盖别人代码;如果有冲突,先备份自己的代码,git checkout 下远程库里最新的的代码,将自己的代码合并进去,然后再提交代码。

  3. 第 2 步:查看当前状态:git status 使用 git status 来查看当前状态,红色的字体显示的就是你修改的文件

  4. 第 3 步:提交代码到本地 git 缓存区:git add 情形一:如果你 git status 查看了当前状态发现都是你修改过的文件,都要提交,那么你可以直接使用 git add . 就可以把你的内容全部添加到本地 git 缓存区中 情形二:如果你 git status 查看了当前状态发现有部分文件你不想提交,那么就使用 git add xxx(上图中的红色文字的文件链接) 就可以提交部分文件到本地 git 缓存区。

  5. 第 4 步:推送代码到本地 git 库:git commit git commit -m "提交代码" 推送修改到本地 git 库中

  6. 第 5 步:提交本地代码到远程仓库:git push git push <远程主机名> <远程分支名> 把当前提交到 git 本地仓库的代码推送到远程主机的某个远程分之上

技术栈

❌
❌