Skip to content

Commit

Permalink
feat(count-down): support progress
Browse files Browse the repository at this point in the history
  • Loading branch information
qianmoQ committed Dec 6, 2024
1 parent ef86ffc commit d0b3920
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 24 deletions.
22 changes: 22 additions & 0 deletions docs/components/count-down.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,38 @@ This document is mainly used to describe some features and usage of the ShadcnCo

:::

## Show Progress

::: raw

<CodeRunner title="Show Progress">
<ShadcnCountDown :time="new Date(Date.now() + 20 * 1000)" show-progress />
</CodeRunner>

:::

::: details Show code

```vue
<template>
<ShadcnCountDown :time="new Date(Date.now() + 20 * 1000)" show-progress />
</template>
```

:::

## Count Down Props

<ApiTable title="Props"
:headers="['Attribute', 'Description', 'Type', 'Default Value']"
:columns="[
['progress', 'The progress of the count down, support <code>v-model:progress</code>', 'number', ''],
['time', 'The time of the count down', 'date', ''],
['simple', 'Whether to display the simple version', 'boolean', 'false'],
['title', 'The title of the count down, only valid when <code>simple</code> is false', 'string', ''],
['toolbar', 'Whether to display the toolbar', 'boolean', 'false'],
['warningThreshold', 'The warning threshold of the count down, only valid when <code>simple</code> is false', 'number', '5'],
['showProgress', 'Whether to display the progress bar', 'boolean', 'false'],
]">
</ApiTable>

Expand Down
9 changes: 5 additions & 4 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<template>
<div class="p-32">
<ShadcnCountDown title="Activity events"
toolbar
:time="new Date(Date.now() + 2 * 24 * 60 * 60 * 1000)"/>
<ShadcnCountDown title="Activity events"
toolbar
show-progress
:time="new Date(Date.now() + 6 * 1000)"/>
</div>
</template>

<script setup>
</script>
</script>
8 changes: 6 additions & 2 deletions src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default {
},
logger: {
placeholder: {
search: 'Search ...'
search: 'Search ...'
},
text: {
allLevel: 'All level'
Expand All @@ -94,7 +94,11 @@ export default {
second: 'Second',
pause: 'Pause',
resume: 'Resume',
reset: 'Reset'
reset: 'Reset',
paused: 'Paused',
completed: 'Completed',
timeUp: 'Time up',
running: 'Running'
}
}
}
8 changes: 6 additions & 2 deletions src/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default {
},
logger: {
placeholder: {
search: '搜索 ...'
search: '搜索 ...'
},
text: {
allLevel: '所有级别'
Expand All @@ -94,7 +94,11 @@ export default {
second: '秒',
pause: '暂停',
resume: '继续',
reset: '重置'
reset: '重置',
paused: '已暂停',
completed: '已完成',
timeUp: '即将到期',
running: '进行中'
}
}
}
158 changes: 142 additions & 16 deletions src/ui/count-down/ShadcnCountDown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@
<div class="grid grid-cols-4 gap-4 text-center">
<!-- Days -->
<div class="relative flex flex-col">
<div :class="['text-4xl font-bold bg-slate-100 rounded-lg p-4',
{ 'animate-pulse': timeLeft.days <= warningThreshold && !isPaused }
<div :class="['text-4xl font-bold rounded-lg p-4 transition-all duration-300',
{
'bg-slate-100': !isWarning && !isCompleted,
'bg-red-50': isWarning && !isCompleted,
'bg-green-50': isCompleted,
'animate-pulse': isWarning && !isPaused && !isCompleted
}
]">
{{ padNumber(timeLeft.days) }}
<span class="absolute -top-1 -right-1 flex h-3 w-3" v-if="timeLeft.days <= warningThreshold">
<span v-if="isWarning && !isCompleted"
class="absolute -top-1 -right-1 flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
Expand All @@ -43,39 +49,82 @@

<!-- Hours -->
<div class="flex flex-col">
<div :class="['text-4xl font-bold bg-slate-100 rounded-lg p-4',
{ 'animate-pulse': timeLeft.days <= warningThreshold && !isPaused }
]">
<div :class="['text-4xl font-bold rounded-lg p-4 transition-all duration-300',
{
'bg-slate-100': !isWarning && !isCompleted,
'bg-red-50': isWarning && !isCompleted,
'bg-green-50': isCompleted,
'animate-pulse': isWarning && !isPaused && !isCompleted
}
]">
{{ padNumber(timeLeft.hours) }}
</div>
<span class="text-sm mt-2">{{ t('countDown.text.hour') }}</span>
</div>

<!-- Minutes -->
<div class="flex flex-col">
<div :class="['text-4xl font-bold bg-slate-100 rounded-lg p-4',
{ 'animate-pulse': timeLeft.days <= warningThreshold && !isPaused }
]">
<div :class="['text-4xl font-bold rounded-lg p-4 transition-all duration-300',
{
'bg-slate-100': !isWarning && !isCompleted,
'bg-red-50': isWarning && !isCompleted,
'bg-green-50': isCompleted,
'animate-pulse': isWarning && !isPaused && !isCompleted
}
]">
{{ padNumber(timeLeft.minutes) }}
</div>
<span class="text-sm mt-2">{{ t('countDown.text.minute') }}</span>
</div>

