開源倉庫地址 gitee
Git倉庫地址:https://gitee.com/zhanhongzhu/zhanhongzhu.git
應(yīng)用地址
windows應(yīng)用地址下載 https://kestrel-task.cn
具體內(nèi)容
也可以看??使用Tauri+vite+koa2+mysql開發(fā)了一款待辦效率應(yīng)用 這篇文章。
??技術(shù)棧
- Tauri: Tauri是一個用于構(gòu)建現(xiàn)代桌面應(yīng)用程序的工具,結(jié)合了Rust、Vue.js和Web技術(shù),提供了強大的跨平臺能力。
- Vue3: Vue3是流行的JavaScript框架Vue.js的最新版本,具有更好的性能、更好的TypeScript支持和更多的特性。
- Vite5: Vite是一個現(xiàn)代化的構(gòu)建工具,Vite5是其最新版本,具有快速的冷啟動、熱模塊替換和原生ES模塊支持。
- Koa2: Koa2是一個基于Node.js的輕量級Web框架,使用異步函數(shù)處理中間件,提供了簡潔而強大的Web開發(fā)體驗。
- MySQL: MySQL是一個流行的關(guān)系型數(shù)據(jù)庫管理系統(tǒng),具有高性能、可靠性和廣泛的應(yīng)用領(lǐng)域,適用于各種規(guī)模的應(yīng)用程序。
我的待辦
快速添加待辦任務(wù),快速查看任務(wù)進(jìn)度,摘要等。新增標(biāo)簽,分類,更好管理待辦任務(wù)。通過標(biāo)簽、分類篩選待辦任務(wù),方便快捷。
[圖片上傳失敗...(image-c98267-1721834837063)]
[圖片上傳失敗...(image-683b1c-1721834837063)]
OKR目標(biāo)管理
我的想法是通過OKR管理系列的任務(wù),這樣每完成一個小任務(wù),就可以關(guān)閉一個小任務(wù),直觀又方便,等到所有關(guān)鍵的小任務(wù)都完成了,整個任務(wù)也就完成了。
[圖片上傳失敗...(image-a61a3-1721834837063)]
番茄工作法
主要是一個計時的時鐘,可以在專注計時的時候,專注地完成某項任務(wù),快捷方便,使用番茄工作法,選擇一個待完成的任務(wù),將番茄時間設(shè)為25分鐘,也可以選擇其他的區(qū)間,專注工作,中途不允許做任何與該任務(wù)無關(guān)的事。時刻保持專注。
[圖片上傳失敗...(image-9bd314-1721834837063)]
日歷視圖
打開日歷界面,通過視圖的形式查看公歷或農(nóng)歷日歷下每個日期的待辦提醒或任務(wù)事項。也可以在日歷視圖,添加任務(wù)。
[圖片上傳失敗...(image-8b539b-1721834837063)]
MEMO快速記錄
為了更好地幫你捕捉想法與靈感,提供了快速記錄的輸入框。專注記錄想法,無需思考標(biāo)題和排版。控制記錄長度,降低記錄壓力,快速捕捉。
[圖片上傳失敗...(image-8e11af-1721834837063)]
統(tǒng)計功能
展示了每天的待辦數(shù),以及每天新增的待辦數(shù)量。
[圖片上傳失敗...(image-317334-1721834837063)]
展示功能點
打包發(fā)布版本
腳本命令
"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;
//獲取版本更新的內(nèi)容
//簽名、版本路徑、發(fā)版日期
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模塊
此處進(jìn)行簡單的封裝,接口請求在控制臺無法被查看到。如果覺得不方便,完全可以使用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函數(shù)
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'
// 創(chuàng)建新窗口
export async function createWin(args) {
await emit('win-create', args)
}
// 獲取窗口
export async function getWin(label) {
return await WebviewWindow.getByLabel(label)
}
/**
* @desc 設(shè)置窗口
* @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";
// 系統(tǒng)參數(shù)配置
export const windowConfig = {
label: null, // 窗口唯一label
title: "", // 窗口標(biāo)題
url: "", // 路由地址url
width: 900, // 窗口寬度
height: 640, // 窗口高度
minWidth: null, // 窗口最小寬度
minHeight: null, // 窗口最小高度
x: null, // 窗口相對于屏幕左側(cè)坐標(biāo)
y: null, // 窗口相對于屏幕頂端坐標(biāo)
center: true, // 窗口居中顯示
resizable: true, // 是否支持縮放
maximized: false, // 最大化窗口
decorations: true, // 窗口是否無邊框及導(dǎo)航條
alwaysOnTop: false, // 置頂窗口
};
class Windows {
constructor() {
this.mainWin = null;
}
// 獲取窗口
getWin(label) {
return WebviewWindow.getByLabel(label);
}
// 獲取全部窗口
getAllWin() {
return getAll();
}
// 創(chuàng)建新窗口
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();
}
// 創(chuàng)建窗口對象
let win = new WebviewWindow(args.label, args);
// 是否最大化
if (args.maximized && args.resizable) {
win.maximize();
}
// 窗口創(chuàng)建完畢/失敗
win.once("tauri://created", async () => {
console.log("window create success!");
});
win.once("tauri://error", async () => {
console.log("window create error!");
});
}
// 開啟主進(jìn)程監(jiān)聽事件
async listen() {
// 創(chuàng)建新窗體
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();
});
// 退出應(yīng)用
await listen("win-exit", async (event) => {
setWin("logout");
await exit();
});
// 重啟應(yīng)用
await listen("win-relaunch", async (event) => {
await relaunch();
});
// 主/渲染進(jìn)程傳參
await listen("win-setdata", async (event) => {
await emit("win-postdata", JSON.parse(event.payload));
});
}
}
export default Windows;
封裝Echart組件,便于使用
[圖片上傳失敗...(image-e7e272-1721834837063)]
<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%' },
// 是否無數(shù)據(jù)
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'], // 顏色數(shù)組
grid: { // 網(wǎng)格
left: '3%', // 左邊距
right: '3%', // 右邊距
bottom: '10', // 下邊距
top: '40', // 上邊距
containLabel: true // 包含標(biāo)簽
},
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: { // 軸標(biāo)簽
color: '#999' // 顏色
},
splitLine: { // 分隔線
show: false, // 不顯示
lineStyle: { // 線條樣式
color: ['#eee'] // 顏色
}
},
splitArea: { // 分隔區(qū)域
show: false, // 不顯示
areaStyle: { // 區(qū)域樣式
color: ['rgba(255,255,255,0.01)', 'rgba(0,0,0,0.01)'] // 顏色
}
}
},
valueAxis: { // 數(shù)值軸
axisLine: { // 軸線
show: false, // 不顯示
lineStyle: { // 線條樣式
color: '#999' // 顏色
}
},
splitLine: { // 分隔線
show: true, // 顯示
lineStyle: { // 線條樣式
color: 'rgba(128,128,128,0.2)' // 顏色
}
}
}
}
export default T
封裝公共的彈窗組件
[圖片上傳失敗...(image-f08344-1721834837063)]
<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">關(guān)閉</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: '' }, // 彈出框標(biāo)題
modalAppendToBody: { type: Boolean, default: false }, // 是否將彈出框插入到body中
appendToBody: { type: Boolean, default: false }, // 是否將內(nèi)容插入到body中
closeOnClickModal: { type: Boolean, default: false }, // 是否在點擊模態(tài)框時關(guān)閉
closeOnPressEscape: { type: Boolean, default: false }, // 是否在按下ESC鍵時關(guān)閉
dblclickDisabled: { type: Boolean, default: false }, // 是否禁用雙擊放大
closeBtn: { type: Boolean, default: false }, // 關(guān)閉按鈕
},
['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函數(shù)
主要是監(jiān)聽元素的變化或者窗口的變化。有一部分用到了可以拉伸的左右布局的模塊。
//directive/index.js
// 監(jiān)聽元素大小變化的指令
const map = new WeakMap();
const ob = new ResizeObserver((entries) => {
for (const entry of entries) {
// 獲取dom元素的回調(diào)
const handler = map.get(entry.target);
//存在回調(diào)函數(shù)
if (handler) {
// 將監(jiān)聽的值給回調(diào)函數(shù)
handler({
width: entry.borderBoxSize[0].inlineSize,
height: entry.borderBoxSize[0].blockSize,
});
}
}
});
export const Resize = {
mounted(el, binding) {
//將dom與回調(diào)的關(guān)系塞入map
map.set(el, binding.value);
//監(jiān)聽el元素的變化
ob.observe(el);
},
unmounted(el) {
//取消監(jiān)聽
ob.unobserve(el);
},
};
const directives = { Resize };
const registerDirective = (app) => {
Object.keys(directives).forEach((key) => {
app.directive(key, directives[key]);
});
};
export default registerDirective;
公共拖曳布局的函數(shù)
主要是用于左右布局寬度的變化,可以使用鼠標(biāo)進(jìn)行拖曳,改變左右盒子的高度。
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) {
// 左側(cè)剩90
previousElement.style.width = asideMainWidth + 'px'
}
} else {
// 向右移動
if (nextElement.clientWidth > 400) {
// 右側(cè)剩90
previousElement.style.width = asideMainWidth + 'px'
}
}
break
case 'MAIN-ASIDE':
let mainAsideWidth = initWidth - moveLen
if (moveLen < 0) {
// 向左移
if (previousElement.clientWidth > 400) {
// 左側(cè)剩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) {
// 上側(cè)剩90
previousElement.style.height = headerMainHeight + 'px'
}
} else {
// 向下移動
if (nextElement.clientHeight > 60) {
// 下側(cè)剩90
previousElement.style.height = headerMainHeight + 'px'
}
}
break
}
case 'MAIN-HEADER': {
let mainHeaderHeight = initHeight - moveHeight
if (moveHeight < 0) {
// 向上移
if (previousElement.clientHeight > 60) {
// 左側(cè)剩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
}
}
公共布局
此處的可以自己查看代碼。
[圖片上傳失敗...(image-be7369-1721834837063)]
invoke調(diào)用rust函數(shù),關(guān)閉splash
import { 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)
})
})
??結(jié)語 感興趣的可以試試,有不清楚的問題,關(guān)于tauri開發(fā)方面的問題,也可以一起交流。歡迎加我:zhan_1337608148。一起成長,一起進(jìn)步。