493 lines
12 KiB
Vue
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>
|