快速入门Tauri开发桌面端应用

2,520 阅读3分钟

快速入门Tauri开发桌面端应用

最近发现自己工作状况愈发杂乱无章,时而奋笔疾书口干舌燥,时而思考半天苦于没有思路,时而发现有趣的文章一看再看不可自拔导致时间挥霍无度,总之就是时间观念日渐模糊,急需一款时间工具改善自身状况。

一、需求分析

首先解决自己的痛点,根据工作状态分析,初版需要的功能如下:

  • 倒计时提醒功能、自定义倒计时时间、包括开始、暂停、重置
  • 放置桌面顶层、能够时刻关注时间进度
  • 半透明模式、至于顶层尽量不遮挡其他应用

二、技术调研

作为一名前端工程师,尽量使用学习成本低的语言入手,桌面端的开发适合前端开发的框架有Electron和新出的Tauri,鉴于开发的是款小工具,所以选择的了打包更轻量的Tauri框架,事实证明确实很小,只有几M大小,为电脑磁盘并不富裕(原神占了很大部分)的我节省下了很大的空间

三、项目准备

Tauri中文官网地址

预先准备

window环境准备

MacOS环境准备

快速开始

npm create tauri-app

根据需要选择用vite作为前端构建工具,然后根据命令提示输入项目名称,选择包管理工具(npm,pnpm,yarn...),选择前端框架(vue,react....),我这边选择的是vue+ts,最后生成目录结构如下

如果你的应用用不到系统API的话,就跟开发WEB应用类似,打包的时候会编译成桌面应用

四、项目开发

项目配置

为了方便项目开发,我在项目中集成了包括ElementPlusWindicssIconify等方便组建库,快速编写样式和引入图标,具体vite.config.ts配置如下

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

import WindiCSS from 'vite-plugin-windicss'

// 图标
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // elementUI组件自动导入
    AutoImport({
      dts:'./auto-imports.d.ts',
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      dts:'./components.d.ts',
      resolvers: [ElementPlusResolver(),IconsResolver()],
    }),
    // 样式封装方便使用
    WindiCSS(),
    Icons({
      compiler'vue3',
      autoInstalltrue,
    }),
  ],

  // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
  // prevent vite from obscuring rust errors
  clearScreenfalse,
  // tauri expects a fixed port, fail if that port is not available
  server: {
    port1420,
    strictPorttrue,
  },
  // to make use of `TAURI_DEBUG` and other env variables
  // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
  envPrefix: ["VITE_""TAURI_"],
  build: {
    // Tauri supports es2021
    target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
    // don't minify for debug builds
    minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
    // produce sourcemaps for debug builds
    sourcemap: !!process.env.TAURI_DEBUG,
  },
});

代码编写

核心功能组件编写clock.vue

<template>
    <div class="w-250px h-250px m-auto h-300" :class="{'bg-white':!state.isDark}" @contextmenu.prevent
    >
        <div class="m-auto  flex justify-between items-center pl-2 pt-2 pr-2" >
            <div class="flex items-center">
                <div class="flex items-center">
                    <Rank class="cursor-pointer" style="width: 1.4em; height: 1.4em;color:#4194f8;" data-tauri-drag-region />
                </div>
                <div class="flex items-center">
                    <el-switch
                    v-model="state.isDark"
                    class="ml-2"
                    inline-prompt
                    :active-icon="Sunny"
                    :inactive-icon="Moon"
                    @change="changeTop"
                />
                </div>
            </div>
            <div class="flex items-center cursor-pointer">
                <Close style="width: 1.4em; height: 1.4em;color:#4194f8" @click="closeWindow" />
            </div>
            
        </div>
        <div class="flex flex-col">
            <div class="mx-auto cursor-pointer select-none" @click="miniStop">
                <el-progress type="circle" :width="110" :stroke-width="12" :percentage="state.progress" :color="state.colors">
                    <div>
                        <div v-if="state.isDark && state.status!=='running'">
                            <el-icon size="30" color="#3772d3">
                                 <i-carbon-continue-filled />
                            </el-icon>
                        </div>
                        <div v-else class="text-blue-500 text-xl font-bold flex justify-center items-center m-auto" >
                            <span>
                                {{ state.down_minute>9?state.down_minute:'0'+state.down_minute }}
                            </span>
                            <div class="spec-circle-box">
                                <span class="spec-circle"></span>
                                <span class="spec-circle"></span>
                            </div>
                            <span>
                                {{ endTime }}
                            </span>
                        </div>
                    </div>
                </el-progress>
            </div>
            <transition name="el-fade-in">
                <div class="flex flex-col mx-auto mt-2" v-if="!state.isDark">
                    <div class="mx-auto flex justify-center">
                        <el-button-group>
                            <el-tooltip
                                effect="dark"
                                content="开始/继续"
                                placement="bottom"
                            >
                                <el-button type="primary" @click="confirmRun">
                                    <i-carbon-continue-filled />
                                </el-button>
                            </el-tooltip>
                            <el-tooltip
                                effect="dark"
                                content="暂停"
                                placement="top"
                            >
                                <el-button type="primary" @click="ClockPause">
                                    <i-clarity-pause-solid />
                                </el-button>
                            </el-tooltip>
                            <el-tooltip
                                effect="dark"
                                content="结束"
                                placement="bottom"
                            >
                                <el-button type="primary" @click="ClockStop">
                                    <i-carbon-stop-filled-alt />
                                </el-button>
                            </el-tooltip>
                        </el-button-group>
                    </div>
                    <div class="w-30 mt-3 mx-auto flex justify-center">
                        <el-input-number :disabled="state.status !== 'default'" placeholder="分钟数" v-model="state.number" />
                    </div>
                </div>
            </transition>
        </div>
        <audio class="hidden " ref="audio" src="./audio.mp3"></audio>
    </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, computed } from "vue"
