完成番茄时小组件
This commit is contained in:
parent
34d37d4efd
commit
c98a7d70b1
|
@ -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 |
|
@ -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>
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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</>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -1,9 +0,0 @@
|
|||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent(() => {
|
||||
return () => (
|
||||
<div class={"w-full h-full"}>
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -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> :
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue