开源仓库地址 giteeGit仓库地址:https://gitee.com/zhanhongzhu/zhanhongzhu.git应用地址windows应用地址下载 https://kestrel-task.cn具体内容也可以看🎉使用Tauri+vite+koa2+mysql开发了一款待办效率应用 这篇文章。💻技术栈Tauri: Tauri是一个用于构建现代桌面应用程序的工具,结合了Rust、Vue.js和Web技术,提供了强大的跨平台能力。Vue3: Vue3是流行的JavaScript框架Vue.js的最新版本,具有更好的性能、更好的TypeScript支持和更多的特性。Vite5: Vite是一个现代化的构建工具,Vite5是其最新版本,具有快速的冷启动、热模块替换和原生ES模块支持。Koa2: Koa2是一个基于Node.js的轻量级Web框架,使用异步函数处理中间件,提供了简洁而强大的Web开发体验。MySQL: MySQL是一个流行的关系型数据库管理系统,具有高性能、可靠性和广泛的应用领域,适用于各种规模的应用程序。我的待办快速添加待办任务,快速查看任务进度,摘要等。新增标签,分类,更好管理待办任务。通过标签、分类筛选待办任务,方便快捷。
图片

图片
OKR目标管理我的想法是通过OKR管理系列的任务,这样每完成一个小任务,就可以关闭一个小任务,直观又方便,等到所有关键的小任务都完成了,整个任务也就完成了。
图片
番茄工作法主要是一个计时的时钟,可以在专注计时的时候,专注地完成某项任务,快捷方便,使用番茄工作法,选择一个待完成的任务,将番茄时间设为25分钟,也可以选择其他的区间,专注工作,中途不允许做任何与该任务无关的事。时刻保持专注。
图片
日历视图打开日历界面,通过视图的形式查看公历或农历日历下每个日期的待办提醒或任务事项。也可以在日历视图,添加任务。
图片
MEMO快速记录为了更好地帮你捕捉想法与灵感,提供了快速记录的输入框。专注记录想法,无需思考标题和排版。控制记录长度,降低记录压力,快速捕捉。
图片
统计功能展示了每天的待办数,以及每天新增的待办数量。
图片
展示功能点打包发布版本脚本命令 "scripts": {

"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"pub": "cd build && node ./updateVersion.js && pnpm tauri build && node ./publish.js"

}npm run pub更新版本号以及更新publicKey//build/publish.js
import fs from 'fs'
// 读取 tauri.conf.json
const tauriConf = JSON.parse(fs.readFileSync('../src-tauri/tauri.conf.json', 'utf8'));
let newVersion = tauriConf.package.version; //更新的版本号

// 读取 update.json
let updateJson = JSON.parse(fs.readFileSync('update.json', 'utf8'));
// 更新 update.json version
updateJson.version = newVersion;

//获取版本更新的内容
//签名、版本路径、发版日期
const signature = fs.readFileSync(../src-tauri/target/release/bundle/msi/kestrel-task_${newVersion}_x64_zh-CN.msi.zip.sig, 'utf8');
updateJson.platforms['windows-x86_64'].signature = signature;
updateJson.platforms['windows-x86_64'].url = https://kestrel-task.cn/kestrel-task_${newVersion}_x64_zh-CN.msi.zip
updateJson.pub_date = new Date();
fs.writeFileSync('update.json', JSON.stringify(updateJson, null, 2));版本json信息//build/update.json

{
"version": "1.0.8",
"notes": "kestrel-task",
"pub_date": "2024-03-23T04:23:39.799Z",
"platforms": {

"windows-x86_64": {
  "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSNVRCUit5Zjc1Y3JLV085djl6eTMza2NqMXFIV0paNkl2ckgrTGZTRm9wcEJwcUlkaTBhM2hvN3pSVkRUZXlTZ2NSejJremg2Vklja041VkZmdGlZZ0hxTGVVM2xlL3dFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzExMTY3ODE5CWZpbGU6a2VzdHJlbC10YXNrXzEuMC44X3g2NF96aC1DTi5tc2kuemlwCldSWVdwb0dwRU1aQUJ2ckFra2FTMjBkcnZtL0FWU3grd3MzeHZVTDhWRFFFUC9QWkpzdUNvUG9HZXBrVmhWMkoxTkpGc2pkYU5rRHYwcVdHdlk5dkFBPT0K",
  "url": "https://kestrel-task.cn/kestrel-task_1.0.8_x64_zh-CN.msi.zip"
}

}
}
更新tauri.conf.json版本信息// build/updateVersion.js
import fs from 'fs' // 读取 tauri.conf.json
const tauriConf = JSON.parse(fs.readFileSync('../src-tauri/tauri.conf.json', 'utf8'));
let version = tauriConf.package.version; //更新的版本号