import { ElMessageBox } from 'element-plus'
import { appWindow } from '@tauri-apps/api/window';
import { RankSunnyMoonClose } from "@element-plus/icons-vue"
import type { Action } from 'element-plus'
import { exit } from '@tauri-apps/api/process'

interface State{
    progress:number
    number:number
    time:string
    colors:Color[]
    timer:any
    progress_time:number
    sum_time:number
    down_minute:number,
    down_second:number,
    isDark: boolean
    // 状态 默认、运行中、暂停
    status'default' | 'running' | 'pause' 
}
interface Color{
    color:string
    percentage:number
}

const audio = ref<HTMLAudioElement | null>(null)

const state = reactive<State>({
    isDark:false,
    progress:0,
    number:25,
    time:'',
    colors:[
        { color'#6f7ad3'percentage20 },
        { color'#1989fa'percentage40 },
        { color'#5cb87a'percentage60 },
        { color:'#e6a23c' , percentage80 },
        { color:'#f56c6c' , percentage100 },
    ],
    timer:null,
    progress_time:0,//进行多少时间
    sum_time:0,
    down_minute:25,
    down_second:0,
    status'default'
})

const endTime = computed(() => {
    return state.down_second>9?state.down_second:'0'+state.down_second 
})

// 改变窗口置顶
const changeTop = async ()=>{
    if(state.isDark){
       await appWindow.setAlwaysOnTop(true);
    }else{
       await appWindow.setAlwaysOnTop(false);
    }
}


const miniStop = ()=>{
    if (state.isDark) {
        if (state.status === 'default' || state.status === 'pause') {
            ClockRun()
            return
        }
        if (state.status === 'running') {
            ClockPause()
            return
        }
    }
}

const closeWindow =  ()=>{
    ElMessageBox.confirm('确认退出应用程序么?''提示', {
        confirmButtonText'退出',
        cancelButtonText'最小化托盘',
        distinguishCancelAndClose:true,
        callback(action: Action) => {
            if(action === 'confirm'){
                exit()
            }
            if(action === 'cancel'){
                appWindow.hide()
            }
        },
    })
}


// 倒计时停止
const ClockStop = () => {
    if (state.status === 'pause' || state.status == 'running') {
        state.status = 'default'
        clearInterval(state.timer)
        state.timer = null
        state.number = 25
        state.progress_time = 0//进行多少时间
        state.progress = 0
        state.sum_time = 0
        state.down_minute = 25
        state.down_second = 0
    }
}

// 倒计时暂停
const ClockPause = () => {
    if (state.status === 'running') {
        state.status = 'pause'
        clearInterval(state.timer)
        state.timer = null
    }
}
// 倒计时开始
const ClockRun = () => {
    if (state.status === 'default' || state.status === 'pause') {
        // 开始运行
        state.status = 'running'
         // 总共多少秒
        state.sum_time = state.number*60
    
        state.timer = setInterval(()=>{
            // 当前进行多少时间
            state.progress_time++
            state.down_second = (state.sum_time - state.progress_time)%60
            state.down_minute = Math.floor((state.sum_time - state.progress_time)/60)
            // 计算百分比
            state.progress = (state.progress_time/state.sum_time)*100
            if(state.progress >= 100 || state.progress_time >= state.sum_time){
                ElMessageBox.alert('休息一下吧''提示', {
                    confirmButtonText'确认',
                    callback(action: Action) => {
                        
                    },
                })
                playMusic()
                ClockStop()
            }
        },1000)
    }
    
}
// 判断是否可以开始
const confirmRun = ()=>{
    if(state.number<=0){
        ElMessageBox.alert('请输入正确的分钟数''提示', {
            // if you want to disable its autofocus
            // autofocus: false,
            confirmButtonText'确认',
            callback(action: Action) => {
                // ElMessage({
                //     type: 'info',
                //     message: `action: ${action}`,
                // })
            },
        })
        return
    }
    ClockRun()
    
}

const playMusic = ()=>{
    audio.value?.play()
}

onMounted(()=>{
})
</script>
<style scoped>
.spec-circle-box{
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;
    width8px;
    height10px;
}
.spec-circle{
    width:4px;
    height4px;
    border-radius50%;
    background#3b82f6;
}
</style>

桌面端配置

  • 桌面端配置文件tauri.conf.json
  • 必须要修改的配置有identifier,打包发布需要包名
  • 修改icon桌面图标,Tauri提供了工具生成多类型图标,根目录下放置./app-icon.png,运行npm run tauri icon即可生成多种适配图标 工具链接
  • 其他修改,如默认窗口大小、标题、是否居中等配置,参考配置链接

代码打包

npm run tauri build

由于国内环境原因,打包很可能会遇到卡住不动的问题,如下图

解决办法如下

根据提示Downloading的链接下载该压缩包,下载完后,在本机 C:\Users\xxxxxxxx\AppData\Loca 中,创建 tauri/WixTools 文件夹,然后把内容解压到里面就可以了,解压后如下图,然后重新运行打包命令即可

项目截图

项目地址

项目地址如下,欢迎大家star

https://gitee.com/q-_-p/focus-clock

欢迎关注我的公众号“总有BUG想害朕”,原创技术文章第一时间推送。