重新设计每日任务进度更新弹窗UI - 修复滑块、输入框、文本框文字颜色为黑色的问题 - 优化整体布局,添加任务序号标签 - 改进进度滑块样式,使用渐变色轨道 - 优化卡片样式,添加悬停效果 - 改进提示横幅设计 - 优化无任务状态和加载状态的展示 - 添加自定义滚动条样式

This commit is contained in:
Your Name 2026-01-14 17:26:13 +08:00
parent 4496af862e
commit c9cd8e3f78

View File

@ -1,90 +1,140 @@
<template>
<a-modal
v-model:open="visible"
title="📝 每日任务进度更新"
:width="700"
:width="680"
:closable="false"
:maskClosable="false"
:keyboard="false"
:footer="null"
centered
class="daily-progress-modal"
>
<!-- 自定义标题 -->
<template #title>
<div class="modal-title">
<span class="title-icon">📋</span>
<span class="title-text">每日任务进度更新</span>
</div>
</template>
<div class="daily-progress-reminder">
<div class="reminder-header">
<Icon name="alert-circle" size="lg" color="#F59E0B" />
<div class="header-text">
<h3>请更新您今天的任务进度</h3>
<p>每日5:30后首次登录需要更新进行中的任务进度</p>
<!-- 提示信息 -->
<div class="reminder-banner">
<div class="banner-icon">
<Icon name="alert-circle" size="md" color="#F59E0B" />
</div>
<div class="banner-content">
<div class="banner-title">请更新您今天的任务进度</div>
<div class="banner-desc">每日 17:30 后首次登录需要更新进行中的任务进度</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" tip="正在加载任务列表..." />
<a-spin size="large" />
<p class="loading-text">正在加载任务列表...</p>
</div>
<!-- 无任务状态 -->
<div v-else-if="tasks.length === 0" class="no-tasks">
<Icon name="check-circle" size="xl" color="#31f08b" />
<p>太棒了!您目前没有进行中的任务</p>
<a-button type="primary" @click="handleNoTasksConfirm">
确认
<div class="no-tasks-icon">
<Icon name="check-circle" size="xl" color="#31f08b" />
</div>
<div class="no-tasks-title">太棒了!</div>
<div class="no-tasks-desc">您目前没有进行中的任务</div>
<a-button type="primary" size="large" @click="handleNoTasksConfirm">
<Icon name="check" size="sm" style="margin-right: 6px;" />
确认并关闭
</a-button>
</div>
<!-- 任务列表 -->
<div v-else class="tasks-container">
<div class="tasks-list">
<div v-for="task in tasks" :key="task.id" class="task-item-card">
<div class="task-header-row">
<span class="task-title">{{ task.title }}</span>
<span class="task-no">{{ task.taskNo }}</span>
</div>
<div v-for="(task, index) in tasks" :key="task.id" class="task-card">
<!-- 任务序号标签 -->
<div class="task-index">{{ index + 1 }}</div>
<div class="task-meta">
<span class="meta-item">
<Icon name="calendar" size="xs" color="#6B6B6B" />
开始: {{ formatDate(task.startTime) }}
</span>
<span class="meta-item">
<Icon name="clock" size="xs" color="#F59E0B" />
截止: {{ formatDate(task.deadline) }}
</span>
<!-- 任务信息 -->
<div class="task-info">
<div class="task-header">
<div class="task-title">{{ task.title }}</div>
<div class="task-no">{{ task.taskNo }}</div>
</div>
<div class="task-time">
<div class="time-item">
<Icon name="calendar" size="xs" />
<span class="time-label">开始:</span>
<span class="time-value">{{ formatDate(task.startTime) }}</span>
</div>
<div class="time-item deadline">
<Icon name="clock" size="xs" />
<span class="time-label">截止:</span>
<span class="time-value">{{ formatDate(task.deadline) }}</span>
</div>
</div>
</div>
<div class="progress-input-row">
<span class="progress-label">当前进度:</span>
<a-slider
v-model:value="task.newProgress"
:min="task.progress || 0"
:max="100"
:marks="{ [task.progress || 0]: `上次:${task.progress || 0}%`, 100: '100%' }"
:tooltip="{ formatter: (value: number) => `${value}%` }"
/>
<a-input-number
v-model:value="task.newProgress"
:min="task.progress || 0"
:max="100"
:precision="0"
style="width: 80px; margin-left: 12px"
addon-after="%"
/>
</div>
<!-- 进度更新区域 -->
<div class="progress-section">
<div class="progress-header">
<span class="progress-title">更新进度</span>
<span class="progress-hint">当前: {{ task.progress || 0 }}%</span>
</div>
<div class="progress-control">
<div class="slider-wrapper">
<a-slider
v-model:value="task.newProgress"
:min="0"
:max="100"
:step="5"
:tooltip="{ formatter: (value: number) => `${value}%` }"
/>
<div class="slider-labels">
<span class="label-start">0%</span>
<span class="label-current" :style="{ left: `${task.progress || 0}%` }">
上次 {{ task.progress || 0 }}%
</span>
<span class="label-end">100%</span>
</div>
</div>
<div class="input-wrapper">
<a-input-number
v-model:value="task.newProgress"
:min="0"
:max="100"
:precision="0"
:controls="true"
/>
<span class="input-suffix">%</span>
</div>
</div>
<div class="remark-input-row">
<a-textarea
v-model:value="task.progressRemark"
placeholder="请简要说明今日进展(选填)"
:rows="2"
:maxlength="200"
show-count
/>
</div>
<!-- 备注输入 -->
<div class="remark-section">
<a-textarea
v-model:value="task.progressRemark"
placeholder="简要说明今日进展(选填)"
:rows="2"
:maxlength="200"
show-count
/>
</div>
<div v-if="task.error" class="error-message">
{{ task.error }}
<!-- 错误提示 -->
<div v-if="task.error" class="error-tip">
<Icon name="alert-circle" size="xs" />
{{ task.error }}
</div>
</div>
</div>
</div>
<div class="reminder-footer">
<!-- 提交按钮 -->
<div class="submit-section">
<a-button
type="primary"
size="large"
@ -92,10 +142,12 @@
:loading="submitting"
@click="handleSubmit"
>
<Icon name="check" size="sm" style="margin-right: 4px; vertical-align: middle;" />
<template #icon>
<Icon name="check" size="sm" />
</template>
提交所有任务进度
</a-button>
<p class="footer-tip">* 必须更新所有任务进度才能关闭此弹窗</p>
<div class="submit-tip">* 必须更新所有任务进度才能关闭此弹窗</div>
</div>
</div>
</div>
@ -103,7 +155,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import Icon from '@/components/Icon.vue'
import * as taskApi from '@/api/task'
@ -136,14 +188,14 @@ const checkAndShow = async () => {
return // 今天已填写,不再提醒
}
// 检查当前时间是否在5:30之后
// 检查当前时间是否在17:30之后
const currentTime = dayjs()
const currentHour = currentTime.hour()
const currentMinute = currentTime.minute()
// 判断是否在5:30之后
if (currentHour < 5 || (currentHour === 5 && currentMinute < 30)) {
return // 还未到5:30,不提醒
// 判断是否在17:30之后
if (currentHour < 17 || (currentHour === 17 && currentMinute < 30)) {
return // 还未到17:30,不提醒
}
// 加载进行中的任务
@ -240,152 +292,505 @@ const handleSubmit = async () => {
defineExpose({
checkAndShow
})
// 组件挂载时不自动检查,由父组件调用
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
// 模态框标题
.modal-title {
display: flex;
align-items: center;
gap: 10px;
.title-icon {
font-size: 20px;
}
.title-text {
font-size: 18px;
font-weight: 600;
color: $text-primary;
}
}
.daily-progress-reminder {
.reminder-header {
// 提示横幅
.reminder-banner {
display: flex;
align-items: flex-start;
gap: $space-md;
padding: $space-lg;
background: rgba(245, 158, 11, 0.05);
border-radius: $radius-md;
margin-bottom: $space-lg;
.header-text {
align-items: center;
gap: 14px;
padding: 16px 20px;
background: linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(245, 158, 11, 0.06) 100%);
border: 1px solid rgba(245, 158, 11, 0.25);
border-radius: 12px;
margin-bottom: 24px;
.banner-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.15);
border-radius: 10px;
}
.banner-content {
flex: 1;
h3 {
margin: 0 0 $space-xs 0;
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
.banner-title {
font-size: 15px;
font-weight: 600;
color: $text-primary;
margin-bottom: 4px;
}
p {
margin: 0;
font-size: $font-size-sm;
.banner-desc {
font-size: 13px;
color: $text-secondary;
}
}
}
// 加载状态
.loading-container {
text-align: center;
padding: $space-xxl;
}
.no-tasks {
text-align: center;
padding: $space-xxl;
p {
margin: $space-lg 0;
font-size: $font-size-base;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
.loading-text {
margin-top: 16px;
color: $text-secondary;
font-size: 14px;
}
}
.tasks-container {
max-height: 60vh;
overflow-y: auto;
}
.tasks-list {
margin-bottom: $space-lg;
}
.task-item-card {
padding: $space-lg;
margin-bottom: $space-md;
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-md;
&:last-child {
margin-bottom: 0;
}
.task-header-row {
// 无任务状态
.no-tasks {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 20px;
.no-tasks-icon {
width: 72px;
height: 72px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-sm;
justify-content: center;
background: rgba(49, 240, 139, 0.1);
border-radius: 50%;
margin-bottom: 20px;
}
.no-tasks-title {
font-size: 20px;
font-weight: 600;
color: $text-primary;
margin-bottom: 8px;
}
.no-tasks-desc {
font-size: 14px;
color: $text-secondary;
margin-bottom: 24px;
}
}
.task-title {
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-primary;
}
.task-no {
font-size: $font-size-xs;
color: $text-tertiary;
// 任务容器
.tasks-container {
max-height: 55vh;
overflow-y: auto;
padding-right: 4px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
&:hover {
background: rgba(255, 255, 255, 0.25);
}
}
}
.task-meta {
// 任务列表
.tasks-list {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
// 任务卡片
.task-card {
position: relative;
background: linear-gradient(145deg, $bg-card 0%, rgba($bg-card, 0.8) 100%);
border: 1px solid $border-primary;
border-radius: 14px;
padding: 20px 20px 20px 56px;
transition: all 0.2s ease;
&:hover {
border-color: rgba($brand-primary, 0.3);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
// 任务序号
.task-index {
position: absolute;
left: 16px;
top: 20px;
width: 28px;
height: 28px;
display: flex;
gap: $space-lg;
margin-bottom: $space-md;
.meta-item {
align-items: center;
justify-content: center;
background: linear-gradient(135deg, $brand-primary 0%, darken($brand-primary, 10%) 100%);
border-radius: 8px;
font-size: 13px;
font-weight: 700;
color: #000;
}
// 任务信息
.task-info {
margin-bottom: 16px;
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 10px;
.task-title {
font-size: 15px;
font-weight: 600;
color: $text-primary;
line-height: 1.4;
flex: 1;
}
.task-no {
font-size: 11px;
color: $text-tertiary;
background: rgba(255, 255, 255, 0.06);
padding: 3px 8px;
border-radius: 4px;
white-space: nowrap;
}
}
.task-time {
display: flex;
flex-wrap: wrap;
gap: 16px;
.time-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: $text-tertiary;
.time-label {
color: $text-tertiary;
}
.time-value {
color: $text-secondary;
}
&.deadline {
.time-value {
color: #F59E0B;
}
}
}
}
}
// 进度区域
.progress-section {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
padding: 16px;
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.progress-title {
font-size: 13px;
font-weight: 600;
color: $text-primary;
}
.progress-hint {
font-size: 12px;
color: $brand-primary;
background: rgba($brand-primary, 0.1);
padding: 2px 10px;
border-radius: 10px;
}
}
.progress-control {
display: flex;
align-items: flex-start;
gap: 16px;
.slider-wrapper {
flex: 1;
position: relative;
padding-bottom: 24px;
.slider-labels {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
font-size: 11px;
color: $text-tertiary;
.label-current {
position: absolute;
transform: translateX(-50%);
color: rgba($brand-primary, 0.8);
white-space: nowrap;
}
}
}
.input-wrapper {
display: flex;
align-items: center;
gap: 6px;
.input-suffix {
font-size: 14px;
font-weight: 600;
color: $text-secondary;
}
}
}
.remark-section {
margin-top: 14px;
}
.error-tip {
display: flex;
align-items: center;
gap: 4px;
font-size: $font-size-xs;
color: $text-tertiary;
gap: 6px;
margin-top: 12px;
padding: 10px 14px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 8px;
font-size: 13px;
color: #EF4444;
}
}
.progress-input-row {
display: flex;
align-items: center;
margin-bottom: $space-md;
.progress-label {
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $text-secondary;
min-width: 80px;
}
:deep(.ant-slider) {
flex: 1;
margin: 0 $space-md;
}
}
.remark-input-row {
margin-bottom: $space-sm;
}
.error-message {
margin-top: $space-sm;
padding: $space-sm;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: $radius-sm;
color: #EF4444;
font-size: $font-size-xs;
}
}
.reminder-footer {
padding-top: $space-lg;
// 提交区域
.submit-section {
padding-top: 20px;
border-top: 1px solid $border-primary;
.footer-tip {
margin-top: $space-sm;
.submit-tip {
margin-top: 10px;
text-align: center;
font-size: $font-size-xs;
font-size: 12px;
color: $text-tertiary;
}
}
}
// 深度样式覆盖
:deep(.ant-slider) {
.ant-slider-rail {
background: rgba(255, 255, 255, 0.1);
height: 8px;
border-radius: 4px;
}
.ant-slider-track {
background: linear-gradient(90deg, $brand-primary 0%, lighten($brand-primary, 10%) 100%);
height: 8px;
border-radius: 4px;
}
.ant-slider-handle {
width: 20px;
height: 20px;
margin-top: -6px;
background: $brand-primary;
border: 3px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
&:hover, &:focus {
border-color: #fff;
box-shadow: 0 2px 12px rgba($brand-primary, 0.5);
}
}
.ant-slider-dot {
display: none;
}
.ant-slider-mark {
display: none;
}
}
:deep(.ant-input-number) {
width: 72px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
.ant-input-number-input {
color: $text-primary;
font-weight: 600;
font-size: 15px;
text-align: center;
}
.ant-input-number-handler-wrap {
background: rgba(255, 255, 255, 0.06);
border-left: 1px solid rgba(255, 255, 255, 0.1);
.ant-input-number-handler {
border-color: rgba(255, 255, 255, 0.1);
.ant-input-number-handler-up-inner,
.ant-input-number-handler-down-inner {
color: $text-secondary;
}
&:hover {
.ant-input-number-handler-up-inner,
.ant-input-number-handler-down-inner {
color: $brand-primary;
}
}
}
}
&:hover, &:focus {
border-color: rgba($brand-primary, 0.5);
}
}
:deep(.ant-input) {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: $text-primary;
&::placeholder {
color: $text-tertiary;
}
&:hover {
border-color: rgba(255, 255, 255, 0.2);
}
&:focus {
border-color: rgba($brand-primary, 0.5);
box-shadow: 0 0 0 2px rgba($brand-primary, 0.1);
}
}
:deep(.ant-input-textarea) {
.ant-input {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: $text-primary;
resize: none;
&::placeholder {
color: $text-tertiary;
}
&:hover {
border-color: rgba(255, 255, 255, 0.2);
}
&:focus {
border-color: rgba($brand-primary, 0.5);
box-shadow: 0 0 0 2px rgba($brand-primary, 0.1);
}
}
.ant-input-data-count {
color: $text-tertiary;
font-size: 11px;
}
}
:deep(.ant-spin) {
.ant-spin-dot-item {
background-color: $brand-primary;
}
}
:deep(.ant-btn-primary) {
height: 48px;
font-size: 15px;
font-weight: 600;
border-radius: 10px;
background: linear-gradient(135deg, $brand-primary 0%, darken($brand-primary, 8%) 100%);
border: none;
&:hover {
background: linear-gradient(135deg, lighten($brand-primary, 5%) 0%, $brand-primary 100%);
}
}
</style>
<style lang="scss">
// 全局模态框样式
.daily-progress-modal {
.ant-modal-content {
background: #1a1a1a;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
overflow: hidden;
}
.ant-modal-header {
background: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 20px 24px;
}
.ant-modal-title {
color: #fff;
}
.ant-modal-body {
padding: 24px;
}
}
</style>