//更新版本号
let versionParts = version.split('.').map(Number);
versionParts[2] += 1;
let newVersion =versionParts.join('.');
// 更新 tauri.conf.json version
tauriConf.package.version = newVersion;
fs.writeFileSync('../src-tauri/tauri.conf.json', JSON.stringify(tauriConf, null, 2));使用tauri的http模块此处进行简单的封装,接口请求在控制台无法被查看到。如果觉得不方便,完全可以使用axios库。也是可以的。import { http } from "@tauri-apps/api";

export function request(config) {
return new Promise((resolve, reject) => {

http
  .fetch("https://kestrel-task.cn" + config.url, {
    method: "POST",
    body: http.Body.json(config.data),
    headers: {
      Authorization: token,
    },
  })
  .then((res) => {
    resolve(res.data.data);
  })
  .catch((err) => {
    reject(err);
  });

});
}
http封装get请求export function requestGet(config, d) {
let url = d ? config.url : "https://kestrel-task.cn" + config.url;
return new Promise((resolve, reject) => {

http
  .fetch(url, {
    method: "get",
    headers: {
      Authorization: token,
    },
  })
  .then((res) => {
    resolve(d ? res.data : res.data.data);
  })
  .catch((err) => {
    reject(err);
  });

});
}
使用封装的request函数export const login = (data) => {
return request({

url: "/web/login",
method: "post",
data,

});
};使用WebviewWindow封装公共的窗口import { WebviewWindow } from '@tauri-apps/api/window'
import { emit } from '@tauri-apps/api/event'

// 创建新窗口
export async function createWin(args) {

await emit('win-create', args)

}

// 获取窗口
export async function getWin(label) {

return await WebviewWindow.getByLabel(label)

}

/**

  • @desc 设置窗口
  • @param type {string} 'show'|'hide'|'close'|'min'|'max'|'max2min'|'exit'|'relaunch'
    */

export async function setWin(type) {

await emit('win-' + type)

}

// 登录窗口
export async function loginWin() {

await createWin({
    label: 'Login',
    title: '登录',
    url: '/login',
    width: 320,
    height: 420,
    resizable: false,
    alwaysOnTop: true,
})

}

// .../**

  • @desc 封装新开多窗体
    */

import {
WebviewWindow,
appWindow,
getAll,
getCurrent,
} from "@tauri-apps/api/window";
import { relaunch, exit } from "@tauri-apps/api/process";
import { emit, listen } from "@tauri-apps/api/event";

import { setWin } from "./actions.js";

// 系统参数配置
export const windowConfig = {
label: null, // 窗口唯一label
title: "", // 窗口标题
url: "", // 路由地址url
width: 900, // 窗口宽度
height: 640, // 窗口高度
minWidth: null, // 窗口最小宽度
minHeight: null, // 窗口最小高度
x: null, // 窗口相对于屏幕左侧坐标
y: null, // 窗口相对于屏幕顶端坐标
center: true, // 窗口居中显示
resizable: true, // 是否支持缩放
maximized: false, // 最大化窗口
decorations: true, // 窗口是否无边框及导航条
alwaysOnTop: false, // 置顶窗口
};

class Windows {
constructor() {

this.mainWin = null;

}

// 获取窗口
getWin(label) {

return WebviewWindow.getByLabel(label);

}

// 获取全部窗口
getAllWin() {

return getAll();

}

// 创建新窗口
async createWin(options) {

const args = Object.assign({}, windowConfig, options);

// 判断窗口是否存在
const existWin = getAll().find((w) => w.label == args.label);
if (existWin) {
  if (existWin.label.indexOf("main") == -1) {
    await existWin?.unminimize();
    await existWin?.setFocus();
    return;
  }
  await existWin?.close();
}

// 创建窗口对象
let win = new WebviewWindow(args.label, args);

// 是否最大化
if (args.maximized && args.resizable) {
  win.maximize();
}

// 窗口创建完毕/失败
win.once("tauri://created", async () => {
  console.log("window create success!");
});

win.once("tauri://error", async () => {
  console.log("window create error!");
});

}

// 开启主进程监听事件
async listen() {

// 创建新窗体
await listen("win-create", (event) => {
  this.createWin(JSON.parse(event.payload));
});

// 显示窗体
await listen("win-show", async (event) => {
  if (appWindow.label.indexOf("main") == -1) return;
  await appWindow.show();
  await appWindow.unminimize();
  await appWindow.setFocus();
});

// 隐藏窗体
await listen("win-hide", async (event) => {
  if (appWindow.label.indexOf("main") == -1) return;
  await appWindow.hide();
});

// 退出应用
await listen("win-exit", async (event) => {
  setWin("logout");
  await exit();
});

// 重启应用
await listen("win-relaunch", async (event) => {
  await relaunch();
});

// 主/渲染进程传参
await listen("win-setdata", async (event) => {
  await emit("win-postdata", JSON.parse(event.payload));
});

}
}

