🍋我用Tauri开发的待办效率工具开源了!

开源仓库地址 gitee

Git仓库地址: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


import fs from 'fs'

const tauriConf = JSON.parse(fs.readFileSync('../src-tauri/tauri.conf.json', 'utf8'));
let newVersion = tauriConf.package.version; 


let updateJson = JSON.parse(fs.readFileSync('update.json', 'utf8'));

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信息



{
  "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版本信息


import fs from 'fs' 
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('.');

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)
}


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,
    })
}




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, 
  title: "", 
  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组件,便于使用


  
  <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主题模块


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

封装公共的弹窗组件


<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 }, 
    appendToBody: { type: Boolean, default: false }, 
    closeOnClickModal: { type: Boolean, default: false }, 
    closeOnPressEscape: { type: Boolean, default: false }, 
    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函数

主要是监听元素的变化或者窗口的变化。有一部分用到了可以拉伸的左右布局的模块。



const map = new WeakMap();
const ob = new ResizeObserver((entries) => {
  for (const entry of entries) {
    
    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) {
    
    map.set(el, binding.value);
    
    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) {
                    
                    previousElement.style.width = asideMainWidth + 'px'
                  }
                } else {
                  
                  if (nextElement.clientWidth > 400) {
                    
                    previousElement.style.width = asideMainWidth + 'px'
                  }
                }
                break
              case 'MAIN-ASIDE':
                let mainAsideWidth = initWidth - moveLen
                if (moveLen < 0) {
                  
                  if (previousElement.clientWidth > 400) {
                    
                    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) {
                    
                    previousElement.style.height = headerMainHeight + 'px'
                  }
                } else {
                  
                  if (nextElement.clientHeight > 60) {
                    
                    previousElement.style.height = headerMainHeight + 'px'
                  }
                }
                break
              }
              case 'MAIN-HEADER': {
                let mainHeaderHeight = initHeight - moveHeight
                if (moveHeight < 0) {
                  
                  if (previousElement.clientHeight > 60) {
                    
                    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函数,关闭splash

import { invoke } from '@tauri-apps/api/tauri'

onMounted(() => {
  
  document.addEventListener('DOMContentLoaded', () => {
    
    
    setTimeout(() => {
      invoke('close_splashscreen')
    }, 1000)
  })
})

🎉结语 感兴趣的可以试试,有不清楚的问题,关于tauri开发方面的问题,也可以一起交流。欢迎加我:zhan_1337608148。一起成长,一起进步。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21072,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?