<!-- Seconds -->
<div class="flex flex-col">
<div :class="['text-4xl font-bold bg-slate-100 rounded-lg p-4',
{ 'animate-pulse': timeLeft.days <= warningThreshold && !isPaused }
]">
<div :class="['text-4xl font-bold rounded-lg p-4 transition-all duration-300',
{
'bg-slate-100': !isWarning && !isCompleted,
'bg-red-50': isWarning && !isCompleted,
'bg-green-50': isCompleted,
'animate-pulse': isWarning && !isPaused && !isCompleted
}
]">
{{ padNumber(timeLeft.seconds) }}
</div>
<span class="text-sm mt-2">{{ t('countDown.text.second') }}</span>
</div>
</div>

<!-- Progress bar -->
<div v-if="showProgress" class="mt-4 h-2 bg-gray-200 rounded-full overflow-hidden">
<div :class="['h-full transition-all duration-300',
{
'bg-primary': !isWarning && !isCompleted,
'bg-red-500': isWarning && !isCompleted,
'bg-green-500': isCompleted,
'animate-pulse': isWarning && !isPaused && !isCompleted
}
]"
:style="{ width: `${progress}%` }"
></div>
</div>

<!-- Status display -->
<div v-if="showProgress" class="mt-2 text-sm flex justify-between">
<span :class="['transition-colors duration-300',
{
'text-gray-500': !isWarning && !isCompleted,
'text-red-500': isWarning && !isCompleted,
'text-green-500': isCompleted
}
]">
{{ status }}
</span>
<span class="text-gray-500">{{ formatProgress }}</span>
</div>
</ShadcnCard>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { t } from '@/utils/locale'
import ShadcnCard from '@/ui/card'
import { CountDownEmits, CountDownProps } from '@/ui/count-down/types'
Expand All @@ -84,7 +133,8 @@ const emit = defineEmits<CountDownEmits>()
const props = withDefaults(defineProps<CountDownProps>(), {
simple: false,
toolbar: false,
warningThreshold: 5
warningThreshold: 5,
showProgress: false
})
const timeLeft = ref({
Expand All @@ -95,28 +145,42 @@ const timeLeft = ref({
})
let timer: NodeJS.Timeout | null = null
const initialDifference = ref(0)
const currentDifference = ref(0)
const isPaused = ref(false)
const calculateTimeLeft = () => {
if (isPaused.value) {
return
}
const now = new Date().getTime()
const target = new Date(props.time).getTime()
const difference = target - now
currentDifference.value = difference
// 如果时间差小于等于 0,设置完成状态
// If the difference is less than or equal to 0, set the completed state
if (difference <= 0) {
timeLeft.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
currentDifference.value = 0
if (timer) {
clearInterval(timer)
timer = null
}
emit('on-complete')
return
}
timeLeft.value = {
const newTimeLeft = {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((difference % (1000 * 60)) / 1000)
}
timeLeft.value = newTimeLeft
emit('on-tick', newTimeLeft)
}
// Pause/Resume
Expand All @@ -136,7 +200,18 @@ const togglePause = () => {
// 重置
const onReset = () => {
isPaused.value = false
const target = new Date(props.time).getTime()
const now = new Date().getTime()
initialDifference.value = target - now
calculateTimeLeft()
// 只有在还有剩余时间的情况下才启动定时器
// Only start the timer if there is remaining time
if (currentDifference.value > 0) {
if (timer) {
clearInterval(timer)
}
timer = setInterval(calculateTimeLeft, 1000)
}
}
// Number pad
Expand All @@ -145,13 +220,64 @@ const padNumber = (num) => {
return String(num).padStart(2, '0')
}
// Calculate progress
// 计算进度
const progress = computed(() => {
if (initialDifference.value === 0 || currentDifference.value <= 0) {
return 100
}
const currentProgress = Math.max(0, Math.min(100,
((initialDifference.value - currentDifference.value) / initialDifference.value) * 100
))
emit('update:progress', currentProgress)
return currentProgress
})
const formatProgress = computed(() => {
return `${ progress.value.toFixed(1) }%`
})
// Status text
// 状态文本
const status = computed(() => {
// 先判断是否已经完成
// First, check if it's completed
if (currentDifference.value <= 0) {
return t('countDown.text.completed')
}
// 其他状态判断
// Other status checks
if (isPaused.value) {
return t('countDown.text.paused')
}
if (timeLeft.value.days <= props.warningThreshold) {
return t('countDown.text.timeUp')
}
return t('countDown.text.running')
})
const isWarning = computed(() => {
return timeLeft.value.days <= props.warningThreshold && currentDifference.value > 0
})
const isCompleted = computed(() => {
return currentDifference.value <= 0
})
watch(() => props.time, () => {
onReset()
})
onMounted(() => {
const target = new Date(props.time).getTime()
const now = new Date().getTime()
initialDifference.value = target - now
calculateTimeLeft()
timer = setInterval(calculateTimeLeft, 1000)
// 只有在还有剩余时间的情况下才启动定时器
// Only start the timer if there is remaining time
if (currentDifference.value > 0) {
timer = setInterval(calculateTimeLeft, 1000)
}
})
onUnmounted(() => {
Expand Down
12 changes: 12 additions & 0 deletions src/ui/count-down/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
export interface TimeLeft
{
days: number
hours: number
minutes: number
seconds: number
}

export interface CountDownProps
{
progress?: number
time: Date
simple?: boolean
title?: string
toolbar?: boolean
warningThreshold?: number
showProgress?: boolean
}

export type CountDownEmits = {
(e: 'on-complete'): void
(e: 'on-tick', timeLeft: TimeLeft): void
(e: 'update:progress', progress: number): void
}

0 comments on commit d0b3920

Please sign in to comment.