export default Windows;
封装Echart组件,便于使用
图片
<template>

<div ref="MyEcharts" :style="{ height: height, width: width }"></div>

</template>

<script>
import * as echarts from 'echarts'
import T from './echarts-theme-T.js'
echarts.registerTheme('T', T)
const unwarp = obj => obj && (obj.__v_raw || obj.valueOf() || obj)
export default {

  ...echarts,
  name: 'Charts',
  props: {
      // 高度
      height: { type: String, default: '100%' },
      // 宽度
      width: { type: String, default: '100%' },
      // 是否无数据
      nodata: { type: Boolean, default: false },
      // 配置项
      option: { type: Object, default: () => {} }
  },
  data() {
      return {
          isActivat: false,
          myChart: null,
          MyEcharts:null
      }
  },
  watch: {
      option: {
          deep: true,
          handler(v) {
              unwarp(this.myChart).setOption(v)
          }
      }
  },
  computed: {
      myOptions: function() {
          return this.option || {}
      }
  },
  activated() {
      if (!this.isActivat) {
          this.$nextTick(() => {
              this.myChart.resize()
          })
      }
  },
  deactivated() {
      this.isActivat = false
  },
  mounted() {
      this.isActivat = true
      this.$nextTick(() => {
          this.draw()
      })
  },
  methods: {
      draw() {
          const myChart = echarts.init(this.$refs.MyEcharts, 'T')
          myChart.setOption(this.myOptions)
          this.myChart = myChart
          window.addEventListener('resize', () => myChart.resize())
      }
  }

}
</script>echart主题模块//echarts-theme-T.js
const T = {

color: ['#409EFF', '#36CE9E', '#f56e6a', '#626c91', '#edb00d', '#909399'], // 颜色数组
grid: { // 网格
    left: '3%', // 左边距
    right: '3%', // 右边距
    bottom: '10', // 下边距
    top: '40', // 上边距
    containLabel: true // 包含标签
},
legend: { // 图例
    textStyle: { // 文本样式
        color: '#999' // 颜色
    },
    inactiveColor: 'rgba(128,128,128,0.4)' // 不活跃颜色
},
categoryAxis: { // 类别轴
    axisLine: { // 轴线
        show: true, // 显示
        lineStyle: { // 线条样式
            color: 'rgba(128,128,128,0.2)', // 颜色
            width: 1 // 宽度
        }
    },
    axisTick: { // 刻度线
        show: false, // 不显示
        lineStyle: { // 线条样式
            color: '#000' // 颜色
        }
    },
    axisLabel: { // 轴标签
        color: '#999' // 颜色
    },
    splitLine: { // 分隔线
        show: false, // 不显示
        lineStyle: { // 线条样式
            color: ['#eee'] // 颜色
        }
    },
    splitArea: { // 分隔区域
        show: false, // 不显示
        areaStyle: { // 区域样式
            color: ['rgba(255,255,255,0.01)', 'rgba(0,0,0,0.01)'] // 颜色
        }
    }
},
valueAxis: { // 数值轴
    axisLine: { // 轴线
        show: false, // 不显示
        lineStyle: { // 线条样式
            color: '#999' // 颜色
        }
    },
    splitLine: { // 分隔线
        show: true, // 显示
        lineStyle: { // 线条样式
            color: 'rgba(128,128,128,0.2)' // 颜色
        }
    }
}

}

export default T封装公共的弹窗组件
图片
<template>
<el-dialog class="my-dialog" draggable v-bind="$attrs" v-model="modelValue" :modal-append-to-body="modalAppendToBody"

:append-to-body="appendToBody" :fullscreen="fullscreen" :close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape" :width="comWidth" :top="top" @closed="closed">
<template v-slot:title>
  <slot name="title">
    <span class="my-dialog-title">{{ dialogTitle || ''}}</span>
  </slot>
</template>
<div v-loading="loading" class="body-content" :style="{'height':comHeight}">
  <slot></slot>
