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