493 lines
12 KiB
Vue

<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>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value task-no-value">
{{ task.taskNo }}
<Icon
name="copy"
size="xs"
class="copy-icon"
@click.stop="handleCopyTaskNo(task.taskNo)"
title="复制任务编号"
/>
</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>
<!-- 进度条(仅进行中任务显示) -->
<div v-if="showProgress" 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 v-if="$slots.actions" class="task-actions">
<slot name="actions" :task="task"></slot>
</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 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>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { message } from 'ant-design-vue'
import Icon from './Icon.vue'
import dayjs from 'dayjs'
import { useConfigStore } from '@/stores/config'
interface Task {
id: number
taskNo: string
title: string
taskTypeId: number
score: number
quantity?: number
status: string
progress?: number
isUrgent?: number
publisherName?: string
receiverName?: string
brandName?: string
categoryName?: string
platformName?: string
spuInfo?: string
taskSubType?: string
deliveryPath?: string
createTime: string
deadline?: string
completeTime?: string
submitTime?: string
}
interface Props {
task: Task
showPublisher?: boolean
showReceiver?: boolean
showProgress?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showPublisher: true,
showReceiver: true,
showProgress: false
})
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 formatDate = (date?: string) => {
if (!date) return '--'
return dayjs(date).format('YYYY-MM-DD HH:mm')
}
// 复制任务编号
const handleCopyTaskNo = async (taskNo: string) => {
try {
await navigator.clipboard.writeText(taskNo)
message.success('任务编号已复制')
} catch (error) {
// 降级方案:使用传统方法
const textarea = document.createElement('textarea')
textarea.value = taskNo
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
try {
document.execCommand('copy')
message.success('任务编号已复制')
} catch (e) {
message.error('复制失败')
}
document.body.removeChild(textarea)
}
}
</script>
<style lang="scss" scoped>
.task-card {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-lg;
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);
}
.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;
.task-title {
font-size: 18px;
font-weight: 600;
color: $text-primary;
margin: 0;
flex: 1;
margin-right: $space-md;
}
.task-badges {
display: flex;
gap: $space-xs;
flex-shrink: 0;
}
}
.task-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $space-sm;
margin-bottom: $space-md;
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.label {
font-size: 12px;
color: $text-secondary;
}
.value {
font-size: 14px;
font-weight: 500;
color: $text-primary;
&.text-brand {
color: #31f08b;
font-weight: 600;
}
&.task-no-value {
display: inline-flex;
align-items: center;
gap: 6px;
.copy-icon {
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
&:hover {
opacity: 1;
color: #31f08b;
}
}
}
}
}
}
.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>