</div>
<template #footer>
  <div class="dialog-footer" v-if="closeBtn">
    <el-button type="close" size="small1" @click="closed">关闭</el-button>
  </div>
</template>

</el-dialog>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps(
{

visible: { type: Boolean, default: false }, // 是否可见
loading: { type: Boolean, default: false }, // 是否加载中
top: { type: String, default: '20vh' }, // 距离顶部的距离
fullscreen: { type: Boolean, default: false }, // 是否全屏
size: { type: String, default: 'big' }, // 大小
width: { type: [Number, String], default: 0 }, // 宽度
height: { type: [Number, String], default: '55vh' }, // 宽度
dialogTitle: { type: String, default: '' }, // 弹出框标题
modalAppendToBody: { type: Boolean, default: false }, // 是否将弹出框插入到body中
appendToBody: { type: Boolean, default: false }, // 是否将内容插入到body中
closeOnClickModal: { type: Boolean, default: false }, // 是否在点击模态框时关闭
closeOnPressEscape: { type: Boolean, default: false }, // 是否在按下ESC键时关闭
dblclickDisabled: { type: Boolean, default: false }, // 是否禁用双击放大
closeBtn: { type: Boolean, default: false }, // 关闭按钮

},
['modelValue']
)

const comWidth = computed(() => {
if (props.size === 'small') {

return props.width || '30%'

} else if (props.size === 'middle') {

return props.width || '40%'

} else if (props.size === 'big') {

return props.width || '60%'

}
return props.width || '40%'
})

const comHeight = computed(() => {
return props.height || '55vh'
})

const emit = defineEmits(['update:modelValue','closed'])
const closed = () => {
emit('update:modelValue')
emit('closed',false)
}
</script>封装ResizeObserver函数主要是监听元素的变化或者窗口的变化。有一部分用到了可以拉伸的左右布局的模块。//directive/index.js
// 监听元素大小变化的指令
const map = new WeakMap();
const ob = new ResizeObserver((entries) => {
for (const entry of entries) {

// 获取dom元素的回调
const handler = map.get(entry.target);
//存在回调函数
if (handler) {
  // 将监听的值给回调函数
  handler({
    width: entry.borderBoxSize[0].inlineSize,
    height: entry.borderBoxSize[0].blockSize,
  });
}

}
});

export const Resize = {
mounted(el, binding) {

//将dom与回调的关系塞入map
map.set(el, binding.value);
//监听el元素的变化
ob.observe(el);

},
unmounted(el) {

//取消监听
ob.unobserve(el);

},
};
const directives = { Resize };

