release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆 Made-with: Cursor
This commit is contained in:
376
WebUI/src/components/QuadrantTaskCard.vue
Normal file
376
WebUI/src/components/QuadrantTaskCard.vue
Normal file
@@ -0,0 +1,376 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useTaskStore } from '@/stores/useTaskStore'
|
||||
import { useUIStore } from '@/stores/useUIStore'
|
||||
import type { TaskResponse } from '@/api/tasks'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
task: TaskResponse
|
||||
}>()
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
const uiStore = useUIStore()
|
||||
const isToggling = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
|
||||
const isAnimating = ref(false)
|
||||
const showStars = ref(false)
|
||||
|
||||
const starPositions = [
|
||||
{ x: -10, y: -10, delay: 0 },
|
||||
{ x: 50, y: -15, delay: 50 },
|
||||
{ x: 100, y: -10, delay: 100 },
|
||||
{ x: -15, y: 30, delay: 75 },
|
||||
{ x: 105, y: 35, delay: 125 }
|
||||
]
|
||||
|
||||
const formattedDueDate = computed(() => {
|
||||
if (!props.task.due_date) return null
|
||||
const date = new Date(props.task.due_date)
|
||||
const now = new Date()
|
||||
const diff = date.getTime() - now.getTime()
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days < 0) return { text: '已过期', class: 'overdue' }
|
||||
if (days === 0) return { text: '今天截止', class: 'today' }
|
||||
if (days === 1) return { text: '明天截止', class: 'tomorrow' }
|
||||
return { text: `${date.getMonth() + 1}/${date.getDate()}`, class: '' }
|
||||
})
|
||||
|
||||
const categoryColor = computed(() => props.task.category?.color)
|
||||
const categoryName = computed(() => props.task.category?.name)
|
||||
const tags = computed(() => props.task.tags ?? [])
|
||||
|
||||
async function handleToggle() {
|
||||
if (isToggling.value) return
|
||||
|
||||
if (!props.task.is_completed) {
|
||||
isAnimating.value = true
|
||||
showStars.value = true
|
||||
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
isAnimating.value = false
|
||||
setTimeout(() => {
|
||||
showStars.value = false
|
||||
}, 600)
|
||||
}
|
||||
|
||||
isToggling.value = true
|
||||
try {
|
||||
const result = await taskStore.toggleTask(props.task.id)
|
||||
if (result && result.is_completed) {
|
||||
ElMessage.success({
|
||||
message: '太棒了!任务完成啦~',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isToggling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
uiStore.openTaskDialog(props.task)
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要删除这个任务吗?删除后可就找不回来了呢~',
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
isDeleting.value = true
|
||||
const success = await taskStore.deleteTask(props.task.id)
|
||||
if (success) {
|
||||
ElMessage.success('任务已删除')
|
||||
}
|
||||
} catch {
|
||||
// 用户取消
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="quadrant-task-card"
|
||||
:class="{
|
||||
completed: task.is_completed,
|
||||
'task-complete': isAnimating
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="showStars"
|
||||
v-for="star in starPositions"
|
||||
:key="`star-${star.delay}-${star.x}`"
|
||||
class="star-burst"
|
||||
:style="{
|
||||
left: star.x + '%',
|
||||
top: star.y + '%',
|
||||
animationDelay: star.delay + 'ms'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div class="card-content">
|
||||
<el-checkbox
|
||||
:model-value="task.is_completed"
|
||||
:loading="isToggling"
|
||||
size="small"
|
||||
class="task-checkbox"
|
||||
@change="handleToggle"
|
||||
/>
|
||||
<div class="task-info">
|
||||
<div class="task-title-row">
|
||||
<h4
|
||||
class="task-title"
|
||||
:class="{ 'line-through': task.is_completed }"
|
||||
>
|
||||
{{ task.title }}
|
||||
</h4>
|
||||
<span
|
||||
v-if="formattedDueDate"
|
||||
class="task-due"
|
||||
:class="formattedDueDate.class"
|
||||
>
|
||||
<el-icon><Calendar /></el-icon>
|
||||
{{ formattedDueDate.text }}
|
||||
</span>
|
||||
<el-button
|
||||
text
|
||||
size="small"
|
||||
class="action-btn edit-btn"
|
||||
@click="handleEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="task-bottom-row">
|
||||
<div v-if="categoryName || tags.length > 0" class="task-tags">
|
||||
<span
|
||||
v-if="categoryName"
|
||||
class="meta-chip category-chip"
|
||||
:style="{ '--chip-color': categoryColor }"
|
||||
>
|
||||
{{ categoryName }}
|
||||
</span>
|
||||
<span
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
class="meta-chip tag-chip"
|
||||
>
|
||||
#{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
text
|
||||
size="small"
|
||||
class="action-btn delete-btn"
|
||||
:loading="isDeleting"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.quadrant-task-card {
|
||||
position: relative;
|
||||
padding: 10px 12px;
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 3px solid var(--quadrant-color, var(--priority-q4));
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
opacity: 0.6;
|
||||
background: var(--background-dark);
|
||||
|
||||
.task-title {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-checkbox {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
border-radius: 50%;
|
||||
border-color: var(--primary);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
&::after {
|
||||
height: 8px;
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.task-due {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
padding: 2px 6px;
|
||||
background: var(--background-dark);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.overdue {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
&.today {
|
||||
background: rgba(255, 179, 71, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.tomorrow {
|
||||
background: rgba(152, 216, 200, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
.task-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-bottom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.meta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.category-chip {
|
||||
background: color-mix(in srgb, var(--chip-color, #ccc) 15%, transparent);
|
||||
color: var(--chip-color, var(--text-secondary));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
background: var(--background-dark);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
color: var(--text-secondary);
|
||||
padding: 4px;
|
||||
min-height: auto;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.delete-btn:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.star-burst {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23FFB7C5'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E") no-repeat center;
|
||||
animation: starBurst 0.6s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes starBurst {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(0) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translate(10px, -10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) translate(20px, -20px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user