完成番茄时小组件

This commit is contained in:
expdsn 2024-11-04 19:30:31 +08:00
parent 34d37d4efd
commit c98a7d70b1
10 changed files with 318 additions and 40 deletions

View File

@ -21,6 +21,7 @@
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"gsap": "^3.12.5",
"localforage": "^1.10.0",
"lunar-typescript": "^1.7.5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

View File

@ -1,4 +1,4 @@
import { computed, defineComponent, onMounted, ref, Transition } from 'vue'
import { computed, defineComponent, onMounted, ref, Transition, watch } from 'vue'
import returnImg from "~/public/icons/work/return.png"
import endImg from "~/public/icons/work/tomotoIconEnd.png"
import playWaveGif from "~/public/icons/work/playMusicIcon.gif"
@ -8,7 +8,7 @@ import useBackgroundStore from '../background/useBackgroundStore'
import useLayoutStore from '../useLayoutStore'
import useTomatoStore, { musicList } from '@/widgets/work/useTomatoStore'
import Search from '../header/search'
import { Tooltip } from 'ant-design-vue'
import { Modal, Tooltip } from 'ant-design-vue'
import { formatSeconds } from '@/utils/tool'
export const DefaultPageSetting = [
{
@ -37,13 +37,11 @@ export const DefaultPageSetting = [
}
]
export default defineComponent(() => {
const show = computed(() => true)
const selectMode = ref(0)
const background = useBackgroundStore()
const layout = useLayoutStore()
const store = useTomatoStore()
const isFirst = ref(false)
const showSelectModal = ref(false)
onMounted(() => {
// 检查 localStorage 是否已经有访问记录
const visited = localStorage.getItem('hasVisited')
@ -54,9 +52,19 @@ export default defineComponent(() => {
// 设置标记,后续访问不会再次显示
}
})
watch(() =>
store.remainingTime
, (val) => {
console.log(val);
if (val <= 0) {
store.stopTomatoTime()
}
})
return () =>
store.openFullscreen && (
<div class="fixed left-0 top-0 z-50 w-full ">
<Transition name="background">
{background.bgTrriger && (
<>
@ -76,6 +84,9 @@ export default defineComponent(() => {
<Transition name="modal">
<div class={"w-[500px] h-[500px] absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 "}>
<Modal open={showSelectModal.value}>
</Modal>
{
Array.from({ length: 60 }).map((_, idx) => (
<div class={"bg-white w-[30px] h-[5px] absolute mt-[-2.5px] top-1/2"} style={{
@ -111,14 +122,26 @@ export default defineComponent(() => {
<img src={returnImg} alt='return' class={"w-[18px] h-[18px]"}></img>
</div>
</Tooltip>
<Tooltip title={"停止"}>
<Tooltip title={store.state.isStart ? "停止" : '开始'}>
<div
onClick={() => {
store.state.isStart ?
store.stopTomatoTime() :
store.beginTomatoTime()
}}
class={"w-[44px] h-[44px] flex items-center justify-center rounded-lg cursor-pointer hover:opacity-90"}
style={{
background: 'linear-gradient(225deg,#707eff 0%,#6b97ff 100%)',
boxShadow: '0 2px 4px #0003'
}}>
<img src={endImg} alt='return' class={"w-[18px] h-[18px]"}></img>
{
store.state.isStart ?
<img src={endImg} alt='return' class={"w-[18px] h-[18px]"}></img>
:
<img src={PlayStartImg} alt="start" class={"w-[18px] h-[18px]"} />
}
</div>
</Tooltip>

View File

@ -7,6 +7,7 @@ import App from './App.vue'
import getFp from './utils/getFp'
import vOutsideClick from './utils/vOutsideClick'
import dayjs from 'dayjs'
import Toast, { useToast, type PluginOptions } from 'vue-toastification'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import 'vue-toastification/dist/index.css'
@ -16,7 +17,6 @@ dayjs.locale('zh-cn')
const app = createApp(App)
export const globalToast = useToast()
dayjs.extend(customParseFormat)
// ! persist 利用 localstorage请不要在大量数据时使用
// 大量数据(扩张内容,文件),清直接使用 ./db.ts
app.use(createPinia().use(persist))

View File

@ -1,9 +1,72 @@
import { defineComponent } from 'vue'
import { formatSeconds } from '@/utils/tool'
import PlayStartImg from "~/public/icons/work/start.png"
import returnImg from "~/public/icons/work/return.png"
import PlusImg from "~/public/icons/work/tomatoIconAdd.png"
import dayjs from 'dayjs'
import { Tooltip } from 'ant-design-vue'
import useTomatoStore from './useTomatoStore'
export default defineComponent(() => {
const store = useTomatoStore()
return () => (
<div class="w-full h-full bg-[#ecfbff] flex flex-col">
<div class="w-full h-full flex relative p-6 justify-between" style={{
background: `url('https://newfatfox.oss-cn-beijing.aliyuncs.com/admin/tomotoback.png')`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat'
}}>
<div class={"bg-[#0000004d] absolute top-0 left-0 right-0 bottom-0 "}></div>
<div class={"w-[115px] h-full flex flex-col items-center z-10 text-white"}>
<div class={"w-full bg-white/20 text-center rounded text-[14px]"}></div>
<span class={"text-[42px] mb-1"}>
{
store.state.beginTime < 0 ? '15:00' : formatSeconds(store.remainingTime)
}
</span>
<span class={"text-[14px]"}>
<span class={"text-[#76e6ff] mx-1"}>
{store.state.timeList.filter(val => dayjs(val).isSame(dayjs(), 'day')).length}</span>
</span>
<span class={"text-[14px]"}></span>
</div>
<div class={"flex flex-col justify-end"}>
<div class={"flex gap-x-3 "}>
<Tooltip title={"沉浸模式"}>
<div
onClick={(e) => {
e.stopPropagation()
store.openFullscreen = true
}}
class={"w-[42px] h-[42px] bg-white/40 backdrop-blur-md flex items-center justify-center rounded-xl"}>
<img src={returnImg} alt="start" class={"w-[18px]"}></img>
</div>
</Tooltip>
<Tooltip title={"开始"}>
<div class={"w-[42px] h-[42px] bg-white/40 backdrop-blur-md flex items-center justify-center rounded-xl"}
onClick={(e) => {
e.stopPropagation()
store.beginTomatoTime()
store.openFullscreen = true
}}>
<img src={PlayStartImg} alt="start" class={"w-[18px]"}></img>
</div>
</Tooltip>
<Tooltip title={"添加目标"}>
<div class={"w-[42px] h-[42px] bg-white/40 backdrop-blur-md flex items-center justify-center rounded-xl"}
onClick={() => {
setTimeout(() => {
store.openShowModel = null
}, 300)
}}>
<img src={PlusImg} alt="start" class={"w-[18px]"}></img>
</div>
</Tooltip>
</div>
</div>
</div>
)
})

View File

@ -13,6 +13,7 @@ import type { TomatoTarget } from './useTomatoStore'
import dayjs from 'dayjs'
import useTomatoStore from './useTomatoStore'
import Calendar from './modal_view/calendar'
import DataDetail from './modal_view/DataDetail'
const workTab = [
{
title: '目标列表',
@ -235,7 +236,7 @@ export default defineComponent(() => {
) : select.value === 1 ? (
<Calendar />
) : select.value === 2 ? (
<div></div>
<DataDetail />
) : (
<>loading</>
)}

View File

@ -0,0 +1,202 @@
import { defineComponent, onBeforeUnmount, onMounted, reactive, ref } from "vue";
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core';
import { LineChart, PieChart, } from 'echarts/charts';
// 引入柱状图图表,图表后缀都为 Chart
import { BarChart } from 'echarts/charts';
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent
} from 'echarts/components';
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers';
import useTomatoStore from "../useTomatoStore";
import dayjs from "dayjs";
import gsap from 'gsap'
import { Progress } from "ant-design-vue";
// 注册必须的组件
echarts.use([
PieChart,
TitleComponent,
LineChart,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
// 接下来的使用就跟之前一样,初始化图表,设置配置项
export default defineComponent(() => {
const chart = ref(null);
const leftChat = ref(null)
const store = useTomatoStore()
const precent1 = reactive({
number: 0
})
const precent2 = reactive({
number: 0
})
let myChart: echarts.ECharts | null = null;
const initChart = () => {
myChart = echarts.init(chart.value);
const option = {
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const dataPoint = params[0];
const timeInMinutes = dataPoint.value;
// 使用 HTML 标签格式化 tooltip 内容
return `${dataPoint.name} <br/> <strong class="text-blue-500">${timeInMinutes} </strong>分钟`;
},
},
xAxis: {
type: 'category',
data: Array.from({ length: 7 }).map((_, idx) => (dayjs().subtract(idx, 'day').format('MM月DD日'))).reverse(),
},
yAxis: {
type: 'value',
},
series: [
{
type: 'line',
data: Array.from({ length: 7 }).map((_, idx) => (dayjs().subtract(idx, 'day').format('MM月DD日')))
.reverse()
.map(item => store.state.timeList.reduce((pre, cur) => {
if (dayjs(cur).isSame(dayjs(item, "MM月DD日"), 'day')) {
return pre + 1
}
return 0
}, 0) * 15),
lineStyle: {
color: '#C876FB', // 线条的颜色
width: 2, // 线条宽度
},
itemStyle: {
color: '#FF8688', // 数据点的颜色
},
showSymbol: false, // 默认不显示数据点
// hover 时显示数据点
emphasis: {
itemStyle: {
color: '#FF8688', // 悬停时数据点的颜色
},
showSymbol: true, // 悬停时显示数据点
},
},
],
};
myChart.setOption(option);
};
onMounted(() => {
initChart();
window.addEventListener('resize', myChart?.resize as any);
setTimeout(() => {
}, 1000)
gsap.to(precent1, { duration: 1, number: Number(100) || 0 })
gsap.to(precent2, { duration: 1, number: Number(100) || 0 })
});
onBeforeUnmount(() => {
window.removeEventListener('resize', myChart?.resize as any);
myChart?.dispose();
});
return () => (
<div class={"w-full h-full flex flex-col px-4 pb-3 gap-y-1"}>
<div class={"h-[144px] mt-[44px] flex justify-between"}>
<div class={"w-[360px] h-full flex justify-end items-center pr-10 relative"} style={{
background: 'linear-gradient(180deg,#F4FAFF 0%,#FFFFFF 100%)',
boxShadow: '0 2px 12px #0000001a',
borderRadius: '46px 8px 8px'
}}>
<div class={" left-5 -top-10 absolute bg-white rounded-full"} style={{
width: '174px',
height: '174px'
}}>
<Progress type="circle"
percent={precent1.number}
showInfo={false}
size={174}
strokeWidth={10}
strokeColor={{
'25%': '#76E05F',
'75%': '#F8E14F',
}}></Progress>
<div class={"w-[65%] bg-[#F6F6F6] shadow rounded-full h-[65%] items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"}>
<span class={"text-[#666]"}></span>
<span class={"font-bold text-[#333] text-[30px]"}>+300%</span>
</div>
</div>
<div class={"flex flex-col"}>
<span class={"text-[#333] text-[16px]"}></span>
<span class={"text-[#5a6eff] font-extrabold text-[36px]"}>0.71
<span class={"text-[20px]"}>h</span>
</span>
</div>
</div>
<div class={"w-[360px] h-full relative flex items-center justify-end pr-5"} style={{
background: 'linear-gradient(180deg,#F4FAFF 0%,#FFFFFF 100%)',
boxShadow: '0 2px 12px #0000001a',
borderRadius: '46px 8px 8px'
}}>
<div class={" left-5 -top-10 absolute bg-white rounded-full"} style={{
width: '174px',
height: '174px'
}}>
<Progress type="circle"
percent={precent2.number}
showInfo={false}
size={174}
strokeWidth={10}
strokeColor={{
'25%': '#DB63FB',
'75%': '#977BFB',
}}></Progress>
<div class={"w-[65%] bg-[#F6F6F6] shadow rounded-full h-[65%] items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"}>
<span class={"text-[#666]"}></span>
<span class={"font-bold text-[#333] text-[30px]"}>+300%</span>
</div>
</div>
<div class={"flex flex-col"}>
<span class={"text-[#333] text-[16px]"}></span>
<span class={"text-[#5a6eff] font-extrabold text-[36px]"}>0.71
<span class={"text-[20px]"}>h</span>
</span>
</div>
</div>
</div>
<div class={"flex-1 h-0 w-full p-1 flex flex-col rounded-lg items-start justify-between relative"} style={{
boxShadow: '0 2px 12px #0000001a'
}}>
<span class={" absolute top-2 left-5 text-[#666666]"}></span>
<div class={"h-[750px] relative w-full"}>
<div ref={chart} class={"w-full h-full"} ></div>
</div>
<span class={"absolute bottom-3 left-5 text-[#9d6dff]"}></span>
</div>
</div>
)
})

View File

@ -1,9 +0,0 @@
import { defineComponent } from "vue";
export default defineComponent(() => {
return () => (
<div class={"w-full h-full"}>
</div>
)
})

View File

@ -86,7 +86,9 @@ export default defineComponent(() => {
<div class={"relative h-[120px] w-full py-1"}>
<div class={"w-full h-full rounded-lg border-[1px] border-black/20 justify-between flex items-center px-4"}>
<div class={"flex flex-col text-[16px] gap-y-2"}>
<span class={" tracking-wide"}> <span class={"text-[#5b47ff]"}>{store.state.list.filter(val => dayjs(val.finishTime).isSame(dayjs(), 'day') && val.isCompleted).length}</span></span>
<span class={" tracking-wide"}><span class={"text-[#5b47ff] mx-1"}>
{store.state.timeList.filter(val => dayjs(val).isSame(dayjs(), 'day')).length}
</span></span>
<span class={"text-[#ff8686]"}></span>
</div>
{
@ -99,7 +101,6 @@ export default defineComponent(() => {
store.stopTomatoTime()
}}
>
<img src={StopImg} alt="play img " class={"w-[13px]"} />
</button> :

View File

@ -42,15 +42,20 @@ export const musicList = [
export default defineStore("work", () => {
const state = reactive({
list: [] as TomatoTarget[],
timeList: [] as TomatoTime[],
timeList: [] as number[],
isPlaying: false as boolean,
selectMusic: 0,
isStart: false as boolean,
beginTime: -1 as number
})
const time = useTimeStore()
const remainingTime = computed(() => {
if (!state.isStart) {
return 0
}
return dayjs(state.beginTime).add(1, 'minute').diff(dayjs(time.date), 'second')
})
const beginTomatoTime = () => {
state.beginTime = dayjs().valueOf()
state.isStart = true
@ -60,27 +65,18 @@ export default defineStore("work", () => {
state.isStart = false
state.beginTime = -1
stopMusic()
if (remainingTime.value <= 0) {
state.timeList.push(
dayjs().valueOf()
)
}
}
const remainingTime = computed(() => {
const totalTime = TOTAL_TIME
return dayjs(state.beginTime).add(15, 'minute').diff(dayjs(time.date), 'second')
})
const playAudio = computed(() => {
const audio = new Audio(musicList[state.selectMusic].music)
return audio
})
watch(() => remainingTime, (val) => {
if (val.value < 0) {
state.isPlaying = false
state.isStart = false
state.beginTime = -1
state.timeList.push({
date: dayjs().valueOf(),
finishTime: TOTAL_TIME
})
}
})
// watch(() => state.isPlaying, (val) => {
// if (val) {
// playAudio.value.play()
@ -116,4 +112,4 @@ export default defineStore("work", () => {
stopMusic,
stopTomatoTime
}
})
})