const registerDirective = (app) => {
Object.keys(directives).forEach((key) => {

app.directive(key, directives[key]);

});
};
export default registerDirective;
公共拖曳布局的函数主要是用于左右布局宽度的变化,可以使用鼠标进行拖曳,改变左右盒子的高度。export const useCommon = ()=>{

function setLayoutDrag(dragId) {
    const resize = document.getElementById(dragId)
    let previousElement = resize.previousSibling
    let nextElement = resize.nextSibling
    let previousTag = previousElement.tagName
    let nextTag = nextElement.tagName
    resize.onmousedown = (e) => {
      const startX = e.clientX
      const startY = e.clientY
      let type = ''
      if (previousTag === 'ASIDE' && nextTag === 'MAIN') {
        type = 'ASIDE-MAIN'
      } else if (previousTag === 'MAIN' && nextTag === 'ASIDE') {
        type = 'MAIN-ASIDE'
      } else if (
        (previousTag === 'HEADER' && nextTag === 'MAIN') ||
        (previousTag === 'FOOTER' && nextTag === 'MAIN')
      ) {
        type = 'HEADER-MAIN'
      } else if (
        (previousTag === 'MAIN' && nextTag === 'HEADER') ||
        (previousTag === 'MAIN' && nextTag === 'FOOTER')
      ) {
        type = 'MAIN-HEADER'
      }
      let initWidth = 0,
        initHeight = 0
      if (type === 'ASIDE-MAIN') {
        initWidth = previousElement.clientWidth // 初始位置
      } else if (type === 'MAIN-ASIDE') {
        initWidth = nextElement.clientWidth // 初始位置
      } else if (type === 'HEADER-MAIN') {
        initHeight = previousElement.clientHeight
      } else if (type === 'MAIN-HEADER') {
        initHeight = nextElement.clientHeight
      }
      document.onmousemove = (k) => {
        const endX = k.clientX
        const endY = k.clientY
        let moveLen = endX - startX // 横向移动宽度
        let moveHeight = endY - startY // 纵向移动高度
        switch (type) {
          case 'ASIDE-MAIN':
            let asideMainWidth = initWidth + moveLen
            if (moveLen < 0) {
              // 向左移
              if (asideMainWidth > 400) {
                // 左侧剩90
                previousElement.style.width = asideMainWidth + 'px'
              }
            } else {
              // 向右移动
              if (nextElement.clientWidth > 400) {
                // 右侧剩90
                previousElement.style.width = asideMainWidth + 'px'
              }
            }
            break
          case 'MAIN-ASIDE':
            let mainAsideWidth = initWidth - moveLen
            if (moveLen < 0) {
              // 向左移
              if (previousElement.clientWidth > 400) {
                // 左侧剩90
                nextElement.style.width = mainAsideWidth + 'px'
              }
            } else {
              // 向右移动
              if (mainAsideWidth > 400) {
                nextElement.style.width = mainAsideWidth + 'px'
              }
            }
            break
          case 'HEADER-MAIN': {
            let headerMainHeight = initHeight + moveHeight
            if (moveHeight < 0) {
              // 向上移
              if (headerMainHeight > 60) {
                // 上侧剩90
                previousElement.style.height = headerMainHeight + 'px'
              }
            } else {
              // 向下移动
              if (nextElement.clientHeight > 60) {
                // 下侧剩90
                previousElement.style.height = headerMainHeight + 'px'
              }
            }
            break
          }
          case 'MAIN-HEADER': {
            let mainHeaderHeight = initHeight - moveHeight
            if (moveHeight < 0) {
              // 向上移
              if (previousElement.clientHeight > 60) {
                // 左侧剩90
                nextElement.style.height = mainHeaderHeight + 'px'
              }
            } else {
              // 向下移动
              if (mainHeaderHeight > 60) {
                nextElement.style.height = mainHeaderHeight + 'px'
              }
            }
            break
          }
  
          default:
        }
      }
      document.onmouseup = (evt) => {
        document.onmousemove = null
        document.onmouseup = null
        resize.releaseCapture && resize.releaseCapture()
      }
      resize.setCapture && resize.setCapture()
      return false
    }
  }

  return {
    setLayoutDrag
  }

}公共布局此处的可以自己查看代码。
图片
invoke调用rust函数,关闭splashimport { invoke } from '@tauri-apps/api/tauri'

onMounted(() => {
// window.addEventListener('contextmenu', (e) => e.preventDefault(), false)
document.addEventListener('DOMContentLoaded', () => {

// This will wait for the window to load, but you could
// run this function on whatever trigger you want
setTimeout(() => {
  invoke('close_splashscreen')
}, 1000)

})
})🎉结语 感兴趣的可以试试,有不清楚的问题,关于tauri开发方面的问题,也可以一起交流。欢迎加我:zhan_1337608148。一起成长,一起进步。前端rustvitejavascriptvue3赞3收藏1分享阅读 1.4k发布于 2024-07-24
图片
微芒不朽1.2k 声望1.3k 粉丝关注作者« 上一篇Vite插件开发秘籍:解锁前端开发新姿势,轻松应对挑战!💪下一篇 »Koa2进阶:CMS系统实战开发秘籍:1.为什么选择Koa2?▲引用和评论推荐阅读
图片
Spring Boot起步,CRUD、错误处理与宝塔部署微芒不朽赞 3阅读 9.5k
图片
Vue.js-Vue实例寒青赞 11阅读 3.8k
图片
2025年最新反编译微信小程序的教程及工具TANKING赞 7阅读 1.8k
图片
Flex 布局学习总结(对齐方式)zZ_jie赞 4阅读 3k
图片
Koa+Typescript起手式(空环境) 不用每次玩node都要搭环境了!alwaysVe赞 4阅读 9.4k
图片
你可能不知道的图片加载相关知识夕水赞 4阅读 620
图片
巧用 CSS 实现高频出现的复杂怪状按钮 - 镂空的内凹圆角边框chokcoco赞 3阅读 1.2k1 条评论得票最新
图片
提交评论评论支持部分 Markdown 语法:粗体 斜体 链接 代码 - 列表 > 引用。你还可以使用 @ 来通知其他用户。
图片
苏克yvj:不错不错,感谢分享回复2024-07-28来自四川©2025 微芒不朽除特别声明外,作品采用《署名-非商业性使用-禁止演绎 4.0 国际》进行许可使用 SegmentFault 发布SegmentFault - 凝聚集体智慧,推动技术进步服务协议隐私政策浙ICP备15005796号-2浙公网安备33010602002000号


慷慨的麦片
1 声望0 粉丝