Vue集成 #
一、项目创建 #
1.1 使用脚手架创建 #
bash
# 创建 Vue + Tauri 项目
npm create tauri-app@latest my-vue-app
# 选择配置
✔ Project name · my-vue-app
✔ Choose which language to use for your frontend · TypeScript / JavaScript
✔ Choose your package manager · pnpm
✔ Choose your UI template · Vue
✔ Choose your UI flavor · TypeScript
1.2 项目结构 #
text
my-vue-app/
├── src/
│ ├── components/
│ │ ├── Header.vue
│ │ └── Sidebar.vue
│ ├── composables/
│ │ └── useTauri.ts
│ ├── views/
│ │ ├── Home.vue
│ │ └── Settings.vue
│ ├── App.vue
│ └── main.ts
├── src-tauri/
│ ├── src/
│ │ └── lib.rs
│ ├── Cargo.toml
│ └── tauri.conf.json
├── index.html
├── package.json
├── vite.config.ts
└── tsconfig.json
1.3 安装依赖 #
bash
# 安装 Tauri API
pnpm add @tauri-apps/api
# 安装常用插件
pnpm add @tauri-apps/plugin-shell
pnpm add @tauri-apps/plugin-dialog
pnpm add @tauri-apps/plugin-fs
# 安装 Vue Router
pnpm add vue-router
# 安装 Pinia
pnpm add pinia
二、基础配置 #
2.1 Vite 配置 #
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
clearScreen: false,
server: {
port: 1420,
strictPort: true,
},
envPrefix: ['VITE_', 'TAURI_'],
build: {
target: ['es2021', 'chrome100', 'safari13'],
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
});
2.2 主入口配置 #
typescript
// main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import router from './router';
import App from './App.vue';
import './styles/main.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');
三、组合式函数 #
3.1 命令调用 #
typescript
// composables/useCommand.ts
import { ref } from 'vue';
import { invoke } from '@tauri-apps/api/core';
export function useCommand<T, P = void>(command: string) {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const execute = async (params?: P) => {
loading.value = true;
error.value = null;
try {
const result = await invoke<T>(command, params);
data.value = result;
return result;
} catch (err) {
error.value = String(err);
throw err;
} finally {
loading.value = false;
}
};
return { data, loading, error, execute };
}
3.2 使用命令 #
vue
<script setup lang="ts">
import { onMounted } from 'vue';
import { useCommand } from '../composables/useCommand';
interface User {
id: number;
name: string;
}
const { data: user, loading, error, execute } = useCommand<User>('get_user');
onMounted(() => {
execute({ id: 1 });
});
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else-if="user">
<h2>{{ user.name }}</h2>
</div>
</template>
3.3 事件监听 #
typescript
// composables/useEvent.ts
import { onMounted, onUnmounted } from 'vue';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
export function useEvent<T>(event: string, callback: (payload: T) => void) {
let unlisten: UnlistenFn | null = null;
onMounted(async () => {
unlisten = await listen<T>(event, (e) => {
callback(e.payload);
});
});
onUnmounted(() => {
if (unlisten) {
unlisten();
}
});
}
3.4 窗口状态 #
typescript
// composables/useWindowState.ts
import { ref, onMounted, onUnmounted } from 'vue';
import { getCurrentWindow } from '@tauri-apps/api/window';
export function useWindowState() {
const isMaximized = ref(false);
const window = getCurrentWindow();
let unlisten: (() => void) | null = null;
onMounted(async () => {
isMaximized.value = await window.isMaximized();
unlisten = await window.onMaximized(({ payload }) => {
isMaximized.value = payload;
});
});
onUnmounted(() => {
if (unlisten) {
unlisten();
}
});
const toggleMaximize = async () => {
await window.toggleMaximize();
};
const minimize = async () => {
await window.minimize();
};
const close = async () => {
await window.close();
};
return { isMaximized, toggleMaximize, minimize, close };
}
四、组件开发 #
4.1 标题栏组件 #
vue
<script setup lang="ts">
import { useWindowState } from '../composables/useWindowState';
const { isMaximized, toggleMaximize, minimize, close } = useWindowState();
</script>
<template>
<div class="titlebar" data-tauri-drag-region>
<div class="titlebar-title">My App</div>
<div class="titlebar-controls">
<button @click="minimize" class="titlebar-button">
<svg width="12" height="12" viewBox="0 0 12 12">
<rect y="5" width="12" height="2" />
</svg>
</button>
<button @click="toggleMaximize" class="titlebar-button">
<svg v-if="isMaximized" width="12" height="12" viewBox="0 0 12 12">
<rect x="2" y="4" width="6" height="6" fill="none" stroke="currentColor" />
<path d="M4 4V2h6v6h-2" fill="none" stroke="currentColor" />
</svg>
<svg v-else width="12" height="12" viewBox="0 0 12 12">
<rect x="2" y="2" width="8" height="8" fill="none" stroke="currentColor" />
</svg>
</button>
<button @click="close" class="titlebar-button close">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M1 1l10 10M11 1L1 11" stroke="currentColor" stroke-width="2" />
</svg>
</button>
</div>
</div>
</template>
<style scoped>
.titlebar {
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
background: #1e1e1e;
padding: 0 8px;
user-select: none;
}
.titlebar-title {
color: #fff;
font-size: 13px;
}
.titlebar-controls {
display: flex;
gap: 4px;
}
.titlebar-button {
width: 32px;
height: 24px;
border: none;
background: transparent;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.titlebar-button:hover {
background: rgba(255, 255, 255, 0.1);
}
.titlebar-button.close:hover {
background: #e81123;
}
</style>
4.2 文件选择组件 #
vue
<script setup lang="ts">
import { ref } from 'vue';
import { open } from '@tauri-apps/plugin-dialog';
interface Props {
filters?: { name: string; extensions: string[] }[];
}
const props = withDefaults(defineProps<Props>(), {
filters: () => [{ name: 'All Files', extensions: ['*'] }]
});
const emit = defineEmits<{
(e: 'fileSelected', path: string): void;
}>();
const selectedPath = ref<string | null>(null);
const handleOpen = async () => {
const selected = await open({
multiple: false,
filters: props.filters,
});
if (selected) {
selectedPath.value = selected as string;
emit('fileSelected', selected as string);
}
};
</script>
<template>
<div class="file-picker">
<button @click="handleOpen">选择文件</button>
<span v-if="selectedPath" class="selected-path">
{{ selectedPath }}
</span>
</div>
</template>
4.3 通知组件 #
vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { listen } from '@tauri-apps/api/event';
interface NotificationData {
title: string;
message: string;
type: 'info' | 'success' | 'error';
}
const notification = ref<NotificationData | null>(null);
let timeout: ReturnType<typeof setTimeout> | null = null;
let unlisten: (() => void) | null = null;
onMounted(async () => {
unlisten = await listen<NotificationData>('notification', (event) => {
notification.value = event.payload;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
notification.value = null;
}, 3000);
});
});
onUnmounted(() => {
if (unlisten) {
unlisten();
}
if (timeout) {
clearTimeout(timeout);
}
});
</script>
<template>
<Transition name="fade">
<div v-if="notification" :class="['notification', `notification-${notification.type}`]">
<strong>{{ notification.title }}</strong>
<p>{{ notification.message }}</p>
</div>
</Transition>
</template>
<style scoped>
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.notification-info {
background: #e6f7ff;
border: 1px solid #1890ff;
}
.notification-success {
background: #f6ffed;
border: 1px solid #52c41a;
}
.notification-error {
background: #fff2f0;
border: 1px solid #ff4d4f;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
五、状态管理 #
5.1 Pinia Store #
typescript
// stores/app.ts
import { defineStore } from 'pinia';
import { invoke } from '@tauri-apps/api/core';
interface User {
id: number;
name: string;
}
export const useAppStore = defineStore('app', {
state: () => ({
user: null as User | null,
theme: 'light' as 'light' | 'dark',
}),
actions: {
setUser(user: User | null) {
this.user = user;
},
setTheme(theme: 'light' | 'dark') {
this.theme = theme;
},
async loadUser() {
const user = await invoke<User>('get_current_user');
this.user = user;
},
},
});
5.2 使用 Store #
vue
<script setup lang="ts">
import { onMounted } from 'vue';
import { useAppStore } from '../stores/app';
const store = useAppStore();
onMounted(() => {
store.loadUser();
});
</script>
<template>
<div :class="['app', store.theme]">
<h1 v-if="store.user">Welcome, {{ store.user.name }}</h1>
</div>
</template>
六、路由配置 #
6.1 创建路由 #
typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/settings',
name: 'Settings',
component: () => import('../views/Settings.vue'),
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
6.2 路由视图 #
vue
<template>
<TitleBar />
<nav>
<router-link to="/">Home</router-link>
<router-link to="/settings">Settings</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view />
</template>
七、最佳实践 #
7.1 组件命名 #
text
组件文件:PascalCase
- Header.vue
- FilePicker.vue
组合式函数:camelCase,use 前缀
- useCommand.ts
- useWindowState.ts
7.2 Props 定义 #
vue
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
</script>
7.3 Emits 定义 #
vue
<script setup lang="ts">
interface Emits {
(e: 'update', value: string): void;
(e: 'close'): void;
}
const emit = defineEmits<Emits>();
const handleUpdate = () => {
emit('update', 'new value');
};
</script>
八、总结 #
8.1 核心要点 #
| 要点 | 说明 |
|---|---|
| 组合式函数 | 封装 Tauri API |
| 组件开发 | 使用 script setup |
| 状态管理 | 使用 Pinia |
| 路由配置 | 使用 Vue Router |
8.2 下一步 #
现在你已经掌握了 Vue 集成,接下来让我们学习 Svelte集成,了解如何在 Tauri 中使用 Svelte 框架!
最后更新:2026-03-28