refactor: 使用通用TaskCard组件重构任务大厅各个Tab

This commit is contained in:
Your Name 2026-01-06 22:01:53 +08:00
parent c2f6aef463
commit ab77e7486f
2 changed files with 514 additions and 822 deletions

View File

@ -1,123 +1,86 @@
<template>
<div :class="['task-card', 'chart-card', statusClass]" @click="$emit('click', task)">
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span :class="['badge', badgeClass]">
<Icon :name="badgeIcon" size="sm" :color="badgeIconColor" style="margin-right: 4px; vertical-align: middle;" />
{{ badgeText }}
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#F59E0B" style="margin-right: 4px; vertical-align: middle;" />
紧急
</span>
<div class="task-card-container" @click="$emit('click', task)">
<!-- 预览图区域 -->
<div v-if="task.previewImage" class="preview-cover">
<img :src="task.previewImage" alt="预览图" loading="lazy" @error="handleImageError" />
<div class="preview-overlay"></div>
<!-- 悬浮标签 -->
<div class="floating-badges">
<span v-if="task.isSubTask" class="badge-mini subtask">子任务</span>
<span v-if="isNewTask" class="badge-mini new">新发布</span>
<span v-if="task.isUrgent" class="badge-mini urgent">紧急</span>
<span v-if="mode === 'in_progress'" class="badge-mini progress">进行中</span>
<span v-if="mode === 'review'" class="badge-mini review">审核中</span>
<span v-if="mode === 'completed'" class="badge-mini completed">已完成</span>
</div>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
<div class="card-body">
<!-- 头部标签(无预览图时显示) -->
<div v-if="!task.previewImage" class="header-badges">
<span v-if="task.isSubTask" class="badge-tag subtask">子任务</span>
<span v-if="isNewTask" class="badge-tag new">新发布</span>
<span v-if="task.isUrgent" class="badge-tag urgent">紧急</span>
<span v-if="isHighScoreTask" class="badge-tag high-score">高分</span>
<span v-if="mode === 'in_progress'" class="badge-tag progress">进行中</span>
<span v-if="mode === 'review'" class="badge-tag review">待审核</span>
<span v-if="mode === 'completed'" class="badge-tag completed">已完成</span>
<span v-if="mode === 'published'" class="badge-tag published">我发起的</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">{{ scoreLabel }}</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div v-if="showPublisher" class="info-item">
<span class="label">发起人</span>
<span class="value">{{ task.publisherName || '--' }}</span>
</div>
<div v-if="showReceiver" class="info-item">
<span class="label">{{ receiverLabel }}</span>
<span class="value">{{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.deliveryPath" class="detail-item">
<span class="icon">
<Icon name="link" size="sm" color="#31f08b" />
</span>
<span>交付: {{ task.deliveryPath }}</span>
</div>
</div>
<!-- 标题 -->
<h3 class="card-title" :title="task.title">{{ task.title }}</h3>
<!-- 进度条(仅进行中任务显示) -->
<div v-if="showProgress" class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 0 }}%</span>
<!-- 核心指标 -->
<div class="metrics-row">
<div class="metric">
<span class="label">类型</span>
<span class="value type" :title="taskTypeName">
{{ taskTypeName }}
</span>
</div>
<div class="metric divider"></div>
<div class="metric">
<span class="label">{{ scoreLabel }}</span>
<span class="value score">{{ task.score || '--' }}</span>
</div>
<div class="metric divider"></div>
<div class="metric">
<span class="label">数量</span>
<span class="value">{{ task.quantity || 1 }}</span>
</div>
</div>
<a-progress
:percent="task.progress || 0"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 操作按钮 -->
<div v-if="$slots.actions" class="task-actions">
<slot name="actions" :task="task"></slot>
</div>
<!-- 详细信息 -->
<div class="details-grid">
<div class="detail-item" v-if="task.brandName || task.categoryName">
<Icon name="tag" size="xs" class="icon" />
<span class="text text-ellipsis">
{{ task.brandName }} <span v-if="task.brandName && task.categoryName">·</span> {{ task.categoryName }}
</span>
</div>
<div class="detail-item" v-if="task.platformName || task.spuInfo">
<Icon name="monitor" size="xs" class="icon" />
<span class="text text-ellipsis">
{{ task.platformName || '--' }} <span v-if="task.spuInfo">· {{ task.spuInfo }}</span>
</span>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
<div class="detail-item">
<Icon name="user" size="xs" class="icon" />
<span class="text text-ellipsis">{{ personName }}</span>
</div>
<div class="detail-item">
<Icon name="clock" size="xs" class="icon" />
<span class="text">{{ timeLabel }}: {{ timeValue }}</span>
</div>
</div>
<div v-if="task.deadline && task.status === 'in_progress'" class="footer-item">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">截止: {{ formatDate(task.deadline) }}</span>
</div>
<div v-if="task.completeTime && task.status === 'completed'" class="footer-item">
<Icon name="check" size="sm" color="#31f08b" />
<span style="color: #ffffff;">完成: {{ formatDate(task.completeTime) }}</span>
</div>
<div v-if="task.submitTime && task.status === 'review'" class="footer-item">
<Icon name="upload" size="sm" color="#31f08b" />
<span style="color: #ffffff;">提交: {{ formatDate(task.submitTime) }}</span>
<!-- 底部操作栏 -->
<div class="card-actions">
<slot name="actions"></slot>
</div>
</div>
</div>
@ -125,29 +88,33 @@
<script setup lang="ts">
import { computed } from 'vue'
import Icon from './Icon.vue'
import Icon from '@/components/Icon.vue'
import dayjs from 'dayjs'
import { useConfigStore } from '@/stores/config'
// 任务数据接口
interface Task {
id: number
taskNo: string
title: string
taskTypeId: number
taskTypeName?: string
taskSubType?: string
score: number
quantity?: number
status: string
status?: string
progress?: number
isUrgent?: number
isSubTask?: boolean
publisherName?: string
receiverName?: string
brandName?: string
categoryName?: string
platformName?: string
spuInfo?: string
taskSubType?: string
deliveryPath?: string
createTime: string
previewImage?: string
estimatedHours?: number
createTime?: string
deadline?: string
completeTime?: string
submitTime?: string
@ -155,288 +122,428 @@ interface Task {
interface Props {
task: Task
showPublisher?: boolean
showReceiver?: boolean
showProgress?: boolean
mode?: 'available' | 'in_progress' | 'review' | 'completed' | 'published'
}
const props = withDefaults(defineProps<Props>(), {
showPublisher: true,
showReceiver: true,
showProgress: false
mode: 'available'
})
defineEmits(['click'])
const configStore = useConfigStore()
// 状态相关计算属性
const statusClass = computed(() => {
const statusMap: Record<string, string> = {
'pending': 'available',
'in_progress': 'in-progress',
'review': 'review',
'completed': 'completed',
'rejected': 'rejected'
}
return statusMap[props.task.status] || ''
})
const badgeClass = computed(() => {
const classMap: Record<string, string> = {
'pending': 'badge-new',
'in_progress': 'badge-progress',
'review': 'badge-review',
'completed': 'badge-completed',
'rejected': 'badge-rejected'
}
return classMap[props.task.status] || 'badge-new'
})
const badgeIcon = computed(() => {
const iconMap: Record<string, string> = {
'pending': 'sparkles',
'in_progress': 'clock',
'review': 'file-text',
'completed': 'check-circle',
'rejected': 'x-circle'
}
return iconMap[props.task.status] || 'sparkles'
})
const badgeIconColor = computed(() => {
const colorMap: Record<string, string> = {
'pending': '#31f08b',
'in_progress': '#3B82F6',
'review': '#F59E0B',
'completed': '#31f08b',
'rejected': '#EF4444'
}
return colorMap[props.task.status] || '#31f08b'
})
const badgeText = computed(() => {
const textMap: Record<string, string> = {
'pending': '待接单',
'in_progress': '进行中',
'review': '待审核',
'completed': '已完成',
'rejected': '已驳回'
}
return textMap[props.task.status] || '新发布'
})
const scoreLabel = computed(() => {
return props.task.status === 'completed' ? '获得积分' : '预估积分'
})
const receiverLabel = computed(() => {
return props.task.status === 'completed' ? '完成人' : '接单人'
})
// 获取任务类型名称
const getTaskTypeName = (taskTypeId: number) => {
const type = configStore.demandTypes.find(t => t.id === taskTypeId)
return type?.name || '--'
const taskTypeName = computed(() => {
// 优先显示任务子类型
if (props.task.taskSubType) return props.task.taskSubType
// 其次显示后端返回的任务类型名称
if (props.task.taskTypeName) return props.task.taskTypeName
// 最后尝试从 store 中查找
const findInTree = (items: any[], id: number): any => {
for (const item of items) {
if (item.id === id) return item
if (item.children) {
const found = findInTree(item.children, id)
if (found) return found
}
}
return null
}
const type = findInTree(configStore.demandTypes, props.task.taskTypeId)
return type?.name || '未知类型'
})
// 是否为新发布任务(24小时内)
const isNewTask = computed(() => {
if (!props.task.createTime) return false
const createTime = new Date(props.task.createTime).getTime()
const now = new Date().getTime()
return (now - createTime) < 24 * 60 * 60 * 1000
})
// 是否为高分任务
const isHighScoreTask = computed(() => {
return (props.task.score || 0) >= 10
})
// 积分标签
const scoreLabel = computed(() => {
return props.mode === 'completed' ? '获得' : '积分'
})
// 人员显示
const personName = computed(() => {
if (props.mode === 'available' || props.mode === 'published') {
return props.task.publisherName || '未知发布人'
}
return props.task.receiverName || '未知接单人'
})
// 时间标签
const timeLabel = computed(() => {
switch (props.mode) {
case 'available': return '时效'
case 'in_progress': return '截止'
case 'review': return '提交'
case 'completed': return '完成'
case 'published': return '发布'
default: return '时间'
}
})
// 时间值
const timeValue = computed(() => {
switch (props.mode) {
case 'available':
return formatEstimatedTime(props.task.estimatedHours)
case 'in_progress':
return formatDeadline(props.task.deadline)
case 'review':
return formatDate(props.task.submitTime)
case 'completed':
return formatDate(props.task.completeTime)
case 'published':
return formatDate(props.task.createTime)
default:
return '--'
}
})
// 格式化工时
const formatEstimatedTime = (hours: number | undefined) => {
if (!hours) return '--'
const WORK_HOURS_PER_DAY = 8
const days = Math.floor(hours / WORK_HOURS_PER_DAY)
const remainingHours = Math.round((hours % WORK_HOURS_PER_DAY) * 100) / 100
if (days === 0) {
return Number.isInteger(remainingHours) ? `${remainingHours}小时` : `${remainingHours.toFixed(1)}小时`
} else if (remainingHours === 0) {
return `${days}天`
} else {
const hourStr = Number.isInteger(remainingHours) ? `${remainingHours}小时` : `${remainingHours.toFixed(1)}小时`
return `${days}天${hourStr}`
}
}
// 格式化截止时间
const formatDeadline = (deadline: string | undefined) => {
if (!deadline) return '--'
const deadlineTime = dayjs(deadline)
const now = dayjs()
const diffHours = deadlineTime.diff(now, 'hour')
if (diffHours < 0) {
return `已逾期`
} else if (diffHours < 24) {
return `${diffHours}小时后`
} else {
const diffDays = Math.floor(diffHours / 24)
return `${diffDays}天后`
}
}
// 格式化日期
const formatDate = (date?: string) => {
if (!date) return '--'
return dayjs(date).format('YYYY-MM-DD HH:mm')
return dayjs(date).format('MM-DD HH:mm')
}
// 图片加载错误处理
const handleImageError = (e: Event) => {
const img = e.target as HTMLImageElement
img.style.display = 'none' // 隐藏错误的图片
// 或者可以设置为默认图片
// img.src = '/default-preview.png'
}
</script>
<style lang="scss" scoped>
.task-card {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-lg;
<style scoped lang="scss">
.task-card-container {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
background: #1e1e1e;
border: 1px solid #333;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: all $transition-base;
&:hover {
background: $bg-card-hover;
border-color: #31f08b;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(49, 240, 139, 0.15);
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
border-color: #444;
.card-title {
color: #31f08b;
}
}
}
.preview-cover {
position: relative;
height: 160px;
overflow: hidden;
background: #141414;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $space-md;
padding-bottom: $space-md;
border-bottom: 1px solid $border-primary;
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.4) 100%);
opacity: 0.8;
transition: opacity 0.3s ease;
}
.task-title {
&:hover {
img {
transform: scale(1.08);
}
.preview-overlay {
opacity: 0.6;
}
}
}
.floating-badges {
position: absolute;
top: 12px;
left: 12px;
display: flex;
gap: 6px;
flex-wrap: wrap;
z-index: 2;
}
.badge-mini {
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
letter-spacing: 0.5px;
&.subtask { background: rgba(24, 144, 255, 0.85); border-color: rgba(24, 144, 255, 0.3); }
&.new { background: rgba(49, 240, 139, 0.85); color: #000; border-color: rgba(49, 240, 139, 0.3); }
&.urgent { background: rgba(245, 34, 45, 0.85); border-color: rgba(245, 34, 45, 0.3); }
&.progress { background: rgba(59, 130, 246, 0.85); border-color: rgba(59, 130, 246, 0.3); }
&.review { background: rgba(245, 158, 11, 0.85); border-color: rgba(245, 158, 11, 0.3); }
&.completed { background: rgba(16, 185, 129, 0.85); border-color: rgba(16, 185, 129, 0.3); }
}
.card-body {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.header-badges {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.badge-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.3px;
&.subtask { background: rgba(24, 144, 255, 0.1); color: #40a9ff; border: 1px solid rgba(24, 144, 255, 0.2); }
&.new { background: rgba(49, 240, 139, 0.1); color: #31f08b; border: 1px solid rgba(49, 240, 139, 0.2); }
&.urgent { background: rgba(245, 34, 45, 0.1); color: #ff4d4f; border: 1px solid rgba(245, 34, 45, 0.2); }
&.high-score { background: rgba(250, 173, 20, 0.1); color: #ffc53d; border: 1px solid rgba(250, 173, 20, 0.2); }
&.progress { background: rgba(59, 130, 246, 0.1); color: #60A5FA; border: 1px solid rgba(59, 130, 246, 0.2); }
&.review { background: rgba(245, 158, 11, 0.1); color: #FBBF24; border: 1px solid rgba(245, 158, 11, 0.2); }
&.completed { background: rgba(16, 185, 129, 0.1); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.2); }
&.published { background: rgba(139, 92, 246, 0.1); color: #A78BFA; border: 1px solid rgba(139, 92, 246, 0.2); }
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #fff;
line-height: 1.5;
margin: 0 0 20px;
// 限制2行
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: 48px;
transition: color 0.3s ease;
letter-spacing: 0.2px;
}
.metrics-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: #262626;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #333;
}
.metric {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
&.divider {
width: 1px;
height: 24px;
background: #3a3a3a;
flex: 0 0 1px;
margin: 0 12px;
}
.label {
font-size: 11px;
color: #888;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
font-size: 13px;
color: #e0e0e0;
font-weight: 500;
max-width: 90px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
&.score {
color: #31f08b;
font-size: 18px;
font-weight: 600;
color: $text-primary;
margin: 0;
flex: 1;
margin-right: $space-md;
font-weight: 700;
text-shadow: 0 0 10px rgba(49, 240, 139, 0.2);
}
.task-badges {
display: flex;
gap: $space-xs;
flex-shrink: 0;
&.type {
color: #fff;
}
}
}
.task-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $space-sm;
margin-bottom: $space-md;
.details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
margin-bottom: 24px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #999;
min-width: 0; // 防止溢出
.icon {
color: #555;
flex-shrink: 0;
}
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.text-ellipsis {
flex: 1;
}
}
.label {
font-size: 12px;
color: $text-secondary;
.card-actions {
display: flex;
gap: 12px;
margin-top: auto;
:deep(button) {
flex: 1;
height: 40px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: none;
letter-spacing: 0.5px;
display: flex;
align-items: center;
justify-content: center;
&.secondary {
background: #2a2a2a;
color: #ccc;
border: 1px solid #3a3a3a;
&:hover {
background: #333;
color: #fff;
border-color: #444;
}
.value {
font-size: 14px;
font-weight: 500;
color: $text-primary;
&.text-brand {
color: #31f08b;
font-weight: 600;
}
&.primary {
background: #31f08b;
color: #000;
box-shadow: 0 2px 8px rgba(49, 240, 139, 0.2);
&:hover {
background: #4de19b;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 240, 139, 0.3);
}
&:active {
transform: translateY(0);
}
&.disabled {
background: #333;
color: #666;
cursor: not-allowed;
border: 1px solid #444;
box-shadow: none;
&:hover {
transform: none;
background: #333;
}
}
}
}
.task-details {
display: flex;
flex-direction: column;
gap: $space-xs;
margin-bottom: $space-md;
padding: $space-sm;
background: $bg-secondary;
border-radius: $radius-md;
.info-item {
display: flex;
justify-content: space-between;
font-size: 13px;
.label {
color: $text-secondary;
}
.value {
color: $text-primary;
font-weight: 500;
}
}
.detail-item {
display: flex;
align-items: center;
gap: $space-xs;
font-size: 13px;
color: $text-secondary;
.icon {
display: flex;
align-items: center;
}
}
}
.task-progress {
margin-bottom: $space-md;
.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: $space-xs;
font-size: 13px;
span:first-child {
color: $text-secondary;
}
span:last-child {
color: #31f08b;
font-weight: 600;
}
}
}
.task-actions {
display: flex;
gap: $space-sm;
margin-bottom: $space-md;
button {
flex: 1;
}
}
.task-footer {
display: flex;
gap: $space-md;
padding-top: $space-md;
border-top: 1px solid $border-primary;
.footer-item {
display: flex;
align-items: center;
gap: $space-xs;
font-size: 12px;
color: $text-secondary;
}
}
.badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: $radius-full;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
&.badge-new {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
}
&.badge-progress {
background: rgba(59, 130, 246, 0.1);
color: #3B82F6;
}
&.badge-review {
background: rgba(245, 158, 11, 0.1);
color: #F59E0B;
}
&.badge-completed {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
}
&.badge-rejected {
background: rgba(239, 68, 68, 0.1);
color: #EF4444;
}
&.badge-urgent {
background: rgba(245, 158, 11, 0.1);
color: #F59E0B;
}
}
}
</style>
</style>

View File

@ -739,157 +739,25 @@
</div>
<div v-else class="task-grid">
<div
<TaskCard
v-for="task in inProgressTaskList"
:key="task.id"
class="task-card chart-card in-progress"
:task="task"
mode="in_progress"
@click="handleTaskClick(task)"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span v-if="task.isSubTask" class="badge badge-subtask">
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
子任务
</span>
<span v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
<Icon name="target" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
指定
</span>
<span class="badge badge-in-progress">
<Icon name="clock" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
进行中
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>发起人: {{ task.publisherName || '--' }}</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>接单人: {{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 进度条 -->
<div class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 0 }}%</span>
</div>
<a-progress
:percent="task.progress || 0"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click="handleUpdateProgress(task)">
更新进度
</a-button>
<a-button v-if="canCompleteTask(task)" type="primary" size="small" @click="handleCompleteTask(task)">
完结任务
</a-button>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.deadline">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">截止: {{ formatDate(task.deadline) }}</span>
</div>
</div>
</div>
<template #actions>
<button class="action-btn secondary" @click.stop="handleTaskClick(task)">
查看详情
</button>
<button
class="action-btn primary"
@click.stop="handleSubmitTask(task)"
>
提交任务
</button>
</template>
</TaskCard>
</div>
<!-- 分页 -->
@ -1063,146 +931,19 @@
</div>
<div v-else class="task-grid">
<div
<TaskCard
v-for="task in completedTaskList"
:key="task.id"
class="task-card chart-card completed"
:task="task"
mode="completed"
@click="handleTaskClick(task)"
style="cursor: pointer;"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span class="badge badge-completed">
<Icon name="check-circle" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
已完成
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">获得积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>发起人: {{ task.publisherName || '--' }}</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>完成人: {{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.deliveryPath" class="detail-item">
<span class="icon">
<Icon name="link" size="sm" color="#31f08b" />
</span>
<span>交付: {{ task.deliveryPath }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 进度条(已完成任务显示100%) -->
<div class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 100 }}%</span>
</div>
<a-progress
:percent="task.progress || 100"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
</div>
<div class="footer-item">
<Icon name="check" size="sm" color="#31f08b" />
<span style="color: #ffffff;">完成: {{ formatDate(task.completeTime) }}</span>
</div>
</div>
</div>
<template #actions>
<button class="action-btn secondary" @click.stop="handleTaskClick(task)">
查看详情
</button>
</template>
</TaskCard>
</div>
<!-- 分页 -->
@ -1383,190 +1124,33 @@
</div>
<div v-else class="task-grid">
<div
<TaskCard
v-for="task in publishedTaskList"
:key="task.id"
class="task-card chart-card"
:class="[`status-${task.status}`]"
:task="task"
mode="published"
@click="handleTaskClick(task)"
style="cursor: pointer;"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span :class="['badge', `status-${task.approvalStatus?.includes('rejected') ? 'rejected' : task.status}`]">
{{ getTaskStatusText(task.status, task) }}
</span>
<span v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
<Icon name="target" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
指定
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
紧急
</span>
</div>
</div>
<!-- 驳回原因显示 -->
<div v-if="task.approvalStatus?.includes('rejected') && task.rejectReason" class="reject-info">
<span class="reject-label">驳回原因:</span>
<span class="reject-reason">{{ task.rejectReason }}</span>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>处理人: {{ task.receiverName || '待接单' }}</span>
</div>
<!-- 子任务进度(流水线任务) -->
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="detail-item subtask-progress">
<span class="icon">
<Icon name="layers" size="sm" color="#31f08b" />
</span>
<span class="subtask-stats">
子任务:
<span class="stat completed">已完成 {{ task.subTaskCompleted || 0 }}</span>
<span class="stat in-progress">进行中 {{ task.subTaskInProgress || 0 }}</span>
<span class="stat pending">待接单 {{ task.subTaskPending || 0 }}</span>
<span class="stat total">/ 共 {{ task.subTaskTotal }}</span>
</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 进度条(进行中和已完成的任务显示) -->
<div v-if="task.status === 'in_progress' || task.status === 'completed'" class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 0 }}%</span>
</div>
<a-progress
:percent="task.progress || 0"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatTime(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.deadline">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">截止: {{ formatTime(task.deadline) }}</span>
</div>
<!-- 驳回时间 -->
<div class="footer-item" v-if="task.approvalStatus?.includes('rejected') && task.updateTime">
<Icon name="x-circle" size="sm" color="#f5222d" />
<span style="color: #f5222d;">驳回: {{ formatTime(task.updateTime) }}</span>
</div>
</div>
<!-- 被驳回或草稿任务的操作按钮 -->
<div v-if="['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button
type="primary"
size="small"
<template #actions>
<!-- 被驳回或草稿任务的操作按钮 -->
<button
v-if="['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')"
class="action-btn primary"
@click.stop="handleEditRejectedTask(task)"
style="width: 100%;">
<Icon name="edit" size="sm" style="margin-right: 4px;" />
>
重新编辑提交
</a-button>
</div>
<!-- 已完成任务:查看详情和交付信息 -->
<div v-if="task.status === 'completed'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button
type="primary"
size="small"
</button>
<!-- 其他状态:查看详情 -->
<button
v-else
class="action-btn secondary"
@click.stop="handleTaskClick(task)"
style="width: 100%;">
<Icon name="eye" size="sm" style="margin-right: 4px;" />
>
查看详情
</a-button>
</div>
</div>
</button>
</template>
</TaskCard>
</div>
<!-- 分页 -->
@ -2198,6 +1782,7 @@ import { useRouter } from 'vue-router'
import { message, Modal, InputNumber, Textarea, Radio, RadioGroup, Button, Select, Space, Tag } from 'ant-design-vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import Icon from '@/components/Icon.vue'
import TaskCard from '@/components/TaskCard.vue'
import * as taskApi from '@/api/task'
import * as userApi from '@/api/user'
import type { Task, TaskApprovalDTO, TaskProduct } from '@/api/task'