8734 lines
310 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<DefaultLayout>
<div class="task-hall-page">
<div class="container">
<!-- 数据总览 -->
<div class="overview-section">
<div class="overview-header">
<h2 class="section-title">数据总览</h2>
<div class="overview-actions">
<!-- 部门视角切换(仅当用户管理多个部门时显示) -->
<a-select
v-if="managedDepartments.length > 0"
v-model:value="currentDeptView"
style="width: 120px; margin-right: 8px;"
size="middle"
@change="handleDeptViewChange"
class="dept-view-select"
>
<a-select-option :value="undefined">
{{ userDeptName }}
</a-select-option>
<a-select-option v-for="dept in managedDepartments" :key="dept.deptId" :value="dept.deptId">
{{ dept.deptName }}
</a-select-option>
</a-select>
<a-month-picker
v-model:value="selectedMonth"
placeholder="选择月份"
format="YYYY-MM"
@change="handleMonthChange"
style="width: 120px; margin-right: 8px;"
size="middle"
/>
<a-button type="primary" size="middle" @click="handleCreateTask">
<Icon name="plus" size="xs" style="margin-right: 4px;" />
发起任务
</a-button>
<a-button
v-if="canExportTaskHall"
type="default"
size="middle"
@click="handleExportTaskHall"
:loading="exportLoading"
style="margin-left: 8px;"
>
<Icon name="download" size="xs" style="margin-right: 4px;" />
一键导出
</a-button>
</div>
</div>
<div class="stats-grid">
<div
v-for="card in dataCards"
:key="card.id"
class="stat-card"
>
<div class="stat-icon">
<Icon :name="card.iconName" size="xl" color="#31f08b" />
</div>
<div class="stat-content">
<div class="stat-label">{{ card.label }}</div>
<div class="stat-value">{{ card.value }}</div>
</div>
</div>
</div>
</div>
<!-- 标签页切换 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange" class="task-tabs">
<a-tab-pane key="available" tab="可接任务">
<!-- 搜索和筛选栏(使用公共组件) -->
<TaskFilters
:model-value="filters"
@update:model-value="(value) => Object.assign(filters, value)"
:stats-text="`共 ${total} 个任务`"
:sort-options="availableSortOptions"
:status-options="availableStatusOptions"
status-placeholder="任务状态"
:disable-search="showBlindBox"
:search-placeholder="showBlindBox ? '盲盒模式下不可搜索' : '搜索任务编号、标题、SPU信息、发布人...'"
:show-receiving-dept="true"
:demand-types="demandTypes"
:task-sub-types="taskSubTypes"
:brands="brands"
:categories="categories"
:platforms="platforms"
:receiving-dept-list="receivingDeptList"
@change="handleFilterChange"
@reset="handleReset"
/>
<!-- 任务列表 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="taskList.length === 0" class="empty-state chart-card">
<div class="empty-icon">
<Icon name="inbox" size="xl" color="#6B6B6B" />
</div>
<h3>暂无可接任务</h3>
<p>当前没有待接单的任务,请稍后再来查看</p>
</div>
<div v-else class="task-grid">
<div
v-for="(task, index) in displayTaskList"
:key="task.id"
:class="['task-card', 'chart-card', { 'blind-box-card': showBlindBox }]"
@click="handleTaskClick(task)"
>
<!-- 盲盒模式显示 -->
<template v-if="showBlindBox">
<div class="blind-box-content">
<div class="blind-box-visual">
<div class="box-icon-wrapper">
<Icon name="box" class="main-box-icon" />
<div class="question-mark">?</div>
</div>
<div class="blind-box-glow"></div>
</div>
<div class="blind-box-info">
<div class="blind-box-queue">
<span class="queue-label">排队序号</span>
<span class="queue-number">#{{ index + 1 }}</span>
</div>
<!-- 可接单部门信息 -->
<div v-if="getTaskAllowedDeptNames(task).length > 0" class="blind-box-depts">
<span class="depts-label">
<Icon name="building" size="sm" color="#31f08b" style="margin-right: 4px;" />
可接单部门
</span>
<div class="depts-tags">
<span
v-for="(deptName, idx) in getTaskAllowedDeptNames(task)"
:key="idx"
class="dept-tag"
>
{{ deptName }}
</span>
</div>
</div>
</div>
<div class="blind-box-action">
<a-button type="primary" size="middle" class="blind-box-grab-btn" @click.stop="handleGrabTask(task)">
<Icon name="zap" size="sm" style="margin-right: 6px;" />
立即抢单
</a-button>
<div class="blind-box-hint">
<Icon name="lock" size="xs" color="#999" style="margin-right: 4px;" />
抢单后解锁任务详情
</div>
</div>
</div>
</template>
<!-- 正常模式显示 -->
<template v-else>
<!-- 任务标题 -->
<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="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
子任务
</span>
<span v-if="isNewTask(task)" class="badge badge-new">
<Icon name="sparkles" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
新发布
</span>
<span v-if="isHighScoreTask(task)" class="badge badge-high-score">
<Icon name="star" size="sm" color="#31f08b" 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="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-wrapper">
<div class="task-info">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ formatScore(task.totalScore || task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || 1 }} 个</span>
</div>
</div>
<!-- 预览图 -->
<div v-if="task.previewImage" class="preview-thumbnail">
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
<div class="preview-popup">
<img :src="task.previewImage" alt="预览图" />
</div>
</div>
</div>
<!-- 🔥 可接单部门标签 -->
<div v-if="getTaskAllowedDeptNames(task).length > 0" class="allowed-depts-section">
<span class="allowed-depts-label">可接单部门:</span>
<span class="allowed-depts-tags">
<span
v-for="(deptName, idx) in getTaskAllowedDeptNames(task)"
:key="idx"
class="dept-tag"
>
{{ deptName }}
</span>
</span>
</div>
<!-- 任务类型明细 -->
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
<div class="type-details-title">任务类型明细</div>
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<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="tag" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<!-- 一级部门需求人(如果是一级需求部门发起的任务,优先显示) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.primaryPublisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#faad14" />
</span>
<Tooltip :title="`一级需求人: ${task.primaryPublisherName} (${task.primaryPublisherDeptName || '一级部门'})`">
<span style="color: #faad14; cursor: help;">一级需求人: {{ truncateText(task.primaryPublisherName, 8) }} ({{ truncateText(task.primaryPublisherDeptName, 8) || '一级部门' }})</span>
</Tooltip>
</div>
<!-- 明细完善人(一级需求任务且有明细完善人) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.detailCompleterName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#52c41a" />
</span>
<Tooltip :title="`需求部门发起人: ${task.detailCompleterName}`">
<span style="color: #52c41a; cursor: help;">需求部门发起人: {{ truncateText(task.detailCompleterName, 8) }}</span>
</Tooltip>
</div>
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
<div v-if="!(Number(task.isPrimaryTask) === 1 && task.detailCompleterName)" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<Tooltip v-if="task.publisherName" :title="`发布人: ${task.publisherName}`">
<span style="cursor: help;">发布人: {{ truncateText(task.publisherName, 8) || '未知' }}</span>
</Tooltip>
<span v-else>发布人: 未知</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item detail-item-attachment">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span v-if="canViewAvailableTaskAttachmentInfo">
附件:
<a
v-for="(url, index) in parseAttachmentUrls(task.attachmentUrl)"
:key="`available-attachment-${task.id}-${index}`"
:href="url"
target="_blank"
rel="noopener noreferrer"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
<span v-else>
附件:
<span class="attachment-locked">
<Icon name="lock" size="xs" style="margin-right: 4px;" />
{{ parseAttachmentUrls(task.attachmentUrl).length }}个附件(接单后可下载)
</span>
</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span>时效要求: {{ formatEstimatedTime(task.estimatedHours) }}</span>
</div>
<div class="detail-item" :title="formatFullDateTime(task.createTime)">
<span class="icon">
<Icon name="calendar" size="sm" color="#31f08b" />
</span>
<span>发布时间: {{ formatTime(task.createTime) }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<button
class="btn btn-secondary"
style="flex: 1"
@click.stop="handleTaskClick(task)"
>
查看详情
</button>
<button
v-if="canEditAvailableTaskDetail(task)"
class="btn btn-secondary"
style="flex: 1"
@click.stop="handleEditAvailableTaskDetail(task)"
>
编辑明细
</button>
<button
:class="[
'btn',
canGrabTask(task).canGrab ? 'btn-primary' : 'btn-disabled'
]"
:style="{
flex: 1,
backgroundColor: canGrabTask(task).canGrab ? '#31f08b' : '#666',
cursor: canGrabTask(task).canGrab ? 'pointer' : 'not-allowed',
opacity: canGrabTask(task).canGrab ? 1 : 0.6
}"
:disabled="!canGrabTask(task).canGrab"
:title="canGrabTask(task).canGrab ? '点击抢单' : canGrabTask(task).reason"
@click.stop="handleGrab(task)"
>
{{ canGrabTask(task).canGrab ? '抢单' : (task.unlockStatus === 'queuing' ? `排队中(第${task.queuePosition || 0}位)` : '不可接单') }}
</button>
</div>
<!-- 任务编号 -->
<div class="task-footer">
<div class="footer-item task-no-item">
<Icon name="hash" size="sm" color="#31f08b" />
<span style="color: #ffffff;">编号: {{ task.isSubTask ? task.itemNo : task.taskNo }}</span>
<Icon
name="copy"
size="xs"
class="copy-icon"
@click.stop="handleCopyTaskNo(task.isSubTask ? task.itemNo : task.taskNo)"
title="复制任务编号"
/>
</div>
</div>
</template>
</div>
</div>
<!-- 分页 -->
<div v-if="total > 0" class="pagination">
<a-pagination
v-model:current="pagination.page"
v-model:page-size="pagination.pageSize"
:total="total"
:show-size-changer="false"
@change="handlePageChange"
/>
</div>
</a-tab-pane>
<!-- 待审核任务标签页 -->
<a-tab-pane key="review">
<template #tab>
<span>待审核任务 <span class="tab-count">({{ reviewTotal }})</span></span>
</template>
<!-- 搜索和筛选栏(使用公共组件) -->
<TaskFilters
:model-value="reviewFilters"
@update:model-value="(value) => Object.assign(reviewFilters, value)"
:stats-text="`共 ${reviewTotal} 个待审核任务`"
:sort-options="reviewSortOptions"
:status-options="reviewStatusOptions"
status-placeholder="审核状态"
time-range-placeholder="提交时间"
:show-task-sub-type="false"
:demand-types="demandTypes"
:task-sub-types="taskSubTypes"
:brands="brands"
:categories="categories"
:platforms="platforms"
@change="loadReviewTasks"
@reset="handleReviewFiltersReset"
/>
<!-- 待审核任务列表 -->
<div v-if="reviewLoading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="reviewTaskList.length === 0" class="empty-state chart-card">
<div class="empty-icon">
<Icon name="file-text" size="xl" color="#6B6B6B" />
</div>
<h3>暂无待审核任务</h3>
<p>还没有需要审核的任务</p>
</div>
<div v-else class="task-grid">
<div
v-for="task in reviewTaskList"
:key="task.id"
class="task-card chart-card review"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span v-if="task._itemType === 'taskGroup'" class="badge badge-subtask">
<Icon name="layers" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
子任务组 - 第{{ task._groupData?.groupNo }}组
</span>
<span v-else class="badge badge-review">
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
待审核
</span>
<!-- 加急图标 -->
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#ff4d4f" style="margin-right: 4px; vertical-align: middle;" />
加急
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStageForReview(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStageForReview(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info-wrapper">
<div class="task-info task-info-3col">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ formatScore(task.totalScore || task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">{{ task._itemType === 'taskGroup' ? '子任务数' : '数量' }}</span>
<span class="value">{{ task._itemType === 'taskGroup' ? (task._groupData?.itemCount || 1) : (task.quantity || '--') }} 个</span>
</div>
</div>
<!-- 预览图 -->
<div v-if="task.previewImage" class="preview-thumbnail">
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
<div class="preview-popup">
<img :src="task.previewImage" alt="预览图" />
</div>
</div>
</div>
<!-- 任务类型明细 -->
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
<div class="type-details-title">任务类型明细</div>
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<!-- 一级部门需求人(如果是一级需求部门发起的任务,优先显示) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.primaryPublisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#faad14" />
</span>
<Tooltip :title="`一级需求人: ${task.primaryPublisherName} (${task.primaryPublisherDeptName || '一级部门'})`">
<span style="color: #faad14; cursor: help;">一级需求人: {{ truncateText(task.primaryPublisherName, 8) }} ({{ truncateText(task.primaryPublisherDeptName, 8) || '一级部门' }})</span>
</Tooltip>
</div>
<!-- 明细完善人(一级需求任务且有明细完善人) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.detailCompleterName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#52c41a" />
</span>
<Tooltip :title="`需求部门发起人: ${task.detailCompleterName}`">
<span style="color: #52c41a; cursor: help;">需求部门发起人: {{ truncateText(task.detailCompleterName, 8) }}</span>
</Tooltip>
</div>
<!-- 发起人:非一级需求任务或一级需求任务无明细完善人时显示 -->
<div v-if="!(Number(task.isPrimaryTask) === 1 && task.detailCompleterName)" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<Tooltip v-if="task.publisherName" :title="`发起人: ${task.publisherName}`">
<span style="cursor: help;">发起人: {{ truncateText(task.publisherName, 8) || '--' }}</span>
</Tooltip>
<span v-else>发起人: --</span>
</div>
<div v-if="task._itemType !== 'taskGroup'" 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 v-if="task._itemType === 'taskGroup' && task.prevDownloadUrls && task.prevDownloadUrls.length > 0" class="detail-item">
<span class="icon">
<Icon name="download" size="sm" color="#31f08b" />
</span>
<span>前置素材:
<a
v-for="(url, index) in task.prevDownloadUrls"
:key="index"
:href="url"
target="_blank"
class="attachment-link download-link"
@click.stop
>
下载{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 任务编号(底部) -->
<div class="task-no-section">
<Icon name="hash" size="sm" color="#31f08b" />
<span style="color: #ffffff;">{{ task.taskNo || '--' }}</span>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<template v-if="task._itemType === 'taskGroup'">
<a-button type="primary" size="small" @click="handleApproveTaskGroupFromReview(task)">
<Icon name="check" size="sm" style="margin-right: 4px;" />
审核通过
</a-button>
<a-button danger size="small" @click="handleRejectTaskGroupFromReview(task)">
<Icon name="x" size="sm" style="margin-right: 4px;" />
驳回
</a-button>
</template>
<template v-else>
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
<!-- 待完善明细状态:显示重新编辑按钮 -->
<a-button v-if="canEditDetailTask(task)" type="primary" size="small" @click="handleEditDetailTask(task)">
<Icon name="edit" size="sm" style="margin-right: 4px;" />
重新编辑
</a-button>
<a-button v-else-if="canApproveTask(task)" type="primary" size="small" @click="handleApproveTask(task)">
通过审核
</a-button>
<a-button v-if="canRejectTask(task)" danger size="small" @click="handleRejectTask(task)">
驳回任务
</a-button>
</template>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">提交: {{ formatDate(task.submitTime || task.updateTime || task.createTime) }}</span>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="reviewTotal > 0" class="pagination">
<a-pagination
v-model:current="reviewPagination.page"
v-model:page-size="reviewPagination.pageSize"
:total="reviewTotal"
:show-size-changer="false"
@change="handleReviewPageChange"
/>
</div>
</a-tab-pane>
<!-- 进行中任务标签页 -->
<a-tab-pane key="inProgress">
<template #tab>
<span>进行中任务 <span class="tab-count">({{ inProgressTotal }})</span></span>
</template>
<!-- 搜索和筛选栏(使用公共组件) -->
<TaskFilters
:model-value="inProgressFilters"
@update:model-value="(value) => Object.assign(inProgressFilters, value)"
:stats-text="`共 ${inProgressTotal} 个进行中任务`"
:sort-options="inProgressSortOptions"
:status-options="inProgressStatusOptions"
status-placeholder="任务状态"
:show-task-sub-type="false"
:show-score-range="false"
:show-progress-range="true"
:demand-types="demandTypes"
:task-sub-types="taskSubTypes"
:brands="brands"
:categories="categories"
:platforms="platforms"
@change="loadInProgressTasks"
@reset="handleInProgressFiltersReset"
/>
<!-- 进行中任务列表 -->
<div v-if="inProgressLoading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="inProgressTaskList.length === 0" class="empty-state chart-card">
<div class="empty-icon">
<Icon name="clock" size="xl" color="#6B6B6B" />
</div>
<h3>暂无进行中任务</h3>
<p>还没有进行中的任务</p>
</div>
<div v-else class="task-grid">
<div
v-for="task in inProgressTaskList"
:key="task.id"
class="task-card chart-card"
:class="[`status-${task.status}`]"
@click="handleTaskClick(task)"
style="cursor: pointer;"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span :class="['badge', getTaskStatusClass(task)]">
{{ getTaskStatusText(task.status, task) }}
</span>
<span v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
<Icon name="target" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
指定
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#ff4d4f" 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-wrapper">
<div class="task-info task-info-3col">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ formatScore(task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 预览图 -->
<div v-if="task.previewImage" class="preview-thumbnail">
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
<div class="preview-popup">
<img :src="task.previewImage" alt="预览图" />
</div>
</div>
</div>
<!-- 子任务进度时间线(流水线任务)- 横向拉满 -->
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="subtask-timeline-wrapper full-width">
<div class="subtask-timeline">
<div
v-for="(subTask, index) in (task.subTasks || []).slice(0, 5)"
:key="subTask.id || index"
class="timeline-node"
:class="getSubTaskStatusClass(subTask.status)"
>
<div class="node-dot"></div>
<div class="node-label">{{ getSubTaskLabel(subTask, index) }}</div>
<div class="node-status">{{ getSubTaskStatusText(subTask.status) }}</div>
</div>
<!-- 显示更多子任务提示 -->
<div v-if="(task.subTasks || []).length > 5" class="timeline-node more">
<div class="node-dot"></div>
<div class="node-label">+{{ (task.subTasks || []).length - 5 }}</div>
</div>
<!-- 完结节点 -->
<div class="timeline-node final" :class="{ completed: task.status === 'completed' }">
<div class="node-dot"></div>
<div class="node-label">完结</div>
</div>
</div>
</div>
<!-- 任务类型明细 -->
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
<div class="type-details-title">任务明细:</div>
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</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
v-if="task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0 && task.id"
class="expand-subtasks"
@click.stop="toggleTaskSubtasks(task.id!)"
>
<Icon :name="expandedTaskSubtasks[task.id!] ? 'arrowUp' : 'arrowDown'" size="xs" style="margin-left: 4px; cursor: pointer;" />
</span>
</span>
</div>
<!-- 流水线任务子任务详细信息(可展开) -->
<div v-if="task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0 && task.id && expandedTaskSubtasks[task.id]"
class="subtasks-detail">
<div v-for="(subTask, index) in task.subTasks" :key="subTask.id || index" class="subtask-item">
<span class="subtask-label">{{ getSubTaskLabel(subTask, index) }}:</span>
<span class="subtask-receiver">{{ subTask.receiverName || '待接单' }}</span>
<span v-if="getSubTaskDeadline(subTask)" class="subtask-deadline">
完成截止日期: {{ getSubTaskDeadline(subTask) }}
</span>
<span :class="['subtask-status', getSubTaskStatusClass(subTask.status)]">
{{ getSubTaskStatusText(subTask.status) }}
</span>
</div>
</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 v-if="Number(task.isPrimaryTask) === 1 && task.primaryPublisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#faad14" />
</span>
<Tooltip :title="`一级需求人: ${task.primaryPublisherName} (${task.primaryPublisherDeptName || '一级部门'})`">
<span style="color: #faad14; cursor: help;">一级需求人: {{ truncateText(task.primaryPublisherName, 8) }} ({{ truncateText(task.primaryPublisherDeptName, 8) || '一级部门' }})</span>
</Tooltip>
</div>
<!-- 明细完善人(一级需求任务且有明细完善人) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.detailCompleterName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#52c41a" />
</span>
<Tooltip :title="`需求部门发起人: ${task.detailCompleterName}`">
<span style="color: #52c41a; cursor: help;">需求部门发起人: {{ truncateText(task.detailCompleterName, 8) }}</span>
</Tooltip>
</div>
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
<div v-if="task.publisherName && !(Number(task.isPrimaryTask) === 1 && task.detailCompleterName)" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<Tooltip v-if="task.publisherName" :title="`发布人: ${task.publisherName}`">
<span style="cursor: help;">发布人: {{ truncateText(task.publisherName, 8) }}</span>
</Tooltip>
<span v-else>发布人: --</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 task-no-item">
<Icon name="hash" size="sm" color="#31f08b" />
<span style="color: #ffffff;">编号: {{ task.isSubTask ? task.itemNo : task.taskNo }}</span>
<Icon
name="copy"
size="xs"
class="copy-icon"
@click.stop="handleCopyTaskNo(task.isSubTask ? task.itemNo : task.taskNo)"
title="复制任务编号"
/>
</div>
<div class="footer-item" :title="formatFullDateTime(task.createTime)" style="cursor: help;">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatTime(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.estimatedHours" :title="'时效要求: ' + formatEstimatedTime(task.estimatedHours)" style="cursor: help;">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">时效: {{ formatEstimatedTime(task.estimatedHours) }}</span>
</div>
<!-- 驳回时间 -->
<div class="footer-item" v-if="task.approvalStatus?.includes('rejected') && task.updateTime" :title="formatFullDateTime(task.updateTime)" style="cursor: help;">
<Icon name="x-circle" size="sm" color="#f5222d" />
<span style="color: #f5222d;">驳回: {{ formatTime(task.updateTime) }}</span>
</div>
</div>
<!-- 操作按钮(进行中任务特有) -->
<div class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button size="small" @click.stop="handleViewTaskDetail(task)">查看详情</a-button>
<!-- 上一环节素材下载(流水线任务) -->
<a-button
v-if="task.prevDownloadUrl"
type="default"
size="small"
@click.stop="handleDownloadPrevAssets(task)"
class="prev-assets-btn"
>
<Icon name="download" size="sm" style="margin-right: 4px;" />
上一环节素材
</a-button>
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click.stop="handleUpdateProgress(task)">
更新进度
</a-button>
<a-button v-if="canCompleteTask(task)" type="primary" size="small" @click.stop="handleCompleteTask(task)">
完结任务
</a-button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="inProgressTotal > 0" class="pagination">
<a-pagination
v-model:current="inProgressPagination.page"
v-model:page-size="inProgressPagination.pageSize"
:total="inProgressTotal"
:show-size-changer="false"
@change="handleInProgressPageChange"
/>
</div>
</a-tab-pane>
<!-- 已完成任务标签页 -->
<a-tab-pane key="completed">
<template #tab>
<span>已完成任务 <span class="tab-count">({{ completedTotal }})</span></span>
</template>
<!-- 搜索和筛选栏(使用公共组件) -->
<TaskFilters
:model-value="completedFilters"
@update:model-value="(value) => Object.assign(completedFilters, value)"
:stats-text="`共 ${completedTotal} 个已完成任务`"
:sort-options="completedSortOptions"
:show-status="false"
time-range-placeholder="完成时间"
:demand-types="demandTypes"
:task-sub-types="taskSubTypes"
:brands="brands"
:categories="categories"
:platforms="platforms"
@change="loadCompletedTasks"
@reset="handleCompletedFiltersReset"
/>
<!-- 已完成任务列表 -->
<div v-if="completedLoading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="completedTaskList.length === 0" class="empty-state chart-card">
<div class="empty-icon">
<Icon name="check-circle" size="xl" color="#6B6B6B" />
</div>
<h3>暂无已完成任务</h3>
<p>还没有完成的任务记录</p>
</div>
<div v-else class="task-grid">
<div
v-for="task in completedTaskList"
:key="task.id"
class="task-card chart-card 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-wrapper">
<div class="task-info task-info-3col">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
</div>
<div class="info-item">
<span class="label">获得积分</span>
<span class="value text-brand">{{ formatScore(task.totalScore || task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 预览图 -->
<div v-if="task.previewImage" class="preview-thumbnail">
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
<div class="preview-popup">
<img :src="task.previewImage" alt="预览图" />
</div>
</div>
</div>
<!-- 子任务进度时间线(流水线任务)- 横向拉满 -->
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="subtask-timeline-wrapper full-width">
<div class="subtask-timeline">
<div
v-for="(subTask, index) in (task.subTasks || []).slice(0, 5)"
:key="subTask.id || index"
class="timeline-node"
:class="getSubTaskStatusClass(subTask.status)"
>
<div class="node-dot"></div>
<div class="node-label">{{ getSubTaskLabel(subTask, index) }}</div>
<div class="node-status">{{ getSubTaskStatusText(subTask.status) }}</div>
</div>
<!-- 显示更多子任务提示 -->
<div v-if="(task.subTasks || []).length > 5" class="timeline-node more">
<div class="node-dot"></div>
<div class="node-label">+{{ (task.subTasks || []).length - 5 }}</div>
</div>
<!-- 完结节点 -->
<div class="timeline-node final" :class="{ completed: task.status === 'completed' }">
<div class="node-dot"></div>
<div class="node-label">完结</div>
</div>
</div>
</div>
<!-- 任务类型明细 -->
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
<div class="type-details-title">任务明细:</div>
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<!-- 一级部门需求人(如果是一级需求部门发起的任务,优先显示) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.primaryPublisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#faad14" />
</span>
<Tooltip :title="`一级需求人: ${task.primaryPublisherName} (${task.primaryPublisherDeptName || '一级部门'})`">
<span style="color: #faad14; cursor: help;">一级需求人: {{ truncateText(task.primaryPublisherName, 8) }} ({{ truncateText(task.primaryPublisherDeptName, 8) || '一级部门' }})</span>
</Tooltip>
</div>
<!-- 明细完善人(一级需求任务且有明细完善人) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.detailCompleterName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#52c41a" />
</span>
<Tooltip :title="`需求部门发起人: ${task.detailCompleterName}`">
<span style="color: #52c41a; cursor: help;">需求部门发起人: {{ truncateText(task.detailCompleterName, 8) }}</span>
</Tooltip>
</div>
<!-- 发起人:非一级需求任务或一级需求任务无明细完善人时显示 -->
<div v-if="!(Number(task.isPrimaryTask) === 1 && task.detailCompleterName)" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<Tooltip v-if="task.publisherName" :title="`发起人: ${task.publisherName}`">
<span style="cursor: help;">发起人: {{ truncateText(task.publisherName, 8) || '--' }}</span>
</Tooltip>
<span v-else>发起人: --</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>
完成人: {{ task.receiverName || '--' }}
<span
v-if="task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0 && task.id"
class="expand-subtasks"
@click.stop="toggleTaskSubtasks(task.id!)"
>
<Icon :name="expandedTaskSubtasks[task.id!] ? 'arrowUp' : 'arrowDown'" size="xs" style="margin-left: 4px; cursor: pointer;" />
</span>
</span>
</div>
<!-- 流水线任务子任务详细信息(可展开) -->
<div v-if="task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0 && task.id && expandedTaskSubtasks[task.id]"
class="subtasks-detail">
<div v-for="(subTask, index) in task.subTasks" :key="subTask.id || index" class="subtask-item">
<span class="subtask-label">{{ getSubTaskLabel(subTask, index) }}:</span>
<span class="subtask-receiver">{{ subTask.receiverName || '待接单' }}</span>
<span v-if="getSubTaskDeadline(subTask)" class="subtask-deadline">
完成截止日期: {{ getSubTaskDeadline(subTask) }}
</span>
<span :class="['subtask-status', getSubTaskStatusClass(subTask.status)]">
{{ getSubTaskStatusText(subTask.status) }}
</span>
</div>
</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 v-if="Number(task.isPrimaryTask) === 1 && task.primaryPublisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#faad14" />
</span>
<Tooltip :title="`一级需求人: ${task.primaryPublisherName} (${task.primaryPublisherDeptName || '一级部门'})`">
<span style="color: #faad14; cursor: help;">一级需求人: {{ truncateText(task.primaryPublisherName, 8) }} ({{ truncateText(task.primaryPublisherDeptName, 8) || '一级部门' }})</span>
</Tooltip>
</div>
<!-- 明细完善人(一级需求任务且有明细完善人) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.detailCompleterName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#52c41a" />
</span>
<Tooltip :title="`需求部门发起人: ${task.detailCompleterName}`">
<span style="color: #52c41a; cursor: help;">需求部门发起人: {{ truncateText(task.detailCompleterName, 8) }}</span>
</Tooltip>
</div>
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
<div v-if="task.publisherName && !(Number(task.isPrimaryTask) === 1 && task.detailCompleterName)" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<Tooltip v-if="task.publisherName" :title="`发布人: ${task.publisherName}`">
<span style="cursor: help;">发布人: {{ truncateText(task.publisherName, 8) }}</span>
</Tooltip>
<span v-else>发布人: --</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 task-no-item">
<Icon name="hash" size="sm" color="#31f08b" />
<span style="color: #ffffff;">编号: {{ task.isSubTask ? task.itemNo : task.taskNo }}</span>
<Icon
name="copy"
size="xs"
class="copy-icon"
@click.stop="handleCopyTaskNo(task.isSubTask ? task.itemNo : task.taskNo)"
title="复制任务编号"
/>
</div>
<div class="footer-item" :title="formatFullDateTime(task.createTime)" style="cursor: help;">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
</div>
<div v-if="task.completeTime && task.status === 'completed'" class="footer-item" :title="formatFullDateTime(task.completeTime)" style="cursor: help;">
<Icon name="check" size="sm" color="#31f08b" />
<span style="color: #ffffff;">完成: {{ formatDate(task.completeTime) }}</span>
</div>
</div>
<!-- 评分区域 -->
<div class="task-rating" style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #303030;">
<!-- 已评分:显示评分结果 -->
<div v-if="task.rating" style="display: flex; align-items: center; justify-content: space-between;">
<span style="color: #888; font-size: 12px;">已评分</span>
<div style="display: flex; gap: 2px;">
<Icon
v-for="i in 5"
:key="i"
name="star"
size="sm"
:color="i <= task.rating ? '#fadb14' : '#303030'"
/>
</div>
</div>
<!-- 未评分且是发布人:显示评分按钮 -->
<a-button
v-else-if="isTaskPublisher(task)"
type="primary"
size="small"
block
@click.stop="showRatingModal(task)"
>
<Icon name="star" size="sm" style="margin-right: 4px;" />
立即评分
</a-button>
<!-- 未评分且不是发布人 -->
<div v-else style="text-align: center; color: #666; font-size: 12px;">
等待发布人评分
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="completedTotal > 0" class="pagination">
<a-pagination
v-model:current="completedPagination.page"
v-model:page-size="completedPagination.pageSize"
:total="completedTotal"
:show-size-changer="false"
@change="handleCompletedPageChange"
/>
</div>
</a-tab-pane>
<!-- 我发起的任务标签页 -->
<a-tab-pane key="published">
<template #tab>
<span>我发起的任务 <span class="tab-count">({{ publishedTotal }})</span></span>
</template>
<!-- 搜索和筛选栏(使用公共组件) -->
<TaskFilters
:model-value="publishedFilters"
@update:model-value="(value) => Object.assign(publishedFilters, value)"
:stats-text="`共 ${publishedTotal} 个任务`"
:sort-options="publishedSortOptions"
:status-options="publishedStatusOptions"
status-placeholder="任务状态"
:show-task-sub-type="false"
:demand-types="demandTypes"
:task-sub-types="taskSubTypes"
:brands="brands"
:categories="categories"
:platforms="platforms"
@change="loadPublishedTasks"
@reset="handlePublishedFiltersReset"
/>
<!-- 我发起的任务列表 -->
<div v-if="publishedLoading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="publishedTaskList.length === 0" class="empty-state chart-card">
<div class="empty-icon">
<Icon name="inbox" size="xl" color="#6B6B6B" />
</div>
<h3>暂无发起的任务</h3>
<p>您还没有发起任何任务</p>
<router-link
to="/tasks/create"
class="btn btn-primary"
style="margin-top: 16px"
>
+ 发起新任务
</router-link>
</div>
<div v-else class="task-grid">
<div
v-for="task in publishedTaskList"
:key="task.id"
class="task-card chart-card"
:class="[`status-${task.status}`]"
@click="handleTaskClick(task)"
style="cursor: pointer;"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span :class="['badge', getTaskStatusClass(task)]">
{{ getTaskStatusText(task.status, task) }}
</span>
<span v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
<Icon name="target" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
指定
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#ff4d4f" 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-wrapper">
<div class="task-info task-info-3col">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ formatScore(task.score) }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 预览图 -->
<div v-if="task.previewImage" class="preview-thumbnail">
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
<div class="preview-popup">
<img :src="task.previewImage" alt="预览图" />
</div>
</div>
</div>
<!-- 子任务进度时间线(流水线任务)- 横向拉满 -->
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="subtask-timeline-wrapper full-width">
<div class="subtask-timeline">
<div
v-for="(subTask, index) in (task.subTasks || []).slice(0, 5)"
:key="subTask.id || index"
class="timeline-node"
:class="getSubTaskStatusClass(subTask.status)"
>
<div class="node-dot"></div>
<div class="node-label">{{ getSubTaskLabel(subTask, index) }}</div>
<div class="node-status">{{ getSubTaskStatusText(subTask.status) }}</div>
</div>
<!-- 显示更多子任务提示 -->
<div v-if="(task.subTasks || []).length > 5" class="timeline-node more">
<div class="node-dot"></div>
<div class="node-label">+{{ (task.subTasks || []).length - 5 }}</div>
</div>
<!-- 完结节点 -->
<div class="timeline-node final" :class="{ completed: task.status === 'completed' }">
<div class="node-dot"></div>
<div class="node-label">完结</div>
</div>
</div>
</div>
<!-- 任务类型明细 -->
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
<div class="type-details-title">任务明细:</div>
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</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
v-if="task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0 && task.id"
class="expand-subtasks"
@click.stop="toggleTaskSubtasks(task.id!)"
>
<Icon :name="expandedTaskSubtasks[task.id!] ? 'arrowUp' : 'arrowDown'" size="xs" style="margin-left: 4px; cursor: pointer;" />
</span>
</span>
</div>
<!-- 流水线任务子任务详细信息(可展开) -->
<div v-if="task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0 && task.id && expandedTaskSubtasks[task.id]"
class="subtasks-detail">
<div v-for="(subTask, index) in task.subTasks" :key="subTask.id || index" class="subtask-item">
<span class="subtask-label">{{ getSubTaskLabel(subTask, index) }}:</span>
<span class="subtask-receiver">{{ subTask.receiverName || '待接单' }}</span>
<span v-if="getSubTaskDeadline(subTask)" class="subtask-deadline">
完成截止日期: {{ getSubTaskDeadline(subTask) }}
</span>
<span :class="['subtask-status', getSubTaskStatusClass(subTask.status)]">
{{ getSubTaskStatusText(subTask.status) }}
</span>
</div>
</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 v-if="Number(task.isPrimaryTask) === 1 && task.primaryPublisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#faad14" />
</span>
<Tooltip :title="`一级需求人: ${task.primaryPublisherName} (${task.primaryPublisherDeptName || '一级部门'})`">
<span style="color: #faad14; cursor: help;">一级需求人: {{ truncateText(task.primaryPublisherName, 8) }} ({{ truncateText(task.primaryPublisherDeptName, 8) || '一级部门' }})</span>
</Tooltip>
</div>
<!-- 明细完善人(一级需求任务且有明细完善人) -->
<div v-if="Number(task.isPrimaryTask) === 1 && task.detailCompleterName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#52c41a" />
</span>
<Tooltip :title="`需求部门发起人: ${task.detailCompleterName}`">
<span style="color: #52c41a; cursor: help;">需求部门发起人: {{ truncateText(task.detailCompleterName, 8) }}</span>
</Tooltip>
</div>
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
<div v-if="task.publisherName && !(Number(task.isPrimaryTask) === 1 && task.detailCompleterName)" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<Tooltip v-if="task.publisherName" :title="`发布人: ${task.publisherName}`">
<span style="cursor: help;">发布人: {{ truncateText(task.publisherName, 8) }}</span>
</Tooltip>
<span v-else>发布人: --</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 task-no-item">
<Icon name="hash" size="sm" color="#31f08b" />
<span style="color: #ffffff;">编号: {{ task.taskNo }}</span>
<Icon
name="copy"
size="xs"
class="copy-icon"
@click.stop="handleCopyTaskNo(task.taskNo)"
title="复制任务编号"
/>
</div>
<div class="footer-item" :title="formatFullDateTime(task.createTime)" style="cursor: help;">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatTime(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.estimatedHours" :title="'时效要求: ' + formatEstimatedTime(task.estimatedHours)" style="cursor: help;">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">时效: {{ formatEstimatedTime(task.estimatedHours) }}</span>
</div>
<!-- 驳回时间 -->
<div class="footer-item" v-if="task.approvalStatus?.includes('rejected') && task.updateTime" :title="formatFullDateTime(task.updateTime)" style="cursor: help;">
<Icon name="x-circle" size="sm" color="#f5222d" />
<span style="color: #f5222d;">驳回: {{ formatTime(task.updateTime) }}</span>
</div>
</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
v-if="task.downloadUrl || task.deliveryPath"
type="primary"
size="small"
@click.stop="handleDownloadAssets(task)"
style="width: 100%;">
<Icon name="download" size="sm" style="margin-right: 4px;" />
下载素材
</a-button>
<a-button
v-else
type="default"
size="small"
disabled
style="width: 100%; opacity: 0.5;">
<Icon name="download" size="sm" style="margin-right: 4px;" />
暂无素材
</a-button>
</div>
<!-- 撤回编辑按钮(待接单或待审核且未被抢单的任务) -->
<div v-if="canWithdrawTask(task)" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button
type="default"
size="small"
@click.stop="handleWithdrawTask(task)"
:loading="withdrawingTaskId === task.id"
style="width: 100%;">
<Icon name="rotate-ccw" size="sm" style="margin-right: 4px;" />
撤回编辑
</a-button>
</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"
@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"
@click.stop="handleTaskClick(task)"
style="width: 100%;">
<Icon name="eye" size="sm" style="margin-right: 4px;" />
查看详情
</a-button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="publishedTotal > 0" class="pagination">
<a-pagination
v-model:current="publishedPagination.page"
v-model:page-size="publishedPagination.pageSize"
:total="publishedTotal"
:show-size-changer="false"
@change="handlePublishedPageChange"
/>
</div>
</a-tab-pane>
<!-- 草稿箱标签页(仅需求部门用户可见) -->
<a-tab-pane key="drafts" v-if="showDraftTab">
<template #tab>
<span>草稿箱 <span class="tab-count">({{ draftsTotal }})</span></span>
</template>
<!-- 搜索和筛选栏(使用公共组件) -->
<TaskFilters
:model-value="draftsFilters"
@update:model-value="(value) => Object.assign(draftsFilters, value)"
search-placeholder="搜索草稿任务..."
:stats-text="`共 ${draftsTotal} 个草稿`"
:sort-options="draftsSortOptions"
:show-status="false"
:show-task-type="false"
:show-task-sub-type="false"
:show-brand="false"
:show-category="false"
:show-platform="false"
:show-time-range="false"
:show-score-range="false"
@change="loadDraftTasks"
@reset="loadDraftTasks"
/>
<!-- 草稿列表 -->
<div v-if="draftsLoading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="draftTaskList.length === 0" class="empty-state chart-card">
<Icon name="file" size="3xl" color="#666" />
<p>暂无草稿任务</p>
<p class="empty-hint">发布任务时可以保存为草稿,稍后继续编辑</p>
</div>
<div v-else class="task-grid">
<div
v-for="task in draftTaskList"
:key="task.id"
class="task-card chart-card"
@click="handleEditDraftTask(task)"
>
<!-- 草稿标签 -->
<div class="task-header">
<h3 class="task-title">{{ task.title || '未命名任务' }}</h3>
<div class="task-badges">
<span class="badge draft-badge">草稿</span>
<span v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
<Icon name="target" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
指定
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#ff4d4f" style="margin-right: 4px; vertical-align: middle;" />
紧急
</span>
</div>
</div>
<!-- 任务信息(三栏布局) -->
<div class="task-info-wrapper">
<div class="task-info task-info-3col">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) || '--' }}</span>
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score ? formatScore(task.score) + ' 分' : '--' }}</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 预览图 -->
<div v-if="task.previewImage" class="preview-thumbnail">
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
<div class="preview-popup">
<img :src="task.previewImage" alt="预览图" />
</div>
</div>
</div>
<!-- 任务类型明细 -->
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
<div class="type-details-title">任务明细:</div>
<div class="type-details-list">
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
<span class="type-name">└ {{ detail.typeName }}</span>
<span class="type-score">x{{ detail.quantity }} = {{ formatScore(detail.totalScore) }}分</span>
</div>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item" v-if="task.brandName">
<span class="icon"><Icon name="tag" size="sm" color="#31f08b" /></span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div class="detail-item" v-if="task.categoryName">
<span class="icon"><Icon name="folder" size="sm" color="#31f08b" /></span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div class="detail-item" v-if="task.platformName">
<span class="icon"><Icon name="monitor" size="sm" color="#31f08b" /></span>
<span>平台: {{ task.platformName }}</span>
</div>
<div class="detail-item" v-if="task.spuInfo">
<span class="icon"><Icon name="package" size="sm" color="#31f08b" /></span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div class="detail-item" v-if="task.taskSubType">
<span class="icon"><Icon name="list" size="sm" color="#31f08b" /></span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div class="detail-item" v-if="task.attachmentUrl">
<span class="icon"><Icon name="paperclip" size="sm" color="#31f08b" /></span>
<span>附件: {{ (task.attachmentUrl || '').split(',').filter(u => u.trim()).length }} 个</span>
</div>
<div class="detail-item" v-if="task.estimatedHours">
<span class="icon"><Icon name="clock" size="sm" color="#31f08b" /></span>
<span>时效要求: {{ formatEstimatedTime(task.estimatedHours) }}</span>
</div>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div v-if="task.taskNo" class="footer-item task-no-item">
<Icon name="hash" size="sm" color="#31f08b" />
<span style="color: #ffffff;">编号: {{ task.taskNo }}</span>
<Icon
name="copy"
size="xs"
class="copy-icon"
@click.stop="handleCopyTaskNo(task.taskNo)"
title="复制任务编号"
/>
</div>
<div class="footer-item" :title="formatFullDateTime(task.createTime)" style="cursor: help;">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">创建: {{ formatTime(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.updateTime && task.updateTime !== task.createTime" :title="formatFullDateTime(task.updateTime)" style="cursor: help;">
<Icon name="edit" size="sm" color="#faad14" />
<span style="color: #faad14;">最后编辑: {{ formatTime(task.updateTime) }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div 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"
@click.stop="handleEditDraftTask(task)">
<Icon name="edit" size="sm" style="margin-right: 4px;" />
继续编辑
</a-button>
<a-button
type="default"
danger
size="small"
@click.stop="handleDeleteDraft(task)">
<Icon name="trash" size="sm" style="margin-right: 4px;" />
删除
</a-button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="draftsTotal > 0" class="pagination">
<a-pagination
v-model:current="draftsPagination.page"
v-model:page-size="draftsPagination.pageSize"
:total="draftsTotal"
:show-size-changer="false"
@change="handleDraftsPageChange"
/>
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 审核通过弹窗 -->
<a-modal
v-model:visible="approveModalVisible"
title="接单部门审核通过"
:width="600"
:confirm-loading="approvalLoading"
@ok="handleApproveConfirm"
class="approve-modal"
:body-style="{ padding: '24px', background: '#1f1f1f' }"
>
<!-- 任务类型编辑 -->
<div class="modal-section">
<div class="section-header" style="margin-bottom: 12px;">
<span class="section-title" style="font-weight: 600; color: #fff;">编辑任务类型</span>
</div>
<div class="task-type-list" style="background: #141414; padding: 12px; border-radius: 8px;">
<div v-for="(item, index) in approveTaskTypes" :key="index" class="task-type-item" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; padding: 12px; background: #262626; border-radius: 6px;">
<div class="type-info" style="flex: 1; min-width: 0;">
<div style="display: flex; flex-direction: column; gap: 4px;">
<span style="color: #fff; font-weight: 500; font-size: 14px;">{{ getTaskTypeFullPath(item.typeId)?.name || getTaskTypeName(item.typeId) }}</span>
<span style="color: #888; font-size: 12px;">{{ getTaskTypeFullPath(item.typeId)?.fullPath || '' }}</span>
</div>
</div>
<div class="type-controls" style="display: flex; align-items: center; gap: 12px;">
<div style="display: flex; align-items: center; background: #31f08b22; padding: 4px 10px; border-radius: 4px;">
<span style="color: #31f08b; font-weight: 600; font-size: 14px;">{{ getTaskTypeUnitScore(item.typeId) }} 分/个</span>
</div>
<div style="display: flex; align-items: center;">
<span class="label" style="color: #888; margin-right: 8px; font-size: 12px;">数量</span>
<a-input-number
v-model:value="item.quantity"
:min="1"
size="small"
style="width: 70px;"
/>
</div>
<a-button
type="text"
danger
size="small"
@click="approveTaskTypes.splice(index, 1)"
>
<Icon name="trash-2" size="sm" />
</a-button>
</div>
</div>
<!-- 添加类型(使用级联选择器) -->
<div class="add-type-row" style="margin-top: 12px; display: flex; gap: 8px;">
<a-cascader
v-model:value="newTaskTypePath"
:options="taskTypeCascaderOptions"
placeholder="选择要添加的任务类型"
style="flex: 1"
allow-clear
change-on-select
:show-search="{ filter: cascaderFilter }"
popup-class-name="dark-cascader-popup"
/>
<a-button type="primary" ghost style="border-color: #31f08b; color: #31f08b;" @click="handleAddApproveTaskType">
<Icon name="plus" size="sm" style="margin-right: 4px" /> 添加
</a-button>
</div>
</div>
<!-- 预估积分显示 -->
<div class="score-summary" style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #1a3a2a 0%, #0d1f16 100%); border-radius: 8px; border: 1px solid #31f08b33;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: #888; font-size: 14px;">预估总积分</span>
<span style="color: #31f08b; font-size: 24px; font-weight: 600;">{{ approveEstimatedScore }} 分</span>
</div>
<div style="margin-top: 8px; font-size: 12px; color: #666;">
<span v-for="(item, index) in approveTaskTypes" :key="index" style="margin-right: 12px;">
{{ getTaskTypeName(item.typeId) }}: {{ getTaskTypeUnitScore(item.typeId) }} × {{ item.quantity }}
</span>
</div>
</div>
</div>
<!-- 接单方式 -->
<div class="modal-section" style="margin-top: 24px;">
<div class="section-header" style="margin-bottom: 12px;">
<span class="section-title" style="font-weight: 600; color: #fff;">选择接单方式</span>
<span v-if="isTaskPreAssigned" style="margin-left: 8px; color: #faad14; font-size: 12px;">
(任务已预先指定设计师,不可修改)
</span>
</div>
<!-- 已预先指定设计师,显示只读信息 -->
<div v-if="isTaskPreAssigned" class="pre-assigned-info" style="padding: 16px; border: 1px solid #52c41a; border-radius: 8px; background: rgba(82, 196, 26, 0.1);">
<div style="display: flex; align-items: center; gap: 8px;">
<Icon name="check-circle" size="lg" color="#52c41a" />
<span style="color: #fff; font-weight: 500;">已预先指定设计师接单</span>
</div>
<!-- 多个设计师列表(同部门多子任务情况) -->
<div v-if="preAssignedDesignersList.length > 0" style="margin-top: 12px;">
<div v-for="(item, index) in preAssignedDesignersList" :key="index"
style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: rgba(49, 240, 139, 0.1); border-radius: 6px; margin-bottom: 8px;">
<div style="display: flex; flex-direction: column; gap: 2px;">
<span style="color: #fff; font-size: 13px;">{{ item.typeName }}</span>
<span style="color: #888; font-size: 12px;">数量: {{ item.quantity }}</span>
</div>
<span style="color: #31f08b; font-weight: 600;">{{ item.designerName }}</span>
</div>
</div>
<!-- 单个设计师(兼容旧逻辑) -->
<div v-else style="margin-top: 12px; color: #888;">
<span>指定设计师:</span>
<span style="color: #31f08b; font-weight: 600;">{{ preAssignedDesignerName }}</span>
</div>
</div>
<!-- 未预先指定,显示选择界面 -->
<a-radio-group v-else v-model:value="approveAssignType" class="assign-type-group" style="width: 100%; display: flex; gap: 12px;">
<a-radio value="auto" class="assign-option" style="flex: 1; display: flex; padding: 16px; border: 1px solid #303030; border-radius: 8px; background: #141414; align-items: center; margin-right: 0;">
<div class="option-content" style="display: inline-flex; flex-direction: column; margin-left: 8px;">
<span class="option-title" style="font-weight: 500; color: #fff; font-size: 14px;">发布到任务大厅</span>
<span class="option-desc" style="font-size: 12px; color: #888; margin-top: 4px;">所有设计师均可抢单</span>
</div>
</a-radio>
<a-radio value="manual" class="assign-option" style="flex: 1; display: flex; padding: 16px; border: 1px solid #303030; border-radius: 8px; background: #141414; align-items: center;">
<div class="option-content" style="display: inline-flex; flex-direction: column; margin-left: 8px;">
<span class="option-title" style="font-weight: 500; color: #fff; font-size: 14px;">指定设计师接单</span>
<span class="option-desc" style="font-size: 12px; color: #888; margin-top: 4px;">直接分配给特定设计师</span>
</div>
</a-radio>
</a-radio-group>
</div>
<!-- 选择设计师 -->
<div v-if="approveAssignType === 'manual' && !isTaskPreAssigned" class="modal-section" style="margin-top: 24px;">
<div class="section-header" style="margin-bottom: 12px;">
<span class="section-title" style="font-weight: 600; color: #fff;">选择设计师</span>
</div>
<a-select
v-model:value="approveDesignerId"
placeholder="请选择设计师"
style="width: 100%"
size="large"
option-label-prop="label"
:dropdown-style="{ background: '#1f1f1f', borderColor: '#303030' }"
>
<a-select-option
v-for="designer in designersList"
:key="designer.id"
:value="designer.id"
:label="designer.realName"
>
<div class="designer-option" style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0;">
<span class="designer-name" style="color: #fff; font-weight: 500;">{{ designer.realName }}</span>
<div class="designer-stats">
<a-tag :color="designer.inProgressCount === 0 ? 'success' : designer.inProgressCount > 5 ? 'error' : 'warning'">
进行中: {{ designer.inProgressCount }}
</a-tag>
<span v-if="designer.earliestDeadline" style="font-size: 12px; color: #888; margin-left: 8px;">
最早完成: {{ formatDeadlineRelative(designer.earliestDeadline) }}
</span>
</div>
</div>
</a-select-option>
</a-select>
</div>
</a-modal>
<!-- 一级需求审核弹窗 -->
<a-modal
v-model:visible="primaryApproveModalVisible"
title="审核一级需求任务"
:width="500"
:confirm-loading="primaryApprovalLoading"
@ok="handlePrimaryApproveConfirm"
class="primary-approve-modal"
:body-style="{ padding: '24px', background: '#1f1f1f' }"
>
<div v-if="primaryApproveTaskData" class="primary-approve-content">
<!-- 任务信息 -->
<div class="task-info" style="margin-bottom: 20px; padding: 16px; background: #141414; border-radius: 8px;">
<div style="font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 8px;">
{{ primaryApproveTaskData.title }}
</div>
<div style="display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; color: #888;">
<span>品牌: {{ primaryApproveTaskData.brandName }}</span>
<span>平台: {{ primaryApproveTaskData.platformName }}</span>
<span>积分: {{ primaryApproveTaskData.score }} 分</span>
</div>
<div style="margin-top: 8px; font-size: 13px; color: #888;">
一级部门发起人: {{ primaryApproveTaskData.primaryPublisherName || primaryApproveTaskData.publisherName }}
</div>
</div>
<!-- 指定完善人 -->
<div class="assign-completer" style="margin-bottom: 20px;">
<div style="font-weight: 600; color: #fff; margin-bottom: 12px;">
<span style="color: #ff4d4f;">*</span> 指定成员完善任务明细
</div>
<a-select
v-model:value="primaryApproveCompleterId"
placeholder="请选择部门成员"
style="width: 100%"
show-search
:filter-option="(input, option) => {
const label = option.label || ''
const deptName = option['data-dept'] || ''
return label.toLowerCase().includes(input.toLowerCase()) || deptName.toLowerCase().includes(input.toLowerCase())
}"
>
<a-select-option
v-for="member in deptMembersList"
:key="member.id"
:value="member.id"
:label="member.realName"
:data-dept="member.deptName"
>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ member.realName }}</span>
<span style="color: #888; font-size: 12px;">
{{ member.deptName ? `${member.deptName} · ` : '' }}{{ member.username }}
</span>
</div>
</a-select-option>
</a-select>
</div>
<!-- 审核备注 -->
<div class="approve-comment">
<div style="font-weight: 600; color: #fff; margin-bottom: 12px;">审核备注(可选)</div>
<a-textarea
v-model:value="primaryApproveComment"
placeholder="请输入审核备注"
:rows="3"
style="width: 100%"
/>
</div>
<!-- 提示信息 -->
<div style="margin-top: 16px; padding: 12px; background: #262626; border-radius: 8px; border-left: 3px solid #1890ff;">
<div style="color: #1890ff; font-size: 13px;">
<Icon name="info" size="sm" style="margin-right: 6px;" />
审核通过后,指定的成员将收到通知,需要完善任务明细后才能正式发布任务。
</div>
</div>
</div>
</a-modal>
<!-- 任务详情抽屉 -->
<a-drawer
v-model:visible="detailDrawerVisible"
title="任务详情"
:width="600"
placement="right"
>
<div v-if="selectedTask" class="task-detail">
<div class="detail-section">
<h3>{{ selectedTask.title }}</h3>
<div class="detail-badges">
<span v-if="selectedTask.isSubTask" class="badge badge-subtask">子任务</span>
<span v-if="isNewTask(selectedTask)" class="badge badge-new">新发布</span>
<span v-if="isHighScoreTask(selectedTask)" class="badge badge-high-score">高分</span>
<span v-if="selectedTask.isUrgent" class="badge badge-urgent">紧急</span>
</div>
</div>
<div class="detail-section">
<h4>基本信息</h4>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="任务编号">
{{ selectedTask.isSubTask ? selectedTask.subTaskNo : selectedTask.taskNo }}
</a-descriptions-item>
<a-descriptions-item v-if="selectedTask.isSubTask" label="所属主任务">
{{ selectedTask.taskNo || '--' }}
</a-descriptions-item>
<a-descriptions-item label="任务类型">{{ getTaskTypeName(selectedTask.taskTypeId) }}</a-descriptions-item>
<a-descriptions-item label="预估积分">{{ selectedTask.score }} 分</a-descriptions-item>
<a-descriptions-item label="时效要求">{{ formatEstimatedTime(selectedTask.estimatedHours) }}</a-descriptions-item>
<a-descriptions-item v-if="!selectedTask.isSubTask" label="数量">{{ selectedTask.quantity || '--' }} 个</a-descriptions-item>
<a-descriptions-item v-if="selectedTask.status === 'in_progress' && getTaskDeadline(selectedTask)" label="完成截止时间">
<span :class="{ 'text-error': isTaskDeadlineOverdue(selectedTask) }">
{{ formatTime(getTaskDeadline(selectedTask) || undefined) }}
</span>
</a-descriptions-item>
</a-descriptions>
</div>
<div class="detail-section">
<h4>关联信息</h4>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="品牌">{{ selectedTask.brandName || '--' }}</a-descriptions-item>
<a-descriptions-item label="类目">{{ selectedTask.categoryName || '--' }}</a-descriptions-item>
<a-descriptions-item label="平台">{{ selectedTask.platformName || '--' }}</a-descriptions-item>
<a-descriptions-item label="发布人">{{ selectedTask.publisherName || '未知' }}</a-descriptions-item>
</a-descriptions>
</div>
<div v-if="selectedTask.spuInfo" class="detail-section">
<h4>SPU信息</h4>
<p>{{ selectedTask.spuInfo }}</p>
</div>
<div v-if="selectedTask.remark" class="detail-section">
<h4>备注说明</h4>
<p>{{ selectedTask.remark }}</p>
</div>
<div v-if="selectedTask.attachmentUrl" class="detail-section">
<h4>附件</h4>
<div class="attachment-links">
<!-- 只有非pending状态或管理员才能下载附件 -->
<template v-if="canDownloadAttachment(selectedTask)">
<a v-for="(url, index) in (selectedTask.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link">
<Icon name="download" size="sm" style="margin-right: 4px;" />
附件 {{ index + 1 }}
</a>
</template>
<!-- 待接单状态显示锁定提示 -->
<div v-else class="attachment-locked-notice">
<Icon name="lock" size="sm" style="margin-right: 4px;" />
{{ (selectedTask.attachmentUrl || '').split(',').filter(u => u.trim()).length }}个附件(接单后可下载)
</div>
</div>
</div>
<!-- 交付路径(已完成或待审核任务显示) -->
<div v-if="selectedTask.deliveryPath && (selectedTask.status === 'completed' || selectedTask.status === 'review')" class="detail-section">
<h4>
<Icon name="link" size="sm" style="margin-right: 6px;" />
交付路径
</h4>
<div class="delivery-path">
<a-button
v-if="selectedTask.deliveryPath.startsWith('http')"
type="primary"
size="small"
@click="openDeliveryPath(selectedTask.deliveryPath)">
<Icon name="download" size="sm" style="margin-right: 4px;" />
下载素材
</a-button>
<p v-else class="delivery-text">{{ selectedTask.deliveryPath }}</p>
</div>
</div>
<!-- 商品ID管理(已完成任务显示) -->
<div v-if="selectedTask.status === 'completed'" class="detail-section product-section">
<h4>
<Icon name="tag" size="sm" style="margin-right: 6px;" />
关联商品ID
<span class="product-count">({{ taskProducts.length }})</span>
</h4>
<!-- 商品ID列表 -->
<div v-if="taskProducts.length > 0" class="product-list">
<div v-for="product in taskProducts" :key="product.id" class="product-item">
<div class="product-info">
<span class="product-id">{{ product.productId }}</span>
<span v-if="product.productName" class="product-name">{{ product.productName }}</span>
</div>
<div class="product-meta">
<span class="creator">{{ product.creatorName }}</span>
<span class="time">{{ formatTime(product.createTime) }}</span>
</div>
<a-button
v-if="canManageProducts(selectedTask)"
type="text"
danger
size="small"
@click="handleDeleteProduct(product.id)">
<Icon name="trash" size="sm" />
</a-button>
</div>
</div>
<div v-else class="empty-products">
<Icon name="inbox" size="lg" style="opacity: 0.3;" />
<p>暂无关联商品ID</p>
</div>
<!-- 添加商品ID -->
<div v-if="canManageProducts(selectedTask)" class="add-product-form">
<a-input-group compact>
<a-input
v-model:value="newProductId"
placeholder="输入商品ID(数字)"
style="width: calc(100% - 80px);"
@pressEnter="handleAddProduct"
/>
<a-button type="primary" @click="handleAddProduct" :loading="addingProduct">
添加
</a-button>
</a-input-group>
<div class="batch-add">
<a-textarea
v-model:value="batchProductIds"
placeholder="批量添加:一行一个商品ID,或用逗号分隔"
:rows="2"
style="margin-top: 8px;"
/>
<a-button
type="default"
size="small"
style="margin-top: 8px;"
@click="handleBatchAddProducts"
:loading="addingProduct">
批量添加
</a-button>
</div>
</div>
<!-- 确认完结按钮:只有添加了商品ID且未确认时显示 -->
<div v-if="canManageProducts(selectedTask) && taskProducts.length > 0 && !selectedTask.productIdFilled" class="confirm-complete-section">
<a-alert
message="请确认商品ID填写完成"
description="确认后任务状态将从「待填写ID」变更为「已完结」"
type="warning"
show-icon
style="margin-bottom: 12px;"
/>
<a-button
type="primary"
block
@click="handleConfirmProductIdFilled"
:loading="confirmingProductId">
<Icon name="checkCircle" size="sm" style="margin-right: 6px;" />
确认完结任务
</a-button>
</div>
</div>
<!-- 流水线任务:显示所有子任务交付路径 -->
<div v-if="selectedTask.isPipeline === 1 && selectedTask.allDeliveryPaths && selectedTask.allDeliveryPaths.length > 0" class="detail-section all-delivery-paths">
<h4>
<Icon name="folder" size="sm" style="margin-right: 6px;" />
所有子任务交付路径
</h4>
<div class="delivery-paths-list">
<div v-for="(path, index) in selectedTask.allDeliveryPaths" :key="index" class="delivery-path-item">
<a-button
v-if="isUrl(path)"
type="primary"
size="small"
@click="openDeliveryPath(extractUrl(path))">
<Icon name="download" size="sm" style="margin-right: 4px;" />
{{ extractLabel(path) }}
</a-button>
<span v-else class="delivery-text">{{ path }}</span>
</div>
</div>
</div>
<!-- 根据任务状态显示不同的操作按钮 -->
<div class="detail-actions">
<!-- 可接任务:显示抢单 -->
<div v-if="selectedTask.status === 'pending'" class="action-buttons">
<a-button
v-if="canEditAvailableTaskDetail(selectedTask)"
size="large"
@click="handleEditAvailableTaskDetail(selectedTask)"
>
编辑任务明细
</a-button>
<a-button
:type="canGrabTask(selectedTask).canGrab ? 'primary' : 'default'"
:style="{
backgroundColor: canGrabTask(selectedTask).canGrab ? '#31f08b' : '#999',
borderColor: canGrabTask(selectedTask).canGrab ? '#31f08b' : '#999',
color: '#fff',
cursor: canGrabTask(selectedTask).canGrab ? 'pointer' : 'not-allowed',
opacity: canGrabTask(selectedTask).canGrab ? 1 : 0.6
}"
size="large"
:block="!canEditAvailableTaskDetail(selectedTask)"
:disabled="!canGrabTask(selectedTask).canGrab"
@click="handleGrabFromDetail">
{{ canGrabTask(selectedTask).canGrab ? '确认抢单' : `不可接单(${canGrabTask(selectedTask).reason})` }}
</a-button>
</div>
<!-- 进行中任务:显示更新进度和完结 -->
<div v-else-if="selectedTask.status === 'in_progress'" class="action-buttons">
<a-button
v-if="canUpdateProgress(selectedTask)"
type="primary"
size="large"
@click="handleUpdateProgress(selectedTask); detailDrawerVisible = false">
更新进度
</a-button>
<a-button
v-if="canCompleteTask(selectedTask)"
type="primary"
size="large"
@click="handleCompleteTask(selectedTask); detailDrawerVisible = false">
完结任务
</a-button>
</div>
<!-- 待审核任务:显示审核按钮 -->
<div v-else-if="selectedTask.status === 'review'" class="action-buttons">
<a-button
v-if="canApproveTask(selectedTask)"
type="primary"
size="large"
@click="handleApproveTask(selectedTask); detailDrawerVisible = false">
通过审核
</a-button>
<a-button
v-if="canRejectTask(selectedTask)"
danger
size="large"
@click="handleRejectTask(selectedTask); detailDrawerVisible = false">
驳回任务
</a-button>
</div>
<!-- 已完成任务:显示评分(如果还没评分,且是发布人) -->
<a-button
v-else-if="selectedTask.status === 'completed' && !selectedTask.rating && isTaskPublisher(selectedTask)"
type="primary"
size="large"
block
@click="showRatingModal(selectedTask)">
<Icon name="star" size="sm" style="margin-right: 6px;" />
评分
</a-button>
<!-- 已评分任务:显示评分结果 -->
<div v-else-if="selectedTask.status === 'completed' && selectedTask.rating" class="rating-result" style="text-align: center; padding: 12px 0;">
<div style="color: #888; margin-bottom: 8px;">已评分</div>
<div style="display: flex; justify-content: center; gap: 4px;">
<Icon
v-for="i in 5"
:key="i"
name="star"
size="lg"
:color="i <= (selectedTask.rating || 0) ? '#fadb14' : '#303030'"
/>
</div>
</div>
<!-- 其他状态:仅显示关闭按钮 -->
<a-button
v-else
size="large"
block
@click="detailDrawerVisible = false">
关闭
</a-button>
</div>
</div>
</a-drawer>
<!-- 评分弹窗 -->
<a-modal
v-model:open="ratingModalVisible"
title="任务评分"
:width="420"
:footer="null"
centered
class="rating-modal"
>
<div class="rating-content" style="padding: 20px 0;">
<div class="rating-task-info" style="margin-bottom: 24px; padding: 16px; background: #1a1a1a; border-radius: 8px;">
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">任务名称</div>
<div style="color: #fff; font-weight: 500;">{{ ratingTask?.title }}</div>
</div>
<div class="rating-stars" style="text-align: center; margin-bottom: 24px;">
<div style="color: #888; margin-bottom: 12px;">请为本次任务评分</div>
<div style="display: flex; justify-content: center; gap: 12px;">
<div
v-for="i in 5"
:key="i"
style="cursor: pointer; transition: transform 0.2s;"
@click="ratingValue = i"
@mouseenter="(e: MouseEvent) => (e.target as HTMLElement).style.transform = 'scale(1.2)'"
@mouseleave="(e: MouseEvent) => (e.target as HTMLElement).style.transform = 'scale(1)'"
>
<Icon
name="star"
size="xl"
:color="i <= ratingValue ? '#fadb14' : '#303030'"
/>
</div>
</div>
<div style="color: #fadb14; margin-top: 8px; font-size: 14px;">
{{ ratingValue === 5 ? '非常满意' : ratingValue === 4 ? '比较满意' : ratingValue === 3 ? '一般' : ratingValue === 2 ? '不太满意' : '非常不满意' }}
</div>
</div>
<div class="rating-comment" style="margin-bottom: 24px;">
<div style="color: #888; margin-bottom: 8px;">评价留言(选填)</div>
<a-textarea
v-model:value="ratingComment"
placeholder="请输入对设计师的评价..."
:rows="3"
:maxlength="200"
show-count
style="background: #1a1a1a; border-color: #303030; color: #fff;"
/>
</div>
<div class="rating-actions" style="display: flex; gap: 12px;">
<a-button
style="flex: 1;"
size="large"
@click="ratingModalVisible = false"
>
取消
</a-button>
<a-button
type="primary"
style="flex: 1;"
size="large"
:loading="ratingLoading"
@click="submitRating"
>
提交评分
</a-button>
</div>
</div>
</a-modal>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted, h, watch } from 'vue'
import { useRouter } from 'vue-router'
import { message, Modal, InputNumber, Textarea, Radio, RadioGroup, Button, Select, Space, Tag, Tooltip } from 'ant-design-vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import Icon from '@/components/Icon.vue'
import TaskFilters from '@/components/TaskFilters.vue'
import * as taskApi from '@/api/task'
import * as userApi from '@/api/user'
import * as configApi from '@/api/config'
import * as authApi from '@/api/auth'
import * as departmentApi from '@/api/department'
import { getWorkHoursConfig, getSystemConfig, getAllSystemConfigs } from '@/api/system'
import type { Task, TaskApprovalDTO, TaskProduct, ExportTaskHallParams } from '@/api/task'
import { exportTaskHall } from '@/api/task'
import type { DesignerWithWorkload } from '@/api/user'
import { useConfigStore } from '@/stores/config'
import dayjs from 'dayjs'
// 扩展Task类型以支持子任务字段
interface TaskTypeDetail {
typeId: number
typeName: string
quantity: number
unitScore: number
totalScore: number
receiverId?: number // 预先指定的设计师ID
receiverName?: string // 预先指定的设计师姓名
}
interface TaskOrSubTask extends Task {
isSubTask?: boolean // 是否为子任务
itemId?: number // 子任务ID
itemNo?: string // 子任务编号
subTaskNo?: string // 子任务编号
groupNo?: number // 子任务组号
taskDescription?: string // 主任务描述
submitTime?: string // 提交时间
taskTypeName?: string // 任务类型名称(子任务直接返回)
prevDownloadUrl?: string // 前置子任务的素材下载链接
typeDetails?: string | TaskTypeDetail[] // 任务类型明细(JSON字符串或解析后的数组)
}
const router = useRouter()
// 全局配置store
const configStore = useConfigStore()
// 标签页
const activeTab = ref('available')
const parseUserInfo = () => {
try {
const userStr = localStorage.getItem('userInfo')
if (!userStr) return {}
return JSON.parse(userStr)
} catch {
return {}
}
}
const currentUserInfo = ref<any>(parseUserInfo())
// 是否为部门负责人
const isDepartmentLeader = computed(() => {
const user = currentUserInfo.value || {}
return user?.isDepartmentLeader === 1 || user?.isDepartmentLeader === true || user?.role === 'dept_leader' || user?.role === 'admin'
})
// 部门视角切换功能(用于管理多个部门的负责人)
interface ManagedDepartment {
deptId: number
deptName: string
deptType: 'demand' | 'receiving'
}
const managedDepartments = ref<ManagedDepartment[]>([]) // 用户管理的所有部门
const currentDeptView = ref<number | undefined>(undefined) // 当前选择的部门视角,undefined表示使用用户自己的部门
const userDeptId = ref<number | null>(null)
const userDeptName = ref<string>('')
const userDeptType = ref<string>('')
const exportLoading = ref(false) // 导出加载状态
const withdrawingTaskId = ref<number | null>(null) // 正在撤回的任务ID
// 获取当前有效的部门类型(根据选择的视角)
const effectiveDeptType = computed(() => {
if (currentDeptView.value === undefined) {
return userDeptType.value
}
const selectedDept = managedDepartments.value.find(d => d.deptId === currentDeptView.value)
return selectedDept?.deptType || userDeptType.value
})
// 获取当前有效的部门ID
const effectiveDeptId = computed(() => {
return currentDeptView.value !== undefined ? currentDeptView.value : userDeptId.value
})
// 计算是否有导出权限(部门负责人或管理员)
const canExportTaskHall = computed(() => {
const userInfo = currentUserInfo.value || {}
if (!userInfo || !userInfo.id) return false
// 管理员可以导出
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) {
return true
}
// 部门负责人可以导出
if (userInfo.isDepartmentLeader === 1 || userInfo.isDepartmentLeader === true) {
return true
}
// 管理多个部门的用户也是部门负责人
if (managedDepartments.value.length > 0) {
return true
}
return false
})
// 初始化部门视角数据
const initDeptViewData = async () => {
try {
let user: any = parseUserInfo()
// 每次进入任务大厅都刷新一次用户信息,避免本地缓存过期导致权限判断错误
try {
const response = await authApi.getUserInfo()
if (response.data) {
user = response.data
localStorage.setItem('userInfo', JSON.stringify(user))
}
} catch (e) {
console.warn('刷新用户信息失败,回退本地缓存:', e)
}
currentUserInfo.value = user || {}
if (user && user.id) {
userDeptId.value = user.deptId
userDeptName.value = user.deptName || '我的部门'
// 优先使用后端返回的部门类型;缺失时从部门列表回填
let resolvedDeptType = user.deptType
if (!resolvedDeptType && Array.isArray(user.managedDepartments) && user.deptId) {
const selfDept = user.managedDepartments.find((dept: ManagedDepartment) => dept.deptId === user.deptId)
if (selfDept?.deptType) {
resolvedDeptType = selfDept.deptType
}
}
userDeptType.value = resolvedDeptType || 'demand'
// 获取用户管理的部门列表
if (user.managedDepartments && Array.isArray(user.managedDepartments)) {
managedDepartments.value = user.managedDepartments.filter(
(dept: ManagedDepartment) => dept.deptId !== user.deptId // 排除用户自己所属的部门
)
} else {
managedDepartments.value = []
}
}
} catch (e) {
console.error('初始化部门视角数据失败:', e)
}
}
// 切换部门视角
const handleDeptViewChange = (deptId: number | null) => {
console.log('切换部门视角:', deptId, '部门类型:', effectiveDeptType.value)
// 切换后重新加载数据
setTimeout(() => {
loadDashboardStats()
loadAllTabCounts()
// 根据当前Tab加载对应数据
switch (activeTab.value) {
case 'available':
loadTasks(true) // 部门切换时强制刷新
break
case 'in-progress':
loadInProgressTasks()
break
case 'completed':
loadCompletedTasks()
break
case 'published':
loadPublishedTasks()
break
case 'review':
loadReviewTasks()
break
case 'drafts':
loadDraftTasks()
break
}
}, 0) // 使用 setTimeout 确保 effectiveDeptType 已更新
}
// 状态
const loading = ref(false)
const taskList = ref<TaskOrSubTask[]>([])
const total = ref(0)
const detailDrawerVisible = ref(false)
const selectedTask = ref<TaskOrSubTask | null>(null)
// 盲盒模式相关
const blindBoxMode = ref(false)
const blindBoxSeed = ref(Date.now()) // 用于混淆任务顺序的随机种子
const isDeptLeader = computed(() => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 部门负责人或管理员不受盲盒模式影响
return userInfo.isDepartmentLeader === 1 || userInfo.isAdmin === 1 || userInfo.isAdmin === true
})
// 是否显示盲盒(开启盲盒模式且不是部门负责人/管理员)
const showBlindBox = computed(() => {
return blindBoxMode.value && !isDeptLeader.value
})
// 基于种子的伪随机数生成器(确保同一种子产生相同的随机序列)
const seededRandom = (seed: number) => {
const x = Math.sin(seed) * 10000
return x - Math.floor(x)
}
// 使用Fisher-Yates算法基于种子打乱数组
const shuffleWithSeed = <T>(array: T[], seed: number): T[] => {
const result = [...array]
let currentSeed = seed
for (let i = result.length - 1; i > 0; i--) {
currentSeed = currentSeed * 1103515245 + 12345
const j = Math.floor(Math.abs(currentSeed % (i + 1)))
;[result[i], result[j]] = [result[j], result[i]]
}
return result
}
// 盲盒模式下混淆后的任务列表(与部门负责人看到的顺序不同)
const displayTaskList = computed(() => {
if (showBlindBox.value && taskList.value.length > 0) {
// 盲盒模式:使用种子打乱顺序
return shuffleWithSeed(taskList.value, blindBoxSeed.value)
}
// 正常模式:原始顺序
return taskList.value
})
// 加载盲盒模式配置
const loadBlindBoxMode = async () => {
try {
const response = await getAllSystemConfigs()
if (response.code === 200 && response.data) {
const config = response.data.find((c: any) => c.configKey === 'task.blind_box_mode')
if (config) {
blindBoxMode.value = config.configValue === 'true'
// 每次加载盲盒配置时更新随机种子,确保每次刷新页面顺序都不同
if (blindBoxMode.value) {
blindBoxSeed.value = Date.now()
}
console.log('盲盒模式:', blindBoxMode.value, '是否显示盲盒:', showBlindBox.value)
}
}
} catch (error) {
console.error('加载盲盒模式配置失败:', error)
}
}
// 评分弹窗
const ratingModalVisible = ref(false)
const ratingTask = ref<TaskOrSubTask | null>(null)
const ratingValue = ref(5)
const ratingComment = ref('')
const ratingLoading = ref(false)
// 商品ID管理
const taskProducts = ref<TaskProduct[]>([])
const newProductId = ref('')
const batchProductIds = ref('')
const addingProduct = ref(false)
const confirmingProductId = ref(false)
// 已完成任务状态
const completedLoading = ref(false)
const completedTaskList = ref<Task[]>([])
const completedTotal = ref(0)
// 我发起的任务状态
const publishedLoading = ref(false)
const publishedTaskList = ref<Task[]>([])
const publishedTotal = ref(0)
// 草稿箱状态
const draftsLoading = ref(false)
const draftTaskList = ref<Task[]>([])
const draftsTotal = ref(0)
// 是否显示草稿箱tab(需求部门或一级需求部门用户可见)
const showDraftTab = computed(() => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 需求部门、一级需求部门用户或管理员可以看到草稿箱
return userDeptType.value === 'demand' || userDeptType.value === 'primary_demand' || userInfo.isAdmin === 1 || userInfo.isAdmin === true
})
// 进行中任务状态
const inProgressLoading = ref(false)
const inProgressTaskList = ref<Task[]>([])
const inProgressTotal = ref(0)
// 待审核任务状态
const reviewLoading = ref(false)
const reviewTaskList = ref<Task[]>([])
const reviewTotal = ref(0)
// 待我审核任务状态(包含主任务和子任务组)
interface ApprovalItem {
id: number
type: 'task' | 'taskGroup' // 区分类型
data: any // 主任务或子任务组数据
title: string
taskNo: string
score: number
deadline?: string | null
createTime?: string | null
isUrgent?: boolean | number
publisherName?: string | null
publisherDeptName?: string | null
}
const pendingApprovalTasks = ref<ApprovalItem[]>([])
const pendingApprovalTotal = ref(0)
const approvalFilters = reactive({
keyword: undefined as string | undefined
})
const approvalLoading = ref(false)
// 数据总览卡片
const dataCards = ref([
{ id: 1, label: '发布的任务数', value: 0, iconName: 'send' },
{ id: 2, label: '进行中任务', value: 0, iconName: 'clock' },
{ id: 3, label: '已完成任务', value: 0, iconName: 'check-circle' },
{ id: 4, label: '待审核任务', value: 0, iconName: 'file-text' },
{ id: 5, label: '累计积分', value: 0, iconName: 'award' }
])
// 月份选择
const selectedMonth = ref(dayjs())
// 使用全局配置store的数据
const demandTypes = computed(() => configStore.demandTypes)
const brands = computed(() => configStore.brands)
const platforms = computed(() => configStore.platforms)
const categories = computed(() => configStore.categories)
// 工作时间配置(用于时效格式化)
const workHoursPerDay = ref(9) // 默认9小时
// 层级任务类型数据(用于级联选择器)
const hierarchicalTaskTypes = ref<any[]>([])
// 任务子类型列表(需求类型:新品/优化/视频/实拍等)
const taskSubTypes = ref<any[]>([])
// 🔥 接单部门列表(用于筛选和显示标签)
const receivingDeptList = ref<Array<{ id: number; deptName: string }>>([])
// 递归查找任务类型(从层级数据中查找,支持三级分类)
const findTaskTypeById = (id: number, items: any[] = hierarchicalTaskTypes.value): any => {
for (const item of items) {
if (item.id === id) return item
if (item.children) {
const found = findTaskTypeById(id, item.children)
if (found) return found
}
}
return null
}
// 获取任务类型的完整路径信息(包含父级名称和积分)
const getTaskTypeFullPath = (id: number, items: any[] = hierarchicalTaskTypes.value, path: string[] = []): { name: string, fullPath: string, unitScore: number } | null => {
for (const item of items) {
const currentPath = [...path, item.name]
if (item.id === id) {
return {
name: item.name,
fullPath: currentPath.join(' > '),
unitScore: item.unitScore || 0
}
}
if (item.children) {
const found = getTaskTypeFullPath(id, item.children, currentPath)
if (found) return found
}
}
return null
}
// 🔥 获取任务可接单部门名称列表
const getTaskAllowedDeptNames = (task: any): string[] => {
if (!task) return []
// 优先从任务类型配置中获取
const taskTypeId = task.taskTypeId
if (!taskTypeId) return []
// 从层级任务类型数据中查找
const taskType = findTaskTypeById(taskTypeId)
if (!taskType) return []
// 获取 allowedDeptIds(支持从父级继承)
let allowedDeptIds = taskType.allowedDeptIds
if ((!allowedDeptIds || allowedDeptIds.trim() === '') && taskType.parentId) {
const parentType = findTaskTypeById(taskType.parentId)
if (parentType) {
allowedDeptIds = parentType.allowedDeptIds
}
}
// 如果为空,表示所有接单部门都可以接单,不显示标签
if (!allowedDeptIds || allowedDeptIds.trim() === '') {
return []
}
// 将部门ID转换为部门名称,只返回能找到名称的部门
const deptIds = allowedDeptIds.split(',').map((id: string) => parseInt(id.trim()))
const deptNames: string[] = []
for (const id of deptIds) {
const dept = receivingDeptList.value.find(d => d.id === id)
if (dept && dept.deptName) {
deptNames.push(dept.deptName)
}
}
return deptNames
}
// 构建级联选择器选项
const taskTypeCascaderOptions = computed(() => {
const buildOptions = (items: any[]): any[] => {
return items.map(item => {
const option: any = {
value: item.id,
label: item.name
}
if (item.children && item.children.length > 0) {
option.children = buildOptions(item.children)
}
return option
})
}
return buildOptions(hierarchicalTaskTypes.value)
})
// 筛选条件
const filters = reactive({
status: '' as string, // 任务状态筛选
taskTypeId: undefined as number | undefined,
taskSubType: undefined as string | undefined, // 需求类型(新品/优化/视频/实拍等)
brandId: undefined as number | undefined,
categoryId: undefined as number | undefined,
platformId: undefined as number | undefined,
minScore: undefined as number | undefined,
maxScore: undefined as number | undefined,
keyword: undefined as string | undefined,
publisherName: undefined as string | undefined,
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
sortBy: 'oldest' as 'oldest' | 'latest' | 'score' | 'deadline' | 'hot', // 🔥 默认改为最早优先
filterDeptId: undefined as number | undefined // 🔥 按接单部门筛选
})
// 已完成任务筛选条件
const completedFilters = reactive({
keyword: undefined as string | undefined,
sortBy: 'latest' as 'latest' | 'score',
taskTypeId: undefined as number | undefined,
taskSubType: undefined as string | undefined, // 需求类型(新品/优化/视频/实拍等)
brandId: undefined as number | undefined,
categoryId: undefined as number | undefined,
platformId: undefined as number | undefined,
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
minScore: undefined as number | undefined,
maxScore: undefined as number | undefined
})
// 我发起的任务筛选条件
const publishedFilters = reactive({
keyword: undefined as string | undefined,
status: '' as string,
sortBy: 'latest' as 'latest' | 'score' | 'deadline',
taskTypeId: undefined as number | undefined,
brandId: undefined as number | undefined,
categoryId: undefined as number | undefined,
platformId: undefined as number | undefined,
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
minScore: undefined as number | undefined,
maxScore: undefined as number | undefined
})
// 草稿箱筛选条件
const draftsFilters = reactive({
keyword: undefined as string | undefined,
sortBy: 'latest' as 'latest' | 'oldest'
})
// 进行中任务筛选条件
const inProgressFilters = reactive({
keyword: undefined as string | undefined,
status: '' as string, // 任务状态筛选
sortBy: 'latest' as 'latest' | 'deadline' | 'progress',
taskTypeId: undefined as number | undefined,
brandId: undefined as number | undefined,
categoryId: undefined as number | undefined,
platformId: undefined as number | undefined,
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
minProgress: undefined as number | undefined,
maxProgress: undefined as number | undefined
})
// 待审核任务筛选条件
const reviewFilters = reactive({
keyword: undefined as string | undefined,
status: '' as string, // 审核状态筛选
sortBy: 'latest' as 'latest' | 'score',
taskTypeId: undefined as number | undefined,
brandId: undefined as number | undefined,
categoryId: undefined as number | undefined,
platformId: undefined as number | undefined,
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
minScore: undefined as number | undefined,
maxScore: undefined as number | undefined
})
// ========== 筛选组件配置选项 ==========
// 可接任务 - 排序选项
const availableSortOptions = [
{ value: 'oldest', label: '最早发布' }, // 🔥 新增:任务排队机制,最早优先
{ value: 'latest', label: '最新发布' },
{ value: 'score', label: '积分最高' },
{ value: 'deadline', label: '时效最短' },
{ value: 'hot', label: '最热门' }
]
// 可接任务 - 状态选项
const availableStatusOptions = [
{ value: '', label: '全部待接' },
{ value: 'pending', label: '待接单' },
{ value: 'pending_receiving_approval', label: '待部门审核' }
]
// 待审核任务 - 排序选项
const reviewSortOptions = [
{ value: 'latest', label: '最新提交' },
{ value: 'score', label: '积分最高' }
]
// 待审核任务 - 状态选项
const reviewStatusOptions = [
{ value: '', label: '全部待审核' },
{ value: 'pending_primary_approval', label: '待一级审核' },
{ value: 'pending_demand_approval', label: '待需求审核' },
{ value: 'pending_receiving_approval', label: '待接单审核' },
{ value: 'pending_detail_completion', label: '待完善明细' }
]
// 进行中任务 - 排序选项
const inProgressSortOptions = [
{ value: 'latest', label: '最新接单' },
{ value: 'deadline', label: '截止时间' },
{ value: 'progress', label: '完成进度' }
]
// 进行中任务 - 状态选项
const inProgressStatusOptions = [
{ value: '', label: '全部进行中' },
{ value: 'in_progress', label: '进行中' },
{ value: 'submitted', label: '已提交待确认' }
]
// 已完成任务 - 排序选项
const completedSortOptions = [
{ value: 'latest', label: '最新完成' },
{ value: 'score', label: '积分最高' }
]
// 我发起的任务 - 排序选项
const publishedSortOptions = [
{ value: 'latest', label: '最新发布' },
{ value: 'score', label: '积分最高' },
{ value: 'deadline', label: '截止时间' }
]
// 我发起的任务 - 状态选项
const publishedStatusOptions = [
{ value: '', label: '全部状态' },
{ value: 'pending', label: '待接单' },
{ value: 'pending_demand_approval', label: '待需求审核' },
{ value: 'pending_receiving_approval', label: '待接单审核' },
{ value: 'in_progress', label: '进行中' },
{ value: 'submitted', label: '已提交' },
{ value: 'completed', label: '已完成' }
]
// 草稿箱 - 排序选项
const draftsSortOptions = [
{ value: 'latest', label: '最新编辑' },
{ value: 'oldest', label: '最早编辑' }
]
// 分页
const pagination = reactive({
page: 1,
pageSize: 12
})
// 已完成任务分页
const completedPagination = reactive({
page: 1,
pageSize: 12
})
// 获取任务类型名称(优先显示任务子类型,如:新品、优化、视频、实拍)
const getTaskTypeName = (typeId: number, taskTypeName?: string, taskSubType?: string) => {
// 优先显示任务子类型(如:新品、优化、视频、实拍)
if (taskSubType) return taskSubType
// 如果有直接传入的类型名称,使用它
if (taskTypeName) return taskTypeName
// 从层级数据中递归查找(支持三级分类)
const typeInfo = findTaskTypeById(typeId)
if (typeInfo) return typeInfo.name
// 兼容:从平铺列表中查找
const type = demandTypes.value.find(t => t.id === typeId)
return type?.name || '未知类型'
}
// 解析任务类型明细
const parseTypeDetails = (typeDetails: string | TaskTypeDetail[] | undefined): TaskTypeDetail[] => {
if (!typeDetails) return []
if (Array.isArray(typeDetails)) return typeDetails
try {
return JSON.parse(typeDetails) as TaskTypeDetail[]
} catch (e) {
console.warn('解析任务类型明细失败:', e)
return []
}
}
// 获取任务类型单位积分
const getTaskTypeUnitScore = (typeId: number) => {
// 从层级数据中递归查找(支持三级分类)
const typeInfo = findTaskTypeById(typeId)
if (typeInfo && typeInfo.unitScore) return typeInfo.unitScore
// 兼容:从平铺列表中查找
const type = demandTypes.value.find(t => t.id === typeId)
return type?.unitScore || 0
}
// 计算审核弹窗中的预估总积分
const approveEstimatedScore = computed(() => {
const total = approveTaskTypes.value.reduce((sum, item) => {
const unitScore = getTaskTypeUnitScore(item.typeId)
return sum + (unitScore * item.quantity)
}, 0)
// 使用 formatScore 处理浮点数精度问题
return formatScore(total)
})
// 截断文本(用于显示长名字)
const truncateText = (text: string | undefined, maxLength: number = 10): string => {
if (!text) return '--'
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return '--'
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
// 格式化日期(用于已完成任务)
const formatDate = (date: string | undefined) => {
if (!date) return '--'
return dayjs(date).format('YYYY-MM-DD')
}
// 格式化完整日期时间(用于鼠标悬停提示)
const formatFullDateTime = (date: string | undefined) => {
if (!date) return ''
return dayjs(date).format('YYYY年MM月DD日 HH:mm:ss')
}
// 格式化积分(保留两位小数,修复浮点数精度问题)
const formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
// 先乘以100,四舍五入取整,再除以100,避免浮点数精度问题
return (Math.round(Number(score) * 100) / 100).toFixed(2)
}
// 格式化截止时间
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 `已过期 (${deadlineTime.format('YYYY-MM-DD HH:mm')})`
} else if (diffHours < 24) {
return `${diffHours} 小时后截止`
} else {
const diffDays = Math.floor(diffHours / 24)
return `${diffDays} 天后截止 (${deadlineTime.format('YYYY-MM-DD HH:mm')})`
}
}
// 格式化时效要求(工时转换为天+小时,基于工作时间配置)
// 🔥 最低时效为1小时:低于1小时的任务显示为1小时
const formatEstimatedTime = (hours: number | undefined) => {
if (!hours) return '--'
// 最低时效1小时:低于1小时的显示为1小时
const effectiveHours = hours < 1 ? 1 : hours
const workHours = workHoursPerDay.value || 9 // 默认9小时
const days = Math.floor(effectiveHours / workHours)
const remainingHours = Math.round((effectiveHours % workHours) * 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 getSubTaskStatusClass = (status: string | undefined) => {
switch (status) {
case 'completed': return 'completed'
case 'in_progress': return 'in-progress'
default: return 'pending'
}
}
const getSubTaskLabel = (subTask: any, index: number) => {
// 如果有子任务编号,显示子任务编号的后两段(部门序号-部门内序号)
// 新格式:TASK...-01-02 显示为 "子01-02"
// 旧格式:TASK...-01 显示为 "子01"
if (subTask.itemNo) {
const parts = subTask.itemNo.split('-')
if (parts.length >= 3) {
// 新格式:取最后两段
return `子${parts[parts.length - 2]}-${parts[parts.length - 1]}`
} else if (parts.length === 2) {
// 旧格式:取最后一段
return `子${parts[parts.length - 1]}`
}
}
return `子${String(index + 1).padStart(2, '0')}`
}
const getSubTaskStatusText = (status: string | undefined) => {
const statusMap: Record<string, string> = {
'locked': '已锁定',
'pending': '待接单',
'pending_approval': '待审核',
'in_progress': '进行中',
'completed': '已完成',
'rejected': '已驳回'
}
return statusMap[status || ''] || status || '未知'
}
// 获取子任务完成截止日期(使用后端返回的deadline字段)
// 只有已接单或进行中的子任务才显示截止时间
const getSubTaskDeadline = (subTask: any) => {
// 只有进行中或已完成的子任务才显示截止时间
if (subTask.status !== 'in_progress' && subTask.status !== 'completed') return null
// 直接使用后端返回的deadline字段
if (!subTask.deadline) return null
// 格式化为"XX月XX日HH:mm"
return dayjs(subTask.deadline).format('MM月DD日 HH:mm')
}
// 获取任务完成截止时间(用于进行中的任务)
// 计算方式:从任务接单开始 + 子任务耗时(流水线任务)或任务耗时(普通任务)
const getTaskDeadline = (task: any): string | null => {
if (!task || task.status !== 'in_progress') return null
// 优先使用后端返回的deadline字段(如果存在且有效)
if (task.deadline) {
return task.deadline
}
// 如果没有deadline,尝试计算(流水线任务:从接单时间开始 + 所有子任务预估工时;普通任务:从接单时间开始 + 任务预估工时)
if (task.startTime) {
let totalHours = 0
// 流水线任务:累加所有子任务的预估工时
if (task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0) {
totalHours = task.subTasks.reduce((sum: number, subTask: any) => {
return sum + (subTask.estimatedHours || 0)
}, 0)
} else if (task.estimatedHours) {
// 普通任务:使用任务的预估工时
totalHours = task.estimatedHours
}
if (totalHours > 0) {
// 简单计算:接单时间 + 预估工时(小时)
// 注意:这是简化计算,不考虑工作时间配置,最准确的应该使用后端计算的deadline
const startTime = dayjs(task.startTime)
const deadline = startTime.add(totalHours, 'hour')
return deadline.format('YYYY-MM-DD HH:mm:ss')
}
}
return null
}
// 判断任务截止时间是否已过期
const isTaskDeadlineOverdue = (task: any): boolean => {
const deadline = getTaskDeadline(task)
if (!deadline) return false
return dayjs().isAfter(dayjs(deadline))
}
// 展开/折叠任务子任务详情
const expandedTaskSubtasks = ref<Record<number, boolean>>({})
const toggleTaskSubtasks = (taskId: number) => {
expandedTaskSubtasks.value[taskId] = !expandedTaskSubtasks.value[taskId]
}
// 判断任务是否有子任务在进行中
const hasInProgressSubTask = (task: any) => {
if (task.isPipeline !== 1) return false
return (task.subTaskInProgress || 0) > 0
}
/**
* 获取任务流程阶段
* @param task 任务对象
* @returns 流程阶段文本
*/
const getProcessStage = (task: TaskOrSubTask): string => {
// 1. 需求负责人审核阶段
if (task.approvalStatus === 'pending_demand_approval' && task.currentApproverRole === 'demand_leader') {
return `需求负责人:${task.currentApproverName || '未知'}审核`
}
// 2. 设计负责人审核阶段
if (task.approvalStatus === 'pending_receiving_approval' && task.currentApproverRole === 'receiving_leader') {
return `设计负责人:${task.currentApproverName || '未知'}审核`
}
// 3. 任务被接单后:开始设计中
if (task.status === 'in_progress' && task.receiverId) {
return '开始设计中'
}
// 4. 任务被接单前:根据排队状态显示
if (task.status === 'pending' && !task.receiverId) {
// 检查排队状态
if (task.unlockStatus === 'queuing') {
const position = task.queuePosition
return position ? `排队中(第${position}位)` : '排队中'
}
return '任务正在被抢单中'
}
// 5. 其他状态
if (task.status === 'review') {
return '待审核'
}
if (task.status === 'completed') {
return '已完成'
}
if (task.status === 'rejected') {
return '已驳回'
}
if (task.status === 'cancelled') {
return '已取消'
}
// 默认返回空字符串
return ''
}
/**
* 获取待审核页面的流程阶段(支持子任务组)
*/
const getProcessStageForReview = (task: any): string => {
// 子任务组:使用后端返回的审核人信息
if (task._itemType === 'taskGroup') {
if (task.currentApproverName) {
return `设计负责人:${task.currentApproverName}审核`
}
return '设计负责人审核'
}
// 主任务:使用原有逻辑
return getProcessStage(task)
}
// 判断是否为新发布任务(1小时内)
const isNewTask = (task: TaskOrSubTask) => {
if (!task.createTime) return false
const createTime = dayjs(task.createTime)
const now = dayjs()
return now.diff(createTime, 'hour') < 1
}
// 判断是否为高分任务(积分 >= 500)
const isHighScoreTask = (task: TaskOrSubTask) => {
return task.score && task.score >= 500
}
// 判断是否可以下载附件
const canDownloadAttachment = (task: TaskOrSubTask) => {
// 管理员可下载
const userInfo = currentUserInfo.value || {}
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) {
return true
}
// 接单部门负责人在可接任务视角可提前查看附件
if (task.status === 'pending' && canViewAvailableTaskAttachmentInfo.value) {
return true
}
// 非pending状态可下载(已接单、进行中、已完成等)
if (task.status !== 'pending') {
return true
}
return false
}
// 解析附件链接
const parseAttachmentUrls = (attachmentUrl?: string) => {
if (!attachmentUrl) return []
return attachmentUrl.split(',').map(url => url.trim()).filter(Boolean)
}
// 可接任务卡片:接单部门负责人可提前查看附件信息
const canViewAvailableTaskAttachmentInfo = computed(() => {
const userInfo = currentUserInfo.value || {}
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
if (isAdmin) {
return true
}
if (!isDepartmentLeader.value) {
return false
}
// 当前切换部门为接单部门
if (effectiveDeptType.value === 'receiving') {
return true
}
// 未切换视角时,用户自身部门为接单部门
if (currentDeptView.value === undefined && userDeptType.value === 'receiving') {
return true
}
return false
})
// 可接任务:接单部门负责人可编辑待接单主任务明细
const canEditAvailableTaskDetail = (task: TaskOrSubTask | null | undefined) => {
if (!task) return false
if (task.status !== 'pending') return false
if (Number((task as any).isSubTask) === 1 || !!task.itemId) return false
return canViewAvailableTaskAttachmentInfo.value
}
// 加载配置数据(从全局store加载)
const loadConfigs = async () => {
try {
await configStore.loadAllConfigs()
// 加载层级任务类型数据(用于级联选择器和递归查找)
const response = await configApi.getDemandTypesTree()
if (response && response.data) {
hierarchicalTaskTypes.value = response.data
}
// 加载任务子类型(需求类型:新品/优化/视频/实拍等)
const subTypeResponse = await configApi.getEnabledTaskSubTypes()
if (subTypeResponse && subTypeResponse.data) {
taskSubTypes.value = subTypeResponse.data
}
// 🔥 加载接单部门列表
const deptResponse = await departmentApi.getDepartmentList()
if (deptResponse && deptResponse.data) {
// 过滤出接单部门(deptType === 'receiving'),只保留一级和二级部门
const allDepts = deptResponse.data
const level1DeptIds = new Set(
allDepts.filter((d: any) => d.deptType === 'receiving' && (!d.parentId || d.parentId === 0))
.map((d: any) => d.id)
)
receivingDeptList.value = allDepts.filter((d: any) =>
d.deptType === 'receiving' &&
(!d.parentId || d.parentId === 0 || level1DeptIds.has(d.parentId))
)
}
} catch (error) {
console.error('加载配置数据失败:', error)
}
}
// 缓存所有可接任务数据(用于前端分页)
let allAvailableTasks: any[] = []
// 加载任务大厅列表(可接单的任务)
// 任务大厅对所有用户开放(包括需求部门和一级需求部门)
const loadTasks = async (forceReload = false) => {
loading.value = true
// 🔀 每次加载任务时更新盲盒随机种子,确保顺序每次都不同
// 防止有人根据排队序号分辨任务
if (blindBoxMode.value) {
blindBoxSeed.value = Date.now() + Math.random() * 1000
}
try {
// 获取用户部门类型(使用部门视角切换后的有效类型)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const currentDeptType = effectiveDeptType.value // 使用切换后的部门类型
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
// 所有用户都可以查看任务大厅(包括需求部门和一级需求部门)
// 注释掉原来的限制逻辑
// if (currentDeptType === 'demand' && !isAdmin) {
// taskList.value = []
// total.value = 0
// allAvailableTasks = []
// return
// }
// 如果不是强制刷新且已有缓存数据,直接使用缓存进行分页
if (!forceReload && allAvailableTasks.length > 0) {
const start = (pagination.page - 1) * pagination.pageSize
const end = start + pagination.pageSize
taskList.value = allAvailableTasks.slice(start, end)
return
}
// 构建筛选参数(不传分页参数,获取所有数据)
const params: any = {
page: 1,
pageSize: 1000 // 获取足够多的数据用于前端分页
}
// 添加筛选条件
if (filters.status) params.status = filters.status
if (filters.taskTypeId) params.taskTypeId = filters.taskTypeId
if (filters.brandId) params.brandId = filters.brandId
if (filters.categoryId) params.categoryId = filters.categoryId
if (filters.platformId) params.platformId = filters.platformId
if (filters.taskSubType) params.taskSubType = filters.taskSubType
if (filters.minScore) params.minScore = Number(filters.minScore)
if (filters.maxScore) params.maxScore = Number(filters.maxScore)
if (!showBlindBox.value && filters.keyword) params.keyword = filters.keyword.trim()
if (!showBlindBox.value && filters.publisherName) params.publisherName = filters.publisherName.trim()
if (filters.timeRange) params.timeRange = filters.timeRange
if (filters.sortBy) params.sortBy = filters.sortBy
if (filters.filterDeptId) params.filterDeptId = filters.filterDeptId // 🔥 按接单部门筛选
let allTasks: any[] = []
// 同时加载普通任务和子任务(流水线任务)
const [taskResponse, itemResponse] = await Promise.all([
taskApi.getTaskHall(params),
taskApi.getAvailableTaskItems(params)
])
// 处理普通任务(非流水线任务)
if (taskResponse.code === 200 && taskResponse.data && taskResponse.data.records) {
const tasks = taskResponse.data.records.map((task: any) => ({
...task,
id: task.id,
taskNo: task.taskNo,
title: task.title,
isSubTask: false, // 普通任务标记为非子任务
taskId: task.id,
taskTypeId: task.taskTypeId,
taskTypeName: task.taskTypeName,
taskSubType: task.taskSubType,
score: task.score,
estimatedHours: task.estimatedHours,
quantity: task.quantity || 1,
previewImage: task.previewImage,
description: task.remark || task.description
}))
allTasks = [...allTasks, ...tasks]
}
// 处理子任务(流水线任务)
if (itemResponse.code === 200 && itemResponse.data && itemResponse.data.records) {
const items = (itemResponse.data.records || []).map((item: any) => ({
...item,
id: item.id,
itemId: item.id,
itemNo: item.itemNo,
title: item.mainTaskTitle || item.taskTitle || item.title,
subTaskNo: item.itemNo,
isSubTask: true, // 子任务标记
taskId: item.taskId,
groupNo: item.groupNo,
taskTypeId: item.taskTypeId,
taskTypeName: item.taskTypeName,
typeDetails: item.typeDetails, // 任务类型明细
taskSubType: item.taskSubType,
score: item.totalScore || item.score,
estimatedHours: item.estimatedHours,
quantity: item.quantity || 1,
previewImage: item.previewImage,
prevDownloadUrl: item.prevDownloadUrl,
description: item.description || item.taskDescription
}))
allTasks = [...allTasks, ...items]
}
// 🔥 移除前端排序,使用后端排序结果(后端已按可抢单优先+时间排序)
// 如果需要时间排序,后端会根据 sortBy 参数处理
// 缓存所有数据
allAvailableTasks = allTasks
total.value = allTasks.length
// 前端分页
const start = (pagination.page - 1) * pagination.pageSize
const end = start + pagination.pageSize
taskList.value = allTasks.slice(start, end)
} catch (error) {
console.error('加载任务失败:', error)
message.error('加载任务失败')
} finally {
loading.value = false
}
}
// 筛选变更
const handleFilterChange = (newFilters?: any) => {
// 如果传入了新的筛选值,合并到 filters 对象
if (newFilters) {
Object.assign(filters, newFilters)
}
pagination.page = 1
loadTasks(true) // 强制重新加载
}
// 重置筛选
const handleReset = () => {
filters.taskTypeId = undefined
filters.taskSubType = undefined
filters.brandId = undefined
filters.categoryId = undefined
filters.platformId = undefined
filters.taskSubType = undefined
filters.minScore = undefined
filters.maxScore = undefined
filters.keyword = undefined
filters.publisherName = undefined
filters.timeRange = undefined
filters.sortBy = 'oldest' // 🔥 默认改为最早优先
pagination.page = 1
loadTasks(true) // 强制重新加载
}
watch(showBlindBox, (enabled) => {
if (enabled) {
if (filters.keyword) {
filters.keyword = undefined
}
if (filters.publisherName) {
filters.publisherName = undefined
}
pagination.page = 1
loadTasks(true)
}
})
// 页码变更
const handlePageChange = () => {
loadTasks() // 使用缓存数据进行分页
}
// 加载已完成任务列表
// - 需求部门用户:看到自己发起的已完成的母任务
// - 接单部门用户:看到自己完成的子任务
// - 管理员:看到所有已完成任务
const loadCompletedTasks = async (newFilters?: any) => {
// 如果传入了新的筛选值,合并到 completedFilters 对象
if (newFilters && typeof newFilters === 'object') {
Object.assign(completedFilters, newFilters)
completedPagination.page = 1 // 重置分页
}
completedLoading.value = true
try {
const params: any = {
page: completedPagination.page,
pageSize: completedPagination.pageSize,
status: 'completed'
}
// 添加筛选条件
if (completedFilters.keyword) params.keyword = completedFilters.keyword
if (completedFilters.sortBy) params.sortBy = completedFilters.sortBy
// 如果切换了部门视角,传递指定部门ID
if (currentDeptView.value !== undefined) {
params.deptId = currentDeptView.value
}
// 获取用户部门类型(使用部门视角切换后的有效类型)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const currentDeptType = effectiveDeptType.value // 使用切换后的部门类型
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
const isDeptLeader = userInfo.isDepartmentLeader === 1 || managedDepartments.value.length > 0
let allTasks: any[] = []
let totalCount = 0
// 优先判断是否是部门负责人切换视角
if (isDeptLeader && currentDeptView.value !== undefined) {
// 部门负责人切换到其他部门视角
console.log('已完成任务 - 部门负责人视角切换,部门ID:', currentDeptView.value, '部门类型:', currentDeptType)
if (currentDeptType === 'receiving') {
// 接单部门视角:看该部门成员完成的子任务
const response = await taskApi.getCompletedTaskItems(params)
if (response.data && response.data.records) {
allTasks = response.data.records.map((item: any) => ({
...mapTaskItemToTask(item),
status: 'completed'
}))
totalCount = response.data.total || 0
}
} else {
// 需求部门视角:看该部门发起的已完成任务
const response = await taskApi.getCompletedTasks({
...params,
publisherDeptId: currentDeptView.value
})
if (response.data && response.data.records) {
allTasks = response.data.records.map((task: any) => ({
...task,
isSubTask: false
}))
totalCount = response.data.total || 0
}
}
} else if (isAdmin) {
// 管理员:看到所有已完成任务(母任务和子任务)
const [taskResponse, itemResponse] = await Promise.all([
taskApi.getCompletedTasks(params),
taskApi.getCompletedTaskItems(params)
])
if (taskResponse.data && taskResponse.data.records) {
const tasks = taskResponse.data.records.map((task: any) => ({
...task,
isSubTask: false
}))
allTasks = [...allTasks, ...tasks]
totalCount += taskResponse.data.total || 0
}
if (itemResponse.data && itemResponse.data.records) {
const items = itemResponse.data.records.map((item: any) => ({
...mapTaskItemToTask(item),
status: 'completed'
}))
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
}
} else if (currentDeptType === 'demand') {
// 需求部门普通用户:看到自己发起的已完成的母任务
const response = await taskApi.getCompletedTasks(params)
if (response.data && response.data.records) {
allTasks = response.data.records.map((task: any) => ({
...task,
isSubTask: false
}))
totalCount = response.data.total || 0
}
} else {
// 接单部门普通用户:看到自己完成的子任务
const response = await taskApi.getCompletedTaskItems(params)
if (response.data && response.data.records) {
allTasks = response.data.records.map((item: any) => ({
...mapTaskItemToTask(item),
status: 'completed'
}))
totalCount = response.data.total || 0
}
}
// 按完成时间排序
allTasks.sort((a, b) => {
const timeA = new Date(a.completeTime || a.updateTime || 0).getTime()
const timeB = new Date(b.completeTime || b.updateTime || 0).getTime()
return timeB - timeA // 降序
})
completedTaskList.value = allTasks
completedTotal.value = totalCount
} catch (error) {
console.error('加载已完成任务失败:', error)
message.error('加载已完成任务失败')
} finally {
completedLoading.value = false
}
}
// 标签页切换
const handleTabChange = (key: string) => {
if (key === 'completed') {
loadCompletedTasks()
} else if (key === 'published') {
loadPublishedTasks()
} else if (key === 'inProgress') {
loadInProgressTasks()
} else if (key === 'review') {
loadReviewTasks()
} else if (key === 'drafts') {
loadDraftTasks() // 加载草稿箱任务
} else {
loadTasks(true) // 切换Tab时强制刷新
}
}
// ==================== 审核相关方法 ====================
// 加载待审核任务(包含主任务和子任务组)
const loadPendingApprovalTasks = async () => {
try {
loading.value = true
// 并行加载主任务和子任务组
const [tasksResponse, groupsResponse] = await Promise.all([
taskApi.getPendingApprovalTasks(),
taskApi.getPendingApprovalTaskGroups()
])
const approvalItems: ApprovalItem[] = []
// 转换主任务为ApprovalItem
if (tasksResponse && tasksResponse.data) {
const tasks = tasksResponse.data as Task[]
tasks.forEach(task => {
if (!task.id || !task.taskNo) return // 跳过无效任务
approvalItems.push({
id: task.id,
type: 'task',
data: task,
title: task.title,
taskNo: task.taskNo,
score: task.score || 0,
deadline: task.deadline || null,
createTime: task.createTime || null,
isUrgent: !!task.isUrgent,
publisherName: task.publisherName || null,
publisherDeptName: task.publisherDeptName || null
})
})
}
// 转换子任务组为ApprovalItem
if (groupsResponse && groupsResponse.data) {
const groups = groupsResponse.data as taskApi.TaskGroup[]
groups.forEach(group => {
approvalItems.push({
id: group.groupId,
type: 'taskGroup',
data: group,
title: `${group.mainTaskTitle} - 第${group.groupId}组`,
taskNo: group.mainTaskNo,
score: group.totalScore || 0,
deadline: null, // 子任务组没有独立的截止时间
createTime: null,
isUrgent: false,
publisherName: null,
publisherDeptName: null
})
})
}
// 如果有搜索关键词,进行本地过滤
if (approvalFilters.keyword && approvalFilters.keyword.trim()) {
const keyword = approvalFilters.keyword.trim().toLowerCase()
pendingApprovalTasks.value = approvalItems.filter(item =>
item.title?.toLowerCase().includes(keyword) ||
item.taskNo?.toLowerCase().includes(keyword)
)
} else {
pendingApprovalTasks.value = approvalItems
}
pendingApprovalTotal.value = pendingApprovalTasks.value.length
} catch (error: any) {
console.error('加载待审核任务失败:', error)
message.error(error.msg || error.message || '加载待审核任务失败')
} finally {
loading.value = false
}
}
// 获取审核状态颜色
const getApprovalStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending_primary_approval': 'purple',
'pending_detail_completion': 'cyan',
'pending_demand_approval': 'orange',
'pending_receiving_approval': 'blue',
'approved': 'green',
'rejected_by_demand': 'red',
'rejected_by_receiving': 'red'
}
return colorMap[status] || 'default'
}
// 获取审核状态文本
const getApprovalStatusText = (status: string) => {
const textMap: Record<string, string> = {
'pending_primary_approval': '一级需求待审核',
'pending_detail_completion': '待完善明细',
'pending_demand_approval': '需求部门待审核',
'pending_receiving_approval': '接单部门待审核',
'approved': '已通过',
'rejected_by_demand': '需求部门已驳回',
'rejected_by_receiving': '接单部门已驳回'
}
return textMap[status] || status
}
// 两级审核:审核通过
const handleApproveTwoLevelTask = async (task: Task) => {
// 如果是接单部门负责人审核,显示指定设计师选项
if (task.currentApproverRole === 'receiving_leader') {
showAssignDesignerModal(task)
} else {
// 需求部门负责人审核,直接通过
Modal.confirm({
title: '确认审核通过',
content: `确定通过任务"${task.title}"的审核吗?`,
okText: '确认通过',
cancelText: '取消',
onOk: async () => {
try {
approvalLoading.value = true
const approvalData: TaskApprovalDTO = {
comment: '审核通过'
}
await taskApi.approveDemandLevel(task.id!, approvalData)
message.success('需求部门审核通过')
// 刷新列表和数量
loadPendingApprovalTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
console.error('审核失败:', error)
message.error(error.msg || error.message || '审核失败')
} finally {
approvalLoading.value = false
}
}
})
}
}
// 审批弹窗状态
const approveModalVisible = ref(false)
const approveTaskData = ref<Task | null>(null)
const approveAssignType = ref<'auto' | 'manual'>('auto')
const approveTaskTypes = ref<Array<{typeId: number, quantity: number}>>([])
const approveDesignerId = ref<number | undefined>(undefined)
const designersList = ref<DesignerWithWorkload[]>([])
const newTaskTypePath = ref<(string | number)[] | undefined>(undefined)
const preAssignedDesignerName = ref<string>('') // 预先指定的设计师姓名
const preAssignedDesignersList = ref<Array<{typeName: string, designerName: string, quantity: number}>>([]) // 多个预先指定的设计师列表
// 一级需求审核弹窗状态
const primaryApproveModalVisible = ref(false)
const primaryApproveTaskData = ref<Task | null>(null)
const primaryApproveCompleterId = ref<number | undefined>(undefined)
const primaryApproveComment = ref('')
const deptMembersList = ref<Array<{id: number, realName: string, username: string, deptName?: string}>>([])
const primaryApprovalLoading = ref(false)
// 判断任务是否已预先指定设计师
const isTaskPreAssigned = computed(() => {
if (!approveTaskData.value) return false
const task = approveTaskData.value as any
// 检查是否有预先指定的设计师列表(多个子任务情况)
if (preAssignedDesignersList.value.length > 0) return true
// 兼容旧逻辑:检查主任务的 receiverId
return task.releaseType === 'assigned' && task.receiverId != null
})
// 级联选择器搜索过滤
const cascaderFilter = (inputValue: string, path: any[]) => {
return path.some(option =>
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
)
}
// 添加审核弹窗中的任务类型
const handleAddApproveTaskType = () => {
if (!newTaskTypePath.value || newTaskTypePath.value.length === 0) {
message.warning('请选择任务类型')
return
}
// 获取最后一个ID(即最深层级的类型ID)
const typeId = newTaskTypePath.value[newTaskTypePath.value.length - 1] as number
if (approveTaskTypes.value.find(t => t.typeId === typeId)) {
message.warning('该任务类型已添加')
return
}
approveTaskTypes.value.push({ typeId, quantity: 1 })
newTaskTypePath.value = undefined
}
// 格式化相对时间
const formatDeadlineRelative = (deadline: string) => {
if (!deadline) return ''
try {
const date = new Date(deadline)
const now = new Date()
const diff = date.getTime() - now.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
if (diff < 0) return '已逾期'
if (days === 0) return `${hours}小时后`
return `${days}天${hours}小时后`
} catch {
return ''
}
}
// 显示指定设计师弹窗(接单部门审核时)
const showAssignDesignerModal = async (task: Task) => {
try {
// 解析任务类型 - 优先从 typeDetails 获取(包含每个类型的实际数量)
let typeDetailsArray: TaskTypeDetail[] = []
if ((task as any).typeDetails) {
try {
const details = (task as any).typeDetails
typeDetailsArray = typeof details === 'string' ? JSON.parse(details) : details
} catch (e) {
console.warn('解析 typeDetails 失败:', e)
}
}
// 获取任务类型ID用于查询设计师
// 优先从 typeDetails 获取第一个任务类型ID,否则使用 taskTypeId
let targetTaskTypeId: number | undefined = task.taskTypeId
if (typeDetailsArray.length > 0 && typeDetailsArray[0].typeId) {
targetTaskTypeId = typeDetailsArray[0].typeId
}
console.log('获取设计师列表,任务类型ID:', targetTaskTypeId, 'typeDetails:', typeDetailsArray)
// 加载设计师列表及工作量(根据任务类型的接单部门配置)
const designers = await userApi.getDesignersWithWorkload(undefined, targetTaskTypeId)
if (designers.length === 0) {
message.warning('未找到对应接单部门的设计师')
return
}
designersList.value = designers
approveTaskData.value = task
newTaskTypePath.value = undefined
// 检查任务是否已预先指定设计师
const taskAny = task as any
preAssignedDesignersList.value = [] // 重置列表
// 检查 typeDetails 中是否有预先指定的设计师(同部门多设计师情况)
if (typeDetailsArray.length > 0) {
const designersInDetails: Array<{typeName: string, designerName: string, quantity: number}> = []
for (const detail of typeDetailsArray) {
if (detail.receiverId && detail.receiverName) {
designersInDetails.push({
typeName: detail.typeName || getTaskTypeName(detail.typeId),
designerName: detail.receiverName,
quantity: detail.quantity || 1
})
}
}
if (designersInDetails.length > 0) {
preAssignedDesignersList.value = designersInDetails
approveAssignType.value = 'manual'
console.log('任务类型明细中预先指定的设计师:', designersInDetails)
}
}
// 兼容旧逻辑:检查主任务的 receiverId
if (taskAny.releaseType === 'assigned' && taskAny.receiverId && preAssignedDesignersList.value.length === 0) {
// 已预先指定设计师,设置相关状态
approveAssignType.value = 'manual'
approveDesignerId.value = taskAny.receiverId
// 从设计师列表中查找预先指定的设计师姓名
const preAssigned = designers.find(d => d.id === taskAny.receiverId)
preAssignedDesignerName.value = preAssigned?.realName || taskAny.receiverName || '未知'
console.log('任务已预先指定设计师:', preAssignedDesignerName.value)
} else if (preAssignedDesignersList.value.length === 0) {
// 未预先指定,重置状态
approveAssignType.value = 'auto'
approveDesignerId.value = undefined
preAssignedDesignerName.value = ''
}
// 如果有 typeDetails,从中获取任务类型和数量
if (typeDetailsArray.length > 0) {
approveTaskTypes.value = typeDetailsArray.map(detail => ({
typeId: detail.typeId,
quantity: detail.quantity || 1
}))
} else {
// 兼容旧数据:从 taskTypeIds 获取
const taskTypeIdsStr = task.taskTypeIds || task.taskTypeId?.toString() || ''
const taskTypeIdArray = taskTypeIdsStr.split(',').filter(id => id.trim())
approveTaskTypes.value = taskTypeIdArray.map(id => ({
typeId: parseInt(id.trim()),
quantity: task.quantity || 1
}))
}
approveModalVisible.value = true
} catch (error: any) {
console.error('获取设计师列表失败:', error)
message.error('获取设计师列表失败')
}
}
// 确认审核通过
const handleApproveConfirm = async () => {
if (!approveTaskData.value) return
try {
approvalLoading.value = true
// 检查是否需要指定设计师
// 如果已经有预先指定的设计师列表,则跳过检查
if (approveAssignType.value === 'manual' && !approveDesignerId.value && preAssignedDesignersList.value.length === 0) {
message.error('请选择要指定的设计师')
return
}
// 检查任务类型是否为空
if (approveTaskTypes.value.length === 0) {
message.error('至少需要一个任务类型')
return
}
// 判断是子任务组还是主任务
const taskData = approveTaskData.value as any
const isTaskGroup = taskData._isTaskGroup === true
if (isTaskGroup) {
// 子任务组审核
await taskApi.approveTaskGroup(taskData.id, {
groupNo: taskData._groupNo,
assignedReceiverId: approveAssignType.value === 'manual' ? approveDesignerId.value : undefined
})
} else {
// 主任务审核
const approvalData: TaskApprovalDTO = {
comment: approveAssignType.value === 'manual' ?
`指定设计师接单` : '审核通过,发布到任务大厅',
assignedReceiverId: approveAssignType.value === 'manual' ? approveDesignerId.value : undefined,
taskUpdates: {
taskTypes: approveTaskTypes.value.map(t => ({
taskTypeId: t.typeId,
quantity: t.quantity
}))
}
}
await taskApi.approveReceivingLevel(taskData.id!, approvalData)
}
if (approveAssignType.value === 'manual') {
const designer = designersList.value.find(d => d.id === approveDesignerId.value)
message.success(`已指定设计师 ${designer?.realName} 接单`)
} else {
message.success('审核通过,任务已发布到大厅')
}
approveModalVisible.value = false
// 刷新列表和数量
loadReviewTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
console.error('审核失败:', error)
message.error(error.msg || error.message || '审核失败')
} finally {
approvalLoading.value = false
}
}
// 两级审核:驳回任务
const handleRejectTwoLevelTask = async (task: Task) => {
let rejectReason = ''
Modal.confirm({
title: '❌ 驳回任务',
content: h('div', { style: 'padding: 16px 0;' }, [
h('div', {
style: {
marginBottom: '12px',
fontSize: '14px',
fontWeight: 500,
color: '#262626'
}
}, '请输入驳回原因'),
h(Textarea, {
rows: 5,
placeholder: '请详细说明驳回原因,以便发起人修改...',
maxlength: 500,
showCount: true,
style: { fontSize: '14px' },
onChange: (e: any) => {
rejectReason = e.target.value
}
})
]),
width: 550,
okText: '确认驳回',
cancelText: '取消',
okType: 'danger',
okButtonProps: {
size: 'large',
style: { minWidth: '100px' }
},
cancelButtonProps: {
size: 'large',
style: { minWidth: '100px' }
},
onOk: async () => {
if (!rejectReason || !rejectReason.trim()) {
message.warning('请输入驳回原因')
return Promise.reject()
}
try {
approvalLoading.value = true
// 优先检查是否为一级需求部门任务(pending_primary_approval状态)
if (task.approvalStatus === 'pending_primary_approval') {
await taskApi.rejectPrimaryTask(task.id!, rejectReason.trim())
message.success('一级需求任务已驳回,任务已退回给发起人')
} else if (task.approvalStatus === 'pending_detail_completion' || task.currentApproverRole === 'detail_completer') {
// 待完善明细状态的任务驳回(退回给一级需求发起人)
await taskApi.rejectPrimaryTask(task.id!, rejectReason.trim())
message.success('任务已驳回,已退回给一级需求发起人')
} else if (task.currentApproverRole === 'demand_leader') {
// 需求部门负责人驳回(pending_demand_approval状态)
await taskApi.rejectByDemandLeader(task.id!, rejectReason.trim())
message.success('已驳回,任务已退回给发起人')
} else if (task.currentApproverRole === 'receiving_leader') {
// 接单部门负责人驳回(pending_receiving_approval状态)
await taskApi.rejectByReceivingLeader(task.id!, rejectReason.trim())
message.success('已驳回,任务已退回给需求部门负责人')
} else {
message.error('无法确定审核角色')
return Promise.reject()
}
// 刷新列表和数量
loadPendingApprovalTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
console.error('驳回失败:', error)
message.error(error.msg || error.message || '驳回失败')
} finally {
approvalLoading.value = false
}
}
})
}
// ==================== 子任务组审核相关方法 ====================
// 审核通过子任务组
const handleApproveTaskGroup = async (item: ApprovalItem) => {
try {
// 获取设计师列表及工作量
const designersWithWorkload = await userApi.getDesignersWithWorkload()
let assignType = ref<'auto' | 'manual'>('auto')
let selectedDesignerId = ref<number | undefined>(undefined)
Modal.confirm({
title: '审核子任务组',
width: 600,
content: h('div', { style: 'padding: 10px 0;' }, [
h('p', { style: 'margin-bottom: 16px; color: #666; font-size: 13px;' },
`即将审核通过子任务组"${item.title}",共${item.data.items?.length || 0}个子任务,总积分${item.score}分。`
),
h('p', { style: 'margin-bottom: 12px; font-weight: 500;' }, '请选择接单方式:'),
h(RadioGroup, {
value: assignType.value,
onChange: (e: any) => {
assignType.value = e.target.value
if (e.target.value === 'auto') {
selectedDesignerId.value = undefined
}
}
}, () => [
h(Radio, { value: 'auto', style: { display: 'block', marginBottom: '12px' } }, () => '发布到任务大厅(设计师抢单)'),
h(Radio, { value: 'manual', style: { display: 'block', marginBottom: '12px' } }, () => '指定设计师接单')
]),
h('div', {
style: {
display: assignType.value === 'manual' ? 'block' : 'none',
marginTop: '16px',
marginLeft: '24px'
}
}, [
h('p', { style: 'margin-bottom: 8px; font-size: 13px; color: #666;' }, '选择设计师:'),
h(RadioGroup, {
value: selectedDesignerId.value,
onChange: (e: any) => {
selectedDesignerId.value = e.target.value
},
style: { width: '100%' }
}, () => designersWithWorkload.map(designer => {
const workloadColor = designer.inProgressCount === 0 ? '#52c41a'
: designer.inProgressCount <= 3 ? '#faad14'
: '#f5222d'
return h(Radio, {
value: designer.id,
style: {
display: 'block',
marginBottom: '8px',
padding: '8px',
border: '1px solid #d9d9d9',
borderRadius: '4px'
}
}, () => h('div', { style: 'display: flex; justify-content: space-between; align-items: center;' }, [
h('span', {}, `${designer.realName} (${designer.deptName || '未知部门'})`),
h('span', {
style: {
fontSize: '12px',
color: workloadColor,
fontWeight: 'bold'
}
}, `进行中: ${designer.inProgressCount}个任务`)
]))
}))
])
]),
okText: '确认审核通过',
cancelText: '取消',
onOk: async () => {
if (assignType.value === 'manual' && !selectedDesignerId.value) {
message.warning('请选择设计师')
return Promise.reject()
}
try {
approvalLoading.value = true
// 从 item.data 获取 taskId 和 groupNo
const taskId = item.data?.taskId || item.id
const groupNo = item.data?.groupNo || item.data?.groupId || 1
await taskApi.approveTaskGroup(taskId, {
groupNo,
assignedReceiverId: assignType.value === 'manual' ? selectedDesignerId.value : undefined
})
if (assignType.value === 'manual') {
const designer = designersWithWorkload.find(d => d.id === selectedDesignerId.value)
message.success(`已审核通过并指定给设计师 ${designer?.realName}`)
} else {
message.success('已审核通过,子任务组已发布到任务大厅')
}
// 刷新列表和数量
loadPendingApprovalTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
console.error('审核失败:', error)
message.error(error.msg || error.message || '审核失败')
return Promise.reject(error)
} finally {
approvalLoading.value = false
}
}
})
} catch (error: any) {
console.error('获取设计师列表失败:', error)
message.error('获取设计师列表失败')
}
}
// 驳回子任务组
const handleRejectTaskGroup = async (item: ApprovalItem) => {
let rejectReason = ''
Modal.confirm({
title: '驳回子任务组',
content: h('div', { style: 'padding: 10px 0;' }, [
h('p', { style: 'margin-bottom: 12px; color: #666;' },
`即将驳回子任务组"${item.title}",共${item.data.items?.length || 0}个子任务。`
),
h('p', { style: 'margin-bottom: 10px; color: #666;' }, '请输入驳回原因:'),
h(Textarea, {
rows: 4,
placeholder: '请详细说明驳回原因',
maxlength: 500,
showCount: true,
onChange: (e: any) => {
rejectReason = e.target.value
}
})
]),
okText: '确认驳回',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
if (!rejectReason || !rejectReason.trim()) {
message.warning('请输入驳回原因')
return Promise.reject()
}
try {
approvalLoading.value = true
await taskApi.rejectTaskGroup(item.data.taskId, item.data.groupNo, rejectReason.trim())
message.success('已驳回子任务组')
// 刷新列表和数量
loadPendingApprovalTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
console.error('驳回失败:', error)
message.error(error.msg || error.message || '驳回失败')
} finally {
approvalLoading.value = false
}
}
})
}
// 从待审核任务页面审核子任务组
const handleApproveTaskGroupFromReview = async (task: any) => {
const groupData = task._groupData
if (!groupData) {
message.error('子任务组数据不完整')
return
}
// 将子任务组数据转换为与主任务相同的格式,复用审核弹窗
const taskForApproval = {
id: groupData.taskId,
title: task.title,
taskNo: task.taskNo,
typeDetails: task.typeDetails,
taskTypeId: task.taskTypeId,
currentApproverRole: 'receiving_leader',
// 标记这是子任务组审核
_isTaskGroup: true,
_groupNo: groupData.groupNo,
_groupData: groupData
} as any
// 调用与第一轮相同的审核弹窗
showAssignDesignerModal(taskForApproval)
}
// 从待审核任务页面驳回子任务组
const handleRejectTaskGroupFromReview = async (task: any) => {
const groupData = task._groupData
if (!groupData) {
message.error('子任务组数据不完整')
return
}
let rejectReason = ''
Modal.confirm({
title: '驳回子任务组',
content: h('div', { style: 'padding: 10px 0;' }, [
h('p', { style: 'margin-bottom: 12px; color: #666;' },
`即将驳回子任务组"${task.title} - 第${groupData.groupNo}组",共${groupData.itemCount || 0}个子任务。`
),
h('p', { style: 'margin-bottom: 10px; color: #666;' }, '请输入驳回原因:'),
h(Textarea, {
rows: 4,
placeholder: '请详细说明驳回原因',
maxlength: 500,
showCount: true,
onChange: (e: any) => {
rejectReason = e.target.value
}
})
]),
okText: '确认驳回',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
if (!rejectReason || !rejectReason.trim()) {
message.warning('请输入驳回原因')
return Promise.reject()
}
try {
approvalLoading.value = true
await taskApi.rejectTaskGroup(groupData.taskId, groupData.groupNo, rejectReason.trim())
message.success('已驳回子任务组')
// 刷新列表和数量
loadReviewTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
console.error('驳回失败:', error)
message.error(error.msg || error.message || '驳回失败')
} finally {
approvalLoading.value = false
}
}
})
}
// 已完成任务分页变化
const handleCompletedPageChange = (page: number) => {
completedPagination.page = page
loadCompletedTasks()
}
// 我发起的任务分页
const publishedPagination = reactive({
page: 1,
pageSize: 10
})
// 草稿箱分页
const draftsPagination = reactive({
page: 1,
pageSize: 12
})
// 进行中任务分页
const inProgressPagination = reactive({
page: 1,
pageSize: 12
})
// 待审核任务分页
const reviewPagination = reactive({
page: 1,
pageSize: 12
})
// 加载我发起的任务
const loadPublishedTasks = async (newFilters?: any) => {
// 如果传入了新的筛选值,合并到 publishedFilters 对象
if (newFilters && typeof newFilters === 'object') {
Object.assign(publishedFilters, newFilters)
publishedPagination.page = 1 // 重置分页
}
publishedLoading.value = true
try {
const response = await taskApi.getMyTasks({
role: 'publisher',
status: publishedFilters.status || undefined,
keyword: publishedFilters.keyword || undefined,
taskTypeId: publishedFilters.taskTypeId || undefined,
brandId: publishedFilters.brandId || undefined,
categoryId: publishedFilters.categoryId || undefined,
platformId: publishedFilters.platformId || undefined,
timeRange: publishedFilters.timeRange || undefined,
minScore: publishedFilters.minScore || undefined,
maxScore: publishedFilters.maxScore || undefined,
page: publishedPagination.page,
pageSize: publishedPagination.pageSize
})
if (response.code === 200) {
publishedTaskList.value = response.data.records || []
publishedTotal.value = response.data.total || 0
}
} catch (error) {
console.error('加载我发起的任务失败:', error)
message.error('加载失败')
} finally {
publishedLoading.value = false
}
}
// 我发起的任务分页变化
const handlePublishedPageChange = (page: number) => {
publishedPagination.page = page
loadPublishedTasks()
}
// 加载草稿箱任务
const loadDraftTasks = async (newFilters?: any) => {
// 如果传入了新的筛选值,合并到 draftsFilters 对象
if (newFilters && typeof newFilters === 'object') {
Object.assign(draftsFilters, newFilters)
draftsPagination.page = 1 // 重置分页
}
draftsLoading.value = true
try {
const response = await taskApi.getMyTasks({
role: 'publisher',
status: 'draft', // 只查询草稿状态的任务
keyword: draftsFilters.keyword || undefined,
page: draftsPagination.page,
pageSize: draftsPagination.pageSize,
sortBy: draftsFilters.sortBy
})
if (response.code === 200) {
draftTaskList.value = response.data.records || []
draftsTotal.value = response.data.total || 0
}
} catch (error) {
console.error('加载草稿任务失败:', error)
message.error('加载草稿失败')
} finally {
draftsLoading.value = false
}
}
// 草稿箱分页变化
const handleDraftsPageChange = (page: number) => {
draftsPagination.page = page
loadDraftTasks()
}
// 编辑草稿任务
const handleEditDraftTask = (task: any) => {
router.push({
path: '/tasks/create',
query: { taskId: task.id, mode: 'edit' }
})
}
// 删除草稿任务
const handleDeleteDraft = async (task: any) => {
try {
await new Promise<void>((resolve, reject) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除草稿"${task.title || '未命名任务'}"吗?删除后无法恢复。`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
onOk: () => resolve(),
onCancel: () => reject()
})
})
const response = await taskApi.deleteTask(task.id)
if (response.code === 200) {
message.success('草稿已删除')
loadDraftTasks()
loadAllTabCounts()
} else {
message.error(response.msg || '删除失败')
}
} catch (error: any) {
if (error) {
console.error('删除草稿失败:', error)
message.error(error.msg || '删除失败')
}
}
}
// 重置已完成任务筛选条件
const handleCompletedFiltersReset = () => {
completedFilters.keyword = undefined
completedFilters.sortBy = 'latest'
completedFilters.taskTypeId = undefined
completedFilters.taskSubType = undefined
completedFilters.brandId = undefined
completedFilters.categoryId = undefined
completedFilters.platformId = undefined
completedFilters.timeRange = undefined
completedFilters.minScore = undefined
completedFilters.maxScore = undefined
completedPagination.page = 1
loadCompletedTasks()
}
// 重置我发起的任务筛选条件
const handlePublishedFiltersReset = () => {
publishedFilters.keyword = undefined
publishedFilters.status = ''
publishedFilters.sortBy = 'latest'
publishedFilters.taskTypeId = undefined
publishedFilters.brandId = undefined
publishedFilters.categoryId = undefined
publishedFilters.platformId = undefined
publishedFilters.timeRange = undefined
publishedFilters.minScore = undefined
publishedFilters.maxScore = undefined
publishedPagination.page = 1
loadPublishedTasks()
}
// 加载数据总览
const loadDashboardStats = async () => {
try {
const month = selectedMonth.value?.format('YYYY-MM')
const params: { month?: string; deptId?: number } = {}
if (month) params.month = month
// 从 localStorage 获取用户信息
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 只有部门负责人切换了部门视角时才传递 deptId
// 普通用户不传 deptId,后端会根据 userId 查询个人数据
const isDeptLeader = userInfo.isDepartmentLeader === 1 || userInfo.isDepartmentLeader === true || managedDepartments.value.length > 0
if (isDeptLeader && currentDeptView.value !== undefined) {
// 部门负责人且切换了部门视角
params.deptId = currentDeptView.value
}
// 注意:普通用户不传 deptId,这样后端会按用户个人统计数据
const response = await taskApi.getDashboardStats(Object.keys(params).length > 0 ? params : undefined)
if (response.code === 200) {
const data = response.data
dataCards.value[0].value = data.publishedCount || 0 // 发布的任务数(当月)
dataCards.value[1].value = data.inProgressCount || 0 // 进行中任务
dataCards.value[2].value = data.completedCount || 0 // 已完成任务
dataCards.value[3].value = data.reviewCount || 0 // 待审核任务
dataCards.value[4].value = data.totalScore || 0 // 累计积分
}
} catch (error) {
console.error('加载数据总览失败:', error)
}
}
// 月份选择改变
const handleMonthChange = () => {
loadDashboardStats()
}
// 发起任务
const handleCreateTask = () => {
router.push('/tasks/create')
}
// 一键导出任务大厅数据
const handleExportTaskHall = async () => {
if (!canExportTaskHall.value) {
message.warning('您没有导出权限,只有部门负责人或管理员可以导出')
return
}
try {
exportLoading.value = true
// 构建导出参数(使用当前筛选条件)
const params: ExportTaskHallParams = {
status: filters.status || undefined,
month: selectedMonth.value ? selectedMonth.value.format('YYYY-MM') : undefined,
brandId: filters.brandId,
categoryId: filters.categoryId,
platformId: filters.platformId
}
await exportTaskHall(params)
message.success('导出成功')
} catch (error: any) {
console.error('导出失败:', error)
message.error(error.message || '导出失败')
} finally {
exportLoading.value = false
}
}
// 加载进行中任务
// - 需求部门用户:只看母任务卡片(自己发起的任务)
// - 接单部门用户:只看子任务卡片(自己接单的任务)
// - 管理员:看到所有任务
const loadInProgressTasks = async (newFilters?: any) => {
// 如果传入了新的筛选值,合并到 inProgressFilters 对象
if (newFilters && typeof newFilters === 'object') {
Object.assign(inProgressFilters, newFilters)
inProgressPagination.page = 1 // 重置分页
}
inProgressLoading.value = true
try {
const params: any = {
page: inProgressPagination.page,
pageSize: inProgressPagination.pageSize,
status: inProgressFilters.status || 'in_progress' // 支持状态筛选,默认进行中
}
// 添加筛选条件
if (inProgressFilters.keyword) params.keyword = inProgressFilters.keyword
if (inProgressFilters.sortBy) params.sortBy = inProgressFilters.sortBy
if (inProgressFilters.taskTypeId) params.taskTypeId = inProgressFilters.taskTypeId
if (inProgressFilters.brandId) params.brandId = inProgressFilters.brandId
if (inProgressFilters.categoryId) params.categoryId = inProgressFilters.categoryId
if (inProgressFilters.platformId) params.platformId = inProgressFilters.platformId
if (inProgressFilters.timeRange) params.timeRange = inProgressFilters.timeRange
if (inProgressFilters.minProgress !== undefined) params.minProgress = inProgressFilters.minProgress
if (inProgressFilters.maxProgress !== undefined) params.maxProgress = inProgressFilters.maxProgress
// 如果切换了部门视角,传递指定部门ID
if (currentDeptView.value !== undefined) {
params.deptId = currentDeptView.value
}
// 获取用户部门类型(使用部门视角切换后的有效类型)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const currentDeptType = effectiveDeptType.value // 使用切换后的部门类型
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
const isDeptLeader = userInfo.isDepartmentLeader === 1 || managedDepartments.value.length > 0
console.log('=== loadInProgressTasks 调试信息 ===')
console.log('userInfo.isDepartmentLeader:', userInfo.isDepartmentLeader)
console.log('managedDepartments.value:', managedDepartments.value)
console.log('isDeptLeader:', isDeptLeader)
console.log('isAdmin:', isAdmin)
console.log('currentDeptView.value:', currentDeptView.value)
console.log('userDeptType.value:', userDeptType.value)
let allTasks: any[] = []
let totalCount = 0
// 根据部门类型决定加载哪些任务
// 优先判断是否是部门负责人(包括切换视角和未切换视角的情况)
if (isDeptLeader) {
if (currentDeptView.value !== undefined) {
// 部门负责人切换到其他部门视角
console.log('部门负责人视角切换,部门ID:', currentDeptView.value, '部门类型:', currentDeptType)
if (currentDeptType === 'receiving') {
// 接单部门视角:看该部门成员的子任务 + 主任务
console.log('接单部门负责人切换视角,调用getInProgressTaskItems和getTasks,params:', params)
const [itemResponse, taskResponse] = await Promise.all([
taskApi.getInProgressTaskItems(params),
taskApi.getTasks({
...params,
receiverDeptId: currentDeptView.value,
status: 'in_progress'
})
])
console.log('getInProgressTaskItems响应:', itemResponse)
console.log('getTasks响应:', taskResponse)
// 处理子任务
if (itemResponse.data && itemResponse.data.records) {
const items = itemResponse.data.records.map((item: any) => mapTaskItemToTask(item))
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
console.log('子任务处理后,allTasks长度:', allTasks.length, 'totalCount:', totalCount)
}
// 处理主任务(对于流水线任务,如果主任务状态是in_progress也要显示)
if (taskResponse.data && taskResponse.data.records) {
console.log('getTasks返回的记录数:', taskResponse.data.records.length)
const tasksWithInProgress = taskResponse.data.records.filter((task: any) => {
console.log('检查任务:', task.title, 'isPipeline:', task.isPipeline, 'status:', task.status)
// 对于流水线任务,如果主任务状态是in_progress,也要显示(因为可能有子任务在进行中)
if (task.isPipeline === 1) {
return task.status === 'in_progress'
}
// 非流水线任务,直接显示
return true
})
console.log('过滤后的主任务数:', tasksWithInProgress.length)
const mappedTasks = tasksWithInProgress.map((task: any) => ({
...task,
isSubTask: false
}))
allTasks = [...allTasks, ...mappedTasks]
totalCount += mappedTasks.length
console.log('主任务处理后,allTasks长度:', allTasks.length, 'totalCount:', totalCount)
}
} else {
// 需求部门视角:看该部门发起的任务(通过 deptId 参数)+ 一级需求任务(明细完善人在该部门)
console.log('需求部门视角切换,查询部门ID:', currentDeptView.value)
// 需求部门负责人:使用 excludeStatuses 排除草稿和已完成的任务
const { status, ...paramsWithoutStatus } = params
const taskResponse = await taskApi.getTasks({
...paramsWithoutStatus,
publisherDeptId: currentDeptView.value, // 按发布者部门过滤
includeDetailCompleterDeptIds: String(currentDeptView.value), // 🔥 同时包含一级需求任务(明细完善人在该部门)
excludeStatuses: 'draft,completed,cancelled' // 🔥 后端排除指定状态
})
if (taskResponse.data && taskResponse.data.records) {
// 需求部门负责人:显示所有处理中的任务(后端已过滤)
console.log('需求部门视角切换 - 返回任务数:', taskResponse.data.records.length, '总数:', taskResponse.data.total)
allTasks = taskResponse.data.records.map((task: any) => ({
...task,
isSubTask: false
}))
// 🔥 使用后端返回的总数,确保分页正确
totalCount = taskResponse.data.total || allTasks.length
}
}
} else {
// 部门负责人未切换视角:根据用户自己的部门类型显示
console.log('部门负责人未切换视角,用户部门类型:', userDeptType.value)
if (userDeptType.value === 'receiving') {
// 接单部门负责人:看自己部门所有成员的子任务(不传deptId,后端会自动获取所有负责的部门)
const itemResponse = await taskApi.getInProgressTaskItems(params)
if (itemResponse.data && itemResponse.data.records) {
const items = itemResponse.data.records.map((item: any) => mapTaskItemToTask(item))
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
}
} else {
// 需求部门负责人:看自己管理的所有需求部门发起的任务 + 一级需求任务(指定本部门成员为明细完善人的)
// 获取用户管理的所有需求部门ID(包括自己所属的部门)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const demandDeptIds: number[] = []
console.log('需求部门负责人 - userDeptId:', userDeptId.value, 'userDeptType:', userDeptType.value)
console.log('需求部门负责人 - userInfo.managedDepartments:', userInfo.managedDepartments)
// 添加用户自己的部门(如果是需求部门或一级需求部门)
if (userDeptId.value && (userDeptType.value === 'demand' || userDeptType.value === 'primary_demand')) {
demandDeptIds.push(userDeptId.value)
}
// 添加用户管理的所有需求部门(包括普通需求部门和一级需求部门下的子部门)
if (userInfo.managedDepartments && Array.isArray(userInfo.managedDepartments)) {
userInfo.managedDepartments.forEach((dept: any) => {
console.log('检查管理的部门:', dept.deptId, dept.deptName, 'deptType:', dept.deptType)
// 🔥 支持 demand 和 primary_demand 类型的部门
if ((dept.deptType === 'demand' || dept.deptType === 'primary_demand') && !demandDeptIds.includes(dept.deptId)) {
demandDeptIds.push(dept.deptId)
}
})
}
console.log('需求部门负责人查询的部门ID列表:', demandDeptIds)
// 需求部门负责人:查询所有处理中的任务(排除 draft、completed、cancelled)
// 🔥 同时查询一级需求任务(指定本部门成员为明细完善人的)
const { status: _, ...paramsWithoutStatus } = params
const taskResponse = await taskApi.getTasks({
...paramsWithoutStatus,
publisherDeptIds: demandDeptIds.join(','), // 按多个发布者部门过滤
includeDetailCompleterDeptIds: demandDeptIds.join(','), // 🔥 同时包含一级需求任务(明细完善人在本部门)
excludeStatuses: 'draft,completed,cancelled' // 🔥 后端排除指定状态
})
if (taskResponse.data && taskResponse.data.records) {
// 需求部门负责人:显示所有处理中的任务(后端已过滤)
allTasks = taskResponse.data.records.map((task: any) => ({
...task,
isSubTask: false
}))
// 🔥 使用后端返回的总数,确保分页正确
totalCount = taskResponse.data.total || allTasks.length
}
}
}
} else if (isAdmin) {
// 管理员:看到所有任务(母任务和子任务)
const [taskResponse, itemResponse] = await Promise.all([
taskApi.getTasks(params),
taskApi.getInProgressTaskItems(params)
])
// 处理普通任务
if (taskResponse.data && taskResponse.data.records) {
const tasks = taskResponse.data.records.map((task: any) => ({
...task,
isSubTask: false
}))
allTasks = [...allTasks, ...tasks]
totalCount += taskResponse.data.total || 0
}
// 处理子任务
if (itemResponse.data && itemResponse.data.records) {
const items = itemResponse.data.records.map((item: any) => mapTaskItemToTask(item))
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
}
} else if (currentDeptType === 'demand') {
// 需求部门普通用户:看自己发起的任务 + 自己作为明细完善人的一级需求任务
const [publisherResponse, detailCompleterResponse] = await Promise.all([
taskApi.getMyTasks({
role: 'publisher',
page: inProgressPagination.page,
pageSize: inProgressPagination.pageSize,
keyword: inProgressFilters.keyword
}),
// 🔥 查询自己作为明细完善人的一级需求任务
taskApi.getMyTasks({
role: 'detail_completer',
page: 1,
pageSize: 100 // 获取所有
})
])
// 处理我发起的任务
if (publisherResponse.code === 200 && publisherResponse.data && publisherResponse.data.records) {
const tasksWithInProgress = publisherResponse.data.records.filter((task: any) => {
if (task.isPipeline === 1) {
return (task.subTaskInProgress || 0) > 0 || task.status === 'in_progress'
}
return task.status === 'in_progress'
})
allTasks = tasksWithInProgress.map((task: any) => ({
...task,
isSubTask: false
}))
}
// 🔥 处理我作为明细完善人的一级需求任务(排除已完成和草稿状态)
if (detailCompleterResponse.code === 200 && detailCompleterResponse.data && detailCompleterResponse.data.records) {
const detailCompleterTasks = detailCompleterResponse.data.records.filter((task: any) => {
// 排除草稿和已完成状态
return task.status !== 'draft' && task.status !== 'completed' && task.status !== 'cancelled'
}).map((task: any) => ({
...task,
isSubTask: false
}))
// 合并并去重(避免同一任务重复显示)
const existingTaskIds = new Set(allTasks.map((t: any) => t.id))
detailCompleterTasks.forEach((task: any) => {
if (!existingTaskIds.has(task.id)) {
allTasks.push(task)
}
})
console.log('需求部门成员 - 发起的任务:', publisherResponse.data.records.length, '明细完善人任务:', detailCompleterTasks.length, '合并后:', allTasks.length)
}
totalCount = allTasks.length
} else {
// 接单部门用户(设计师):看子任务卡片 + 直接分配给自己的普通任务
const [itemResponse, taskResponse] = await Promise.all([
taskApi.getInProgressTaskItems(params),
// 获取直接分配给当前用户的普通任务(使用正确的 /my 接口)
taskApi.getMyTasks({
role: 'receiver',
status: 'in_progress',
page: inProgressPagination.page,
pageSize: inProgressPagination.pageSize
})
])
// 处理子任务
if (itemResponse.data && itemResponse.data.records) {
// 调试:打印原始 API 数据中的 subTasks
console.log('=== API 原始数据调试 ===')
if (itemResponse.data.records[0] && itemResponse.data.records[0].subTasks) {
console.log('第一条记录的 subTasks[0]:', JSON.stringify(itemResponse.data.records[0].subTasks[0]))
console.log('subTasks[0].deadline:', itemResponse.data.records[0].subTasks[0].deadline)
}
const items = itemResponse.data.records.map((item: any) => {
const mapped = mapTaskItemToTask(item)
// 调试:打印映射后的 subTasks
if (mapped.subTasks && mapped.subTasks[0]) {
console.log('映射后 subTasks[0].deadline:', mapped.subTasks[0].deadline)
}
return mapped
})
console.log('=== API 原始数据调试结束 ===')
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
}
// 处理直接分配的普通任务(排除流水线任务,因为流水线任务由子任务显示)
if (taskResponse.data && taskResponse.data.records) {
const normalTasks = taskResponse.data.records
.filter((task: any) => task.isPipeline !== 1) // 排除流水线任务
.map((task: any) => ({
...task,
isSubTask: false
}))
allTasks = [...allTasks, ...normalTasks]
totalCount += normalTasks.length
}
}
// 按更新时间排序
allTasks.sort((a, b) => {
const timeA = new Date(a.updateTime || a.startTime || 0).getTime()
const timeB = new Date(b.updateTime || b.startTime || 0).getTime()
return timeB - timeA // 降序
})
inProgressTaskList.value = allTasks
inProgressTotal.value = totalCount
} catch (error) {
console.error('加载进行中任务失败:', error)
message.error('加载进行中任务失败')
} finally {
inProgressLoading.value = false
}
}
// 将子任务映射为任务格式(供显示用)
const mapTaskItemToTask = (item: any) => {
// 调试:输出原始 item 中 subTasks 的 deadline 信息
if (item.subTasks && item.subTasks.length > 0) {
console.log('=== mapTaskItemToTask 调试 ===')
console.log('item.id:', item.id)
console.log('item.subTasks:', JSON.stringify(item.subTasks.map((st: any) => ({
id: st.id,
status: st.status,
deadline: st.deadline
}))))
}
return {
...item,
id: item.id,
itemId: item.id,
itemNo: item.itemNo,
title: item.mainTaskTitle || item.taskTitle || item.title,
subTaskNo: item.itemNo,
isSubTask: true,
taskId: item.taskId,
groupNo: item.groupNo,
taskTypeId: item.taskTypeId,
taskTypeName: item.taskTypeName,
typeDetails: item.typeDetails, // 任务类型明细(JSON字符串)
score: item.totalScore || item.score,
estimatedHours: item.estimatedHours,
quantity: item.quantity || 1,
previewImage: item.previewImage,
prevDownloadUrl: item.prevDownloadUrl,
description: item.description || item.taskDescription,
progress: item.progress,
status: 'in_progress',
receiverName: item.receiverName, // 接单人姓名(部门负责人查看时显示)
deadline: item.deadline, // 子任务截止时间(接单时计算)
startTime: item.createTime, // 使用创建时间作为开始时间
isPipeline: item.isPipeline, // 是否流水线任务(从主任务获取)
subTasks: item.subTasks, // 同一主任务的所有子任务列表
subTaskTotal: item.subTaskTotal || (item.subTasks ? item.subTasks.length : 0), // 子任务总数
// 🔥 一级需求任务相关字段
isPrimaryTask: item.isPrimaryTask, // 是否一级需求任务
primaryPublisherName: item.primaryPublisherName, // 一级需求发起人姓名
primaryPublisherDeptName: item.primaryPublisherDeptName, // 一级需求发起人部门名称
detailCompleterName: item.detailCompleterName, // 明细完善人姓名
// 🔥 主任务备注信息(需求部门审核时填写)
remark: item.remark
}
}
// 加载待审核任务(所有处于审批流程中的任务 + 部门负责人的子任务组)
const loadReviewTasks = async (newFilters?: any) => {
// 如果传入了新的筛选值,合并到 reviewFilters 对象
if (newFilters && typeof newFilters === 'object') {
Object.assign(reviewFilters, newFilters)
reviewPagination.page = 1 // 重置分页
}
reviewLoading.value = true
try {
const params: any = {
page: reviewPagination.page,
pageSize: reviewPagination.pageSize
}
// 添加筛选条件
if (reviewFilters.status) params.status = reviewFilters.status
if (reviewFilters.taskTypeId) params.taskTypeId = reviewFilters.taskTypeId
// 并行加载:主任务 + 子任务组(如果是部门负责人)
const promises: Promise<any>[] = [
taskApi.getAllPendingApprovalTasks(params)
]
// 如果是部门负责人,还要加载待审核的子任务组
if (isDepartmentLeader.value) {
promises.push(taskApi.getPendingApprovalTaskGroups())
}
const [tasksResponse, groupsResponse] = await Promise.all(promises)
let allItems: any[] = []
// 处理主任务
const tasks = tasksResponse.data?.records || []
for (const task of tasks) {
allItems.push({
...task,
_itemType: 'task' // 标记类型
})
}
// 处理子任务组(如果有)- 使用与主任务相同的数据结构
if (groupsResponse && groupsResponse.code === 200 && groupsResponse.data) {
for (const group of groupsResponse.data) {
allItems.push({
id: `group-${group.taskId}-${group.groupNo}`,
title: group.taskTitle || '子任务组',
taskNo: group.taskNo || '',
score: group.totalScore || 0,
totalScore: group.totalScore || 0,
createTime: group.createTime,
// 继承主任务的完整信息
brandId: group.brandId,
brandName: group.brandName,
categoryId: group.categoryId,
categoryName: group.categoryName,
platformId: group.platformId,
platformName: group.platformName,
spuInfo: group.spuInfo,
quantity: group.itemCount || 1, // 子任务数量作为数量显示
previewImage: group.previewImage,
attachmentUrl: group.attachmentUrl,
remark: group.remark,
typeDetails: group.typeDetails,
taskTypeId: group.taskTypeId,
taskTypeName: group.taskTypeName,
publisherId: group.publisherId,
publisherName: group.publisherName,
publisherDeptName: group.publisherDeptName,
// 一级需求部门发起人信息
isPrimaryTask: group.isPrimaryTask,
primaryPublisherId: group.primaryPublisherId,
primaryPublisherName: group.primaryPublisherName,
primaryPublisherDeptId: group.primaryPublisherDeptId,
primaryPublisherDeptName: group.primaryPublisherDeptName,
// 明细完善人信息
detailCompleterId: group.detailCompleterId,
detailCompleterName: group.detailCompleterName,
// 审核人信息(流程阶段显示)
currentApproverName: group.currentApproverName,
currentApproverRole: group.currentApproverRole,
approvalStatus: group.approvalStatus,
// 前置任务的素材下载链接
prevDownloadUrls: group.prevDownloadUrls || [],
// 标记类型
_itemType: 'taskGroup',
_groupData: group // 保存原始子任务组数据
})
}
}
// 前端筛选(因为新接口只支持taskTypeId筛选)
if (reviewFilters.keyword) {
const keyword = reviewFilters.keyword.trim().toLowerCase()
allItems = allItems.filter((item: any) =>
item.title?.toLowerCase().includes(keyword) ||
item.taskNo?.toLowerCase().includes(keyword) ||
item.spuInfo?.toLowerCase().includes(keyword) ||
item.publisherName?.toLowerCase().includes(keyword)
)
}
if (reviewFilters.brandId) {
allItems = allItems.filter((item: any) => item._itemType === 'taskGroup' || item.brandId === reviewFilters.brandId)
}
if (reviewFilters.categoryId) {
allItems = allItems.filter((item: any) => item._itemType === 'taskGroup' || item.categoryId === reviewFilters.categoryId)
}
if (reviewFilters.platformId) {
allItems = allItems.filter((item: any) => item._itemType === 'taskGroup' || item.platformId === reviewFilters.platformId)
}
if (reviewFilters.minScore !== undefined) {
allItems = allItems.filter((item: any) => (item.score || 0) >= reviewFilters.minScore!)
}
if (reviewFilters.maxScore !== undefined) {
allItems = allItems.filter((item: any) => (item.score || 0) <= reviewFilters.maxScore!)
}
if (reviewFilters.timeRange) {
const now = dayjs()
let startTime: dayjs.Dayjs
if (reviewFilters.timeRange === 'today') {
startTime = now.startOf('day')
} else if (reviewFilters.timeRange === 'week') {
startTime = now.subtract(7, 'day')
} else if (reviewFilters.timeRange === 'month') {
startTime = now.subtract(30, 'day')
} else {
startTime = now.subtract(1, 'year')
}
allItems = allItems.filter((item: any) =>
item.createTime && dayjs(item.createTime).isAfter(startTime)
)
}
// 排序
if (reviewFilters.sortBy === 'score') {
allItems.sort((a: any, b: any) => (b.score || 0) - (a.score || 0))
} else {
allItems.sort((a: any, b: any) =>
dayjs(b.createTime || 0).valueOf() - dayjs(a.createTime || 0).valueOf()
)
}
reviewTaskList.value = allItems
reviewTotal.value = allItems.length
} catch (error) {
console.error('加载待审核任务失败:', error)
message.error('加载待审核任务失败')
} finally {
reviewLoading.value = false
}
}
// 进行中任务分页变化
const handleInProgressPageChange = (page: number) => {
inProgressPagination.page = page
loadInProgressTasks()
}
// 待审核任务分页变化
const handleReviewPageChange = (page: number) => {
reviewPagination.page = page
loadReviewTasks()
}
// 重置进行中任务筛选条件
const handleInProgressFiltersReset = () => {
inProgressFilters.keyword = undefined
inProgressFilters.sortBy = 'latest'
inProgressFilters.taskTypeId = undefined
inProgressFilters.brandId = undefined
inProgressFilters.categoryId = undefined
inProgressFilters.platformId = undefined
inProgressFilters.timeRange = undefined
inProgressFilters.minProgress = undefined
inProgressFilters.maxProgress = undefined
inProgressPagination.page = 1
loadInProgressTasks()
}
// 重置待审核任务筛选条件
const handleReviewFiltersReset = () => {
reviewFilters.keyword = undefined
reviewFilters.sortBy = 'latest'
reviewFilters.taskTypeId = undefined
reviewFilters.brandId = undefined
reviewFilters.categoryId = undefined
reviewFilters.platformId = undefined
reviewFilters.timeRange = undefined
reviewFilters.minScore = undefined
reviewFilters.maxScore = undefined
reviewPagination.page = 1
loadReviewTasks()
}
// 判断是否可以更新进度(接单人)
const canUpdateProgress = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
return task.receiverId === userInfo.id
}
// 判断是否可以完结任务(接单人)
const canCompleteTask = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
return task.receiverId === userInfo.id && (task.progress || 0) >= 100
}
// 判断是否是任务发布人(包括一级需求发起人)
const isTaskPublisher = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 普通发起人
if (task.publisherId === userInfo.id) return true
// 一级需求发起人
if ((task as any).primaryPublisherId === userInfo.id) return true
// 明细完善人
if ((task as any).detailCompleterId === userInfo.id) return true
return false
}
// 显示评分弹窗
const showRatingModal = (task: TaskOrSubTask) => {
ratingTask.value = task
ratingValue.value = 5
ratingComment.value = ''
ratingModalVisible.value = true
}
// 提交评分
const submitRating = async () => {
if (!ratingTask.value) return
ratingLoading.value = true
try {
await taskApi.rateTask(ratingTask.value.id!, ratingValue.value, ratingComment.value || undefined)
message.success('评分成功!')
ratingModalVisible.value = false
// 更新当前任务的评分
if (selectedTask.value && selectedTask.value.id === ratingTask.value.id) {
selectedTask.value.rating = ratingValue.value
}
// 刷新已完成任务列表
if (activeTab.value === 'completed') {
loadCompletedTasks()
}
} catch (error: any) {
console.error('评分失败:', error)
message.error(error.msg || error.message || '评分失败')
} finally {
ratingLoading.value = false
}
}
// 判断是否可以审核任务
// 对于审批流程中的任务:只有当前审核人可以审核
// 对于其他情况:只有部门负责人可以审核
const canApproveTask = (task: Task) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const currentUserId = userInfo.id
// 如果是审批流程中的任务(一级需求审核、需求部门审核或接单部门审核)
if (task.approvalStatus === 'pending_primary_approval' ||
task.approvalStatus === 'pending_demand_approval' ||
task.approvalStatus === 'pending_receiving_approval') {
// 只有当前审核人可以审核
return task.currentApproverId === currentUserId
}
// 其他情况(如任务完成后等待审核):只有部门负责人可以审核
return userInfo.roleId === 2 || userInfo.isDepartmentLeader === 1 ||
userInfo.role === 'dept_leader' || userInfo.role === 'admin'
}
// 判断是否可以驳回任务
const canRejectTask = (task: Task) => {
return canApproveTask(task)
}
// 判断是否可以编辑完善任务明细(一级需求部门任务)
const canEditDetailTask = (task: Task) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const currentUserId = userInfo.id
// 只有待完善明细状态的任务,且当前用户是指定的完善人
if (task.approvalStatus === 'pending_detail_completion') {
return task.currentApproverId === currentUserId || task.detailCompleterId === currentUserId
}
return false
}
// 处理编辑完善任务明细(跳转到发布任务页面)
const handleEditDetailTask = (task: Task) => {
// 将任务信息存储到 sessionStorage,供发布任务页面使用
// 使用 any 类型断言来访问可能不在类型定义中但实际存在的属性
const taskData = task as any
const editTaskData = {
taskId: task.id,
isPrimaryTaskEdit: true, // 标记为一级需求任务的明细编辑
brandId: task.brandId,
brandName: task.brandName,
platformId: task.platformId,
platformName: task.platformName,
categoryId: task.categoryId,
categoryName: task.categoryName,
title: task.title,
description: taskData.description || '',
score: task.score,
taskTypeId: task.taskTypeId,
taskTypeIds: taskData.taskTypeIds || [],
demandTypeId: taskData.demandTypeId,
spuInfo: task.spuInfo,
deadline: task.deadline,
timeLimit: taskData.timeLimit,
// 一级需求部门信息
primaryPublisherId: task.primaryPublisherId,
primaryPublisherDeptId: task.primaryPublisherDeptId,
isPrimaryTask: task.isPrimaryTask
}
sessionStorage.setItem('editPrimaryTaskData', JSON.stringify(editTaskData))
// 跳转到发布任务页面
router.push('/tasks/create?editPrimaryTask=true')
}
// 更新进度(支持普通任务和子任务)
const handleUpdateProgress = (task: TaskOrSubTask) => {
let newProgress = task.progress || 0
const isSubTask = task.isSubTask
const taskType = isSubTask ? '子任务' : '任务'
Modal.confirm({
title: `更新${taskType}进度`,
content: h('div', [
h('div', { style: 'margin-bottom: 12px;' }, `${taskType}:${task.title}`),
h('div', { style: 'margin-bottom: 12px;' }, `当前进度:${task.progress || 0}%`),
h(InputNumber, {
min: 0,
max: 100,
defaultValue: task.progress || 0,
style: 'width: 100%',
placeholder: '输入新的进度(0-100)',
onChange: (value: number) => { newProgress = value }
})
]),
onOk: async () => {
try {
if (isSubTask) {
// 更新子任务进度
await taskApi.updateTaskItemProgress(task.itemId || task.id!, newProgress)
} else {
// 更新普通任务进度
await taskApi.updateTaskProgress(task.id!, newProgress)
}
message.success('进度更新成功')
loadInProgressTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.message || '进度更新失败')
}
}
})
}
// 复制任务编号
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)
}
}
// 完结任务(支持普通任务和子任务)
// 编辑被驳回的任务
// 下载素材
const handleDownloadAssets = (task: Task) => {
const downloadUrl = task.downloadUrl || task.deliveryPath
if (downloadUrl) {
// 如果是http链接,直接打开
if (downloadUrl.startsWith('http')) {
window.open(downloadUrl, '_blank')
} else {
// 复制到剪贴板
navigator.clipboard.writeText(downloadUrl).then(() => {
message.success('素材路径已复制到剪贴板')
}).catch(() => {
message.info(`素材路径: ${downloadUrl}`)
})
}
} else {
message.warning('暂无可下载的素材')
}
}
// 下载上一环节素材(流水线任务)
const handleDownloadPrevAssets = (task: TaskOrSubTask) => {
const prevDownloadUrl = task.prevDownloadUrl
if (prevDownloadUrl) {
// 如果包含多个链接(逗号分隔),显示弹窗让用户选择
const urls = prevDownloadUrl.split(',').filter(url => url.trim())
if (urls.length === 1) {
// 只有一个链接,直接打开
if (urls[0].startsWith('http')) {
window.open(urls[0], '_blank')
} else {
navigator.clipboard.writeText(urls[0]).then(() => {
message.success('素材路径已复制到剪贴板')
}).catch(() => {
message.info(`素材路径: ${urls[0]}`)
})
}
} else {
// 多个链接,显示列表让用户选择
Modal.info({
title: '上一环节素材下载',
content: h('div', [
h('p', { style: 'margin-bottom: 12px;' }, `共 ${urls.length} 个素材文件:`),
...urls.map((url, index) =>
h('div', { style: 'margin-bottom: 8px;' }, [
h('a', {
href: url,
target: '_blank',
style: 'color: #1890ff; word-break: break-all;'
}, `素材 ${index + 1}: ${url.length > 50 ? url.substring(0, 50) + '...' : url}`)
])
)
]),
width: 600
})
}
} else {
message.warning('暂无上一环节的素材')
}
}
// 判断任务是否可以撤回
const canWithdrawTask = (task: Task) => {
// 已被驳回或草稿状态的任务不显示撤回按钮(它们有自己的编辑按钮)
if (['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')) {
return false
}
// 已完成、已取消的任务不能撤回
if (['completed', 'cancelled'].includes(task.status)) {
return false
}
// 已被抢单的任务不能撤回(receiverId 不为空)
if (task.receiverId) {
return false
}
// 进行中的任务不能撤回
if (task.status === 'in_progress') {
return false
}
// 待接单或待审核的任务可以撤回
if (task.status === 'pending') {
return true
}
// 审批状态中包含 pending 的可以撤回
if (task.approvalStatus?.includes('pending')) {
return true
}
return false
}
// 撤回任务
const handleWithdrawTask = async (task: Task) => {
if (!task.id) return
Modal.confirm({
title: '撤回任务',
content: h('div', [
h('p', `确认撤回任务【${task.title}】?`),
h('p', { style: 'color: #999; font-size: 13px; margin-top: 8px;' }, '撤回后任务将变为草稿状态,您可以进行编辑后重新发布。')
]),
okText: '确认撤回',
cancelText: '取消',
async onOk() {
try {
withdrawingTaskId.value = task.id!
await taskApi.withdrawTask(task.id!)
message.success('任务已撤回,正在跳转到编辑页面...')
// 撤回成功后跳转到编辑页面
router.push({
path: '/tasks/create',
query: {
editId: task.id,
mode: 'edit'
}
})
} catch (error: any) {
console.error('撤回任务失败:', error)
message.error(error.response?.data?.message || error.message || '撤回任务失败')
} finally {
withdrawingTaskId.value = null
}
}
})
}
const handleEditRejectedTask = (task: Task) => {
// 跳转到创建任务页面,并传入任务ID作为编辑标识
router.push({
path: '/tasks/create',
query: {
editId: task.id,
mode: 'edit'
}
})
}
const handleEditAvailableTaskDetail = (task: TaskOrSubTask) => {
if (!task?.id) {
message.error('任务ID不存在,无法编辑')
return
}
detailDrawerVisible.value = false
router.push({
path: '/tasks/create',
query: {
editId: task.id,
mode: 'edit'
}
})
}
const handleCompleteTask = (task: TaskOrSubTask) => {
const isSubTask = task.isSubTask
const taskType = isSubTask ? '子任务' : '任务'
const taskNo = isSubTask ? (task.itemNo || task.taskNo) : task.taskNo
Modal.confirm({
title: `完结${taskType}`,
content: h('div', [
h('div', { style: 'margin-bottom: 12px;' }, `确认完结${taskType}:${task.title}`),
h('div', { style: 'color: #666; font-size: 13px;' }, [
h('p', { style: 'margin-bottom: 8px;' }, '系统将自动从素材平台获取您提交的素材。'),
h('p', { style: 'margin: 0;' }, `任务编号:${taskNo || '--'}`)
])
]),
okText: '确认完结',
cancelText: '取消',
onOk: async () => {
try {
if (isSubTask) {
// 完结子任务(后端会自动调用第三方API验证)
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath: '' })
message.success('子任务已完成')
} else {
// 完结普通任务
await taskApi.completeTask(task.id!, { deliveryPath: '' })
message.success('任务已提交审核')
}
loadInProgressTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
// 如果是素材未提交的错误,显示特定提示
const errorMsg = error.message || error.msg || '任务完结失败'
if (errorMsg.includes('素材未提交') || errorMsg.includes('3D.YYDD.AI')) {
Modal.warning({
title: '素材未提交',
content: h('div', [
h('p', '请先前往素材平台提交任务素材后再完结任务。'),
h('p', { style: 'margin-top: 8px;' }, [
'任务编号:',
h('strong', taskNo || '--')
]),
h('a', {
href: 'https://3d.yydd.ai',
target: '_blank',
style: 'color: #1890ff; margin-top: 12px; display: inline-block;'
}, '前往素材平台 →')
])
})
} else {
message.error(errorMsg)
}
}
}
})
}
// 审核通过(使用两级审核流程)
const handleApproveTask = (task: Task) => {
// 如果是接单部门负责人审核,需要提供选择指派设计师的功能
// 注意:一级需求部门任务在接单部门审核时,也应该使用完整的审核弹窗(编辑任务类型、选择接单方式等)
if (task.currentApproverRole === 'receiving_leader') {
handleApproveTwoLevelTask(task)
return
}
// 如果是一级需求部门任务审核(需求部门负责人审核一级任务),需要指定成员完善明细
// 注意:这个分支只在需求部门负责人审核一级任务时使用,不是接单部门审核
if (task.approvalStatus === 'pending_primary_approval' && task.currentApproverRole === 'demand_leader') {
handleApprovePrimaryTask(task)
return
}
// 需求部门负责人审核,直接通过即可
Modal.confirm({
title: '审核通过',
content: `确认通过任务审核:${task.title}?`,
onOk: async () => {
try {
const approvalData: TaskApprovalDTO = {
comment: '审核通过'
}
await taskApi.approveDemandLevel(task.id!, approvalData)
message.success('需求部门审核通过')
loadReviewTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.msg || error.message || '审核失败')
}
}
})
}
// 处理一级需求部门任务审核(指定成员完善明细)
const handleApprovePrimaryTask = async (task: Task) => {
try {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 先检查是否开启了跨部门分配功能
let crossDeptAssignEnabled = false
try {
const configResponse = await getSystemConfig('primary_dept_cross_dept_assign')
// API 返回的 data 直接是字符串 "true" 或 "false"
crossDeptAssignEnabled = configResponse.data === 'true' || configResponse.data === true
console.log('跨部门分配功能状态:', crossDeptAssignEnabled, '原始值:', configResponse.data)
} catch (e) {
console.log('获取跨部门分配配置失败,使用默认值(关闭)')
}
if (crossDeptAssignEnabled) {
// 开启跨部门分配:获取所有需求部门的成员
console.log('跨部门分配已开启,获取所有需求部门成员')
const allMembers = await userApi.getAllDemandDeptMembers()
deptMembersList.value = (allMembers || []).map(u => ({
id: u.id!,
realName: u.realName || u.username,
username: u.username,
deptName: u.deptName || ''
}))
console.log(`获取到${deptMembersList.value.length}个需求部门成员(跨部门)`)
} else {
// 未开启跨部门分配:只获取当前负责人部门的成员
// 优先使用用户管理的需求部门(dept_type = 'demand'),而不是接单部门
let deptId = userInfo.deptId
let selectedDeptName = ''
// 从管理的部门中找需求部门(demand类型)
if (managedDepartments.value.length > 0) {
// 优先找需求部门
const demandDept = managedDepartments.value.find(d => d.deptType === 'demand')
if (demandDept) {
deptId = demandDept.deptId
selectedDeptName = demandDept.deptName
console.log(`使用用户管理的需求部门: ${selectedDeptName} (ID: ${deptId})`)
} else {
// 没有需求部门,使用第一个部门
deptId = managedDepartments.value[0].deptId
selectedDeptName = managedDepartments.value[0].deptName
console.log(`没有找到需求部门,使用第一个管理的部门: ${selectedDeptName} (ID: ${deptId})`)
}
} else if (userInfo.managedDepartments && userInfo.managedDepartments.length > 0) {
// 优先找需求部门
const demandDept = userInfo.managedDepartments.find((d: any) => d.deptType === 'demand')
if (demandDept) {
deptId = demandDept.deptId
selectedDeptName = demandDept.deptName
console.log(`使用userInfo中管理的需求部门: ${selectedDeptName} (ID: ${deptId})`)
} else {
deptId = userInfo.managedDepartments[0].deptId
selectedDeptName = userInfo.managedDepartments[0].deptName
console.log(`没有找到需求部门,使用userInfo中第一个管理的部门: ${selectedDeptName} (ID: ${deptId})`)
}
}
if (!deptId) {
message.error('无法获取部门信息')
return
}
console.log(`查询部门 ${deptId} (${selectedDeptName}) 的成员(包含子部门)`)
// 获取部门成员(包含所有子部门)
const response = await userApi.getUserList(1, 1000, undefined, deptId, 1, true)
deptMembersList.value = (response.records || []).map(u => ({
id: u.id!,
realName: u.realName || u.username,
username: u.username,
deptName: u.deptName || ''
}))
console.log(`获取到${deptMembersList.value.length}个部门成员(含子部门)`)
}
primaryApproveTaskData.value = task
primaryApproveCompleterId.value = undefined
primaryApproveComment.value = ''
primaryApproveModalVisible.value = true
} catch (error: any) {
message.error('获取部门成员失败:' + (error.msg || error.message))
}
}
// 确认一级需求审核
const handlePrimaryApproveConfirm = async () => {
if (!primaryApproveTaskData.value) return
if (!primaryApproveCompleterId.value) {
message.error('请选择完善明细的成员')
return
}
try {
primaryApprovalLoading.value = true
await taskApi.approvePrimaryTask(
primaryApproveTaskData.value.id!,
primaryApproveCompleterId.value,
primaryApproveComment.value || '审核通过,请完善任务明细'
)
message.success('审核通过,已指派成员完善明细')
primaryApproveModalVisible.value = false
loadReviewTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.msg || error.message || '审核失败')
} finally {
primaryApprovalLoading.value = false
}
}
// 驳回任务(使用两级审核流程)
const handleRejectTask = (task: Task) => {
let reason = ''
Modal.confirm({
title: '驳回任务',
content: h('div', [
h('div', { style: 'margin-bottom: 12px;' }, `驳回任务:${task.title}`),
h('div', { style: 'margin-bottom: 12px;' }, '驳回原因:'),
h(Textarea, {
rows: 4,
placeholder: '请输入驳回原因',
style: 'width: 100%',
onChange: (e: any) => { reason = e.target.value }
})
]),
onOk: async () => {
if (!reason.trim()) {
message.error('请输入驳回原因')
return Promise.reject()
}
try {
// 优先检查是否为一级需求部门任务(pending_primary_approval状态)
if (task.approvalStatus === 'pending_primary_approval') {
await taskApi.rejectPrimaryTask(task.id!, reason)
message.success('一级需求任务已驳回')
} else if (task.approvalStatus === 'pending_detail_completion' || task.currentApproverRole === 'detail_completer') {
// 待完善明细状态的任务驳回(退回给一级需求发起人)
await taskApi.rejectPrimaryTask(task.id!, reason)
message.success('任务已驳回,已退回给一级需求发起人')
} else if (task.currentApproverRole === 'demand_leader') {
// 需求部门负责人驳回(pending_demand_approval状态)
await taskApi.rejectByDemandLeader(task.id!, reason)
message.success('需求部门已驳回')
} else if (task.currentApproverRole === 'receiving_leader') {
// 接单部门负责人驳回(pending_receiving_approval状态)
await taskApi.rejectByReceivingLeader(task.id!, reason)
message.success('接单部门已驳回')
} else {
message.error('无法确定审核角色')
return Promise.reject()
}
loadReviewTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.msg || error.message || '驳回失败')
}
}
})
}
// 查看任务详情(进行中/待审核)
const handleViewTaskDetail = async (task: Task) => {
selectedTask.value = task
detailDrawerVisible.value = true
// 调试:输出任务对象信息
console.log('=== 任务详情调试信息 ===')
console.log('任务对象:', task)
console.log('任务状态:', task.status)
console.log('deadline字段:', task.deadline)
console.log('startTime字段:', task.startTime)
console.log('isPipeline:', task.isPipeline)
console.log('subTasks:', task.subTasks)
// 输出每个子任务的deadline
if (task.subTasks && task.subTasks.length > 0) {
task.subTasks.forEach((st: any, i: number) => {
console.log(`子任务${i+1} - id:${st.id}, status:${st.status}, deadline:${st.deadline}, receiverName:${st.receiverName}`)
})
}
console.log('estimatedHours:', task.estimatedHours)
console.log('getTaskDeadline结果:', getTaskDeadline(task))
console.log('====================')
// 已完成任务加载商品ID列表
if (task.status === 'completed' && task.id) {
await loadTaskProducts(task.id)
} else {
taskProducts.value = []
}
}
// 任务点击
const handleTaskClick = async (task: TaskOrSubTask) => {
selectedTask.value = task
detailDrawerVisible.value = true
// 调试:输出任务对象信息
console.log('=== 任务详情调试信息 ===')
console.log('任务对象:', task)
console.log('任务状态:', task.status)
console.log('deadline字段:', task.deadline)
console.log('startTime字段:', (task as any).startTime)
console.log('isPipeline:', (task as any).isPipeline)
console.log('subTasks:', (task as any).subTasks)
// 输出每个子任务的deadline
if ((task as any).subTasks && (task as any).subTasks.length > 0) {
(task as any).subTasks.forEach((st: any, i: number) => {
console.log(`子任务${i+1} - id:${st.id}, status:${st.status}, deadline:${st.deadline}, receiverName:${st.receiverName}`)
})
}
console.log('estimatedHours:', task.estimatedHours)
console.log('getTaskDeadline结果:', getTaskDeadline(task))
console.log('====================')
// 已完成任务加载商品ID列表
if (task.status === 'completed' && task.id) {
await loadTaskProducts(task.id)
} else {
taskProducts.value = []
}
}
// 判断用户是否可以抢单
const canGrabTask = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const currentDeptId = effectiveDeptId.value // 使用切换后的部门ID
const currentDeptType = effectiveDeptType.value // 使用切换后的部门类型
// 管理员拥有所有权限,可以直接抢单
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) {
return { canGrab: true, reason: '' }
}
// 检查1:没有部门不能抢单
if (!currentDeptId) {
return { canGrab: false, reason: '您未分配部门,无法接单' }
}
// 检查2:需求部门不能抢单
if (currentDeptType === 'demand') {
return { canGrab: false, reason: '需求部门成员不能接单' }
}
// 检查3:部门负责人不能抢单(如果是切换到其他部门视角则允许)
if (currentDeptView.value === undefined && (userInfo.isDepartmentLeader === 1 || userInfo.isDepartmentLeader === true)) {
return { canGrab: false, reason: '部门负责人不能接单' }
}
// 检查4:验证部门是否允许接该需求类型(如果任务有需求类型限制)
const taskTypeId = task.isSubTask ? task.taskTypeId : (task as any).taskTypeId
if (taskTypeId) {
// 从配置store中获取任务类型详情
const demandType = configStore.demandTypes.find(t => t.id === taskTypeId)
if (demandType && demandType.allowedDeptIds) {
const allowedIds = demandType.allowedDeptIds.split(',')
// 检查当前有效部门ID是否在允许列表中
if (currentDeptId && !allowedIds.includes(String(currentDeptId))) {
return { canGrab: false, reason: '您的部门无权接收此类型的任务' }
}
}
}
// 检查5:验证任务是否在排队中(排队中的任务不能抢单)
if (task.canGrab === false && task.unlockStatus === 'queuing') {
const position = task.queuePosition || 0
return { canGrab: false, reason: `任务排队中,当前位置:第${position}位` }
}
return { canGrab: true, reason: '' }
}
// 抢单(支持普通任务和子任务)
const handleGrab = async (task: TaskOrSubTask) => {
const isSubTask = task.isSubTask
const taskType = isSubTask ? '子任务' : '任务'
const taskNo = isSubTask ? task.subTaskNo : task.taskNo
// 先检查权限
const { canGrab, reason } = canGrabTask(task)
if (!canGrab) {
message.warning(reason)
return
}
Modal.confirm({
title: `确认抢单${taskType}`,
content: `确定要抢单${taskType}"${task.title}"吗?${taskNo ? `(编号:${taskNo})` : ''}`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
let response
if (isSubTask) {
// 抢子任务
response = await taskApi.grabTaskItem(task.itemId || task.id!)
} else {
// 抢普通任务
response = await taskApi.grabTask(task.id!)
}
if (response.code === 200) {
message.success(`${taskType}抢单成功!`)
loadTasks(true) // 强制刷新列表
loadAllTabCounts() // 刷新所有Tab数量
}
} catch (error: any) {
console.error('抢单失败:', error)
message.error(error.message || '抢单失败')
}
}
})
}
// 从详情抽屉抢单
const handleGrabFromDetail = async () => {
if (!selectedTask.value) return
await handleGrab(selectedTask.value)
detailDrawerVisible.value = false
}
// 盲盒模式抢单
const handleGrabTask = async (task: TaskOrSubTask) => {
const isSubTask = task.isSubTask
const taskType = isSubTask ? '子任务' : '任务'
// 先检查权限
const { canGrab, reason } = canGrabTask(task)
if (!canGrab) {
message.warning(reason)
return
}
Modal.confirm({
title: '确认抢单',
content: h('div', {}, [
h('p', { style: { marginBottom: '8px' } }, '您正在盲盒模式下抢单'),
h('p', { style: { color: '#fa8c16', fontSize: '14px' } }, '任务信息将在抢单成功后显示'),
h('p', { style: { color: '#888', fontSize: '12px' } }, '确定要抢单这个任务吗?')
]),
okText: '确认抢单',
cancelText: '取消',
okButtonProps: { style: { background: '#fa8c16', borderColor: '#fa8c16' } },
onOk: async () => {
try {
let response
if (isSubTask) {
response = await taskApi.grabTaskItem(task.itemId || task.id!)
} else {
response = await taskApi.grabTask(task.id!)
}
if (response.code === 200) {
message.success(`${taskType}抢单成功!任务已添加到您的进行中任务`)
loadTasks(true)
loadAllTabCounts()
}
} catch (error: any) {
console.error('抢单失败:', error)
message.error(error.message || '抢单失败')
}
}
})
}
// ==================== 商品ID管理 ====================
// 加载任务商品ID列表
const loadTaskProducts = async (taskId: number) => {
try {
const response = await taskApi.getTaskProducts(taskId)
taskProducts.value = response.data || []
} catch (error) {
console.error('加载商品ID列表失败:', error)
taskProducts.value = []
}
}
// 判断是否可以管理商品ID(任务发起者、一级需求发起人、明细完善人或管理员)
const canManageProducts = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 管理员可以管理
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) return true
// 任务发起者可以管理
if (task.publisherId && task.publisherId === userInfo.id) return true
// 一级需求发起人可以管理
if ((task as any).primaryPublisherId && (task as any).primaryPublisherId === userInfo.id) return true
// 明细完善人可以管理
if ((task as any).detailCompleterId && (task as any).detailCompleterId === userInfo.id) return true
return false
}
// 添加商品ID
const handleAddProduct = async () => {
if (!selectedTask.value || !newProductId.value.trim()) {
message.warning('请输入商品ID')
return
}
addingProduct.value = true
try {
await taskApi.addTaskProduct(selectedTask.value.id!, {
productId: newProductId.value.trim()
})
message.success('添加成功')
newProductId.value = ''
await loadTaskProducts(selectedTask.value.id!)
} catch (error: any) {
message.error(error.message || '添加失败')
} finally {
addingProduct.value = false
}
}
// 批量添加商品ID
const handleBatchAddProducts = async () => {
if (!selectedTask.value || !batchProductIds.value.trim()) {
message.warning('请输入商品ID')
return
}
// 解析商品ID(支持换行和逗号分隔)
const ids = batchProductIds.value
.split(/[\n,]/)
.map(id => id.trim())
.filter(id => id.length > 0)
if (ids.length === 0) {
message.warning('请输入有效的商品ID')
return
}
addingProduct.value = true
try {
const count = await taskApi.addTaskProductsBatch(selectedTask.value.id!, ids)
message.success(`成功添加 ${count.data} 个商品ID`)
batchProductIds.value = ''
await loadTaskProducts(selectedTask.value.id!)
} catch (error: any) {
message.error(error.message || '批量添加失败')
} finally {
addingProduct.value = false
}
}
// 删除商品ID
const handleDeleteProduct = async (productId: number) => {
if (!selectedTask.value) return
Modal.confirm({
title: '确认删除',
content: '确定要删除这个商品ID吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await taskApi.deleteTaskProduct(selectedTask.value!.id!, productId)
message.success('删除成功')
await loadTaskProducts(selectedTask.value!.id!)
} catch (error: any) {
message.error(error.message || '删除失败')
}
}
})
}
// 确认商品ID填写完成
const handleConfirmProductIdFilled = async () => {
if (!selectedTask.value || !selectedTask.value.id) return
confirmingProductId.value = true
try {
await taskApi.confirmTaskProductFilled(selectedTask.value.id)
message.success('任务已确认完结')
// 更新当前任务状态
selectedTask.value.productIdFilled = 1
// 刷新任务列表
await loadCompletedTasks()
await loadMyTasks()
} catch (error: any) {
message.error(error.message || '确认失败')
} finally {
confirmingProductId.value = false
}
}
// 判断是否是URL
const isUrl = (str: string) => {
// 从字符串中提取可能的URL部分
const urlMatch = str.match(/https?:\/\/[^\s]+/)
return urlMatch !== null
}
// 从字符串中提取URL
const extractUrl = (str: string) => {
const urlMatch = str.match(/https?:\/\/[^\s]+/)
return urlMatch ? urlMatch[0] : str
}
// 从子任务交付路径提取标签(例如 "【TASK...】https://..." -> "下载 TASK...")
const extractLabel = (str: string) => {
// 尝试提取【】中的内容作为标签
const labelMatch = str.match(/【([^】]+)】/)
if (labelMatch) {
return `下载 ${labelMatch[1]}`
}
return '下载素材'
}
// 打开交付路径
const openDeliveryPath = (url: string) => {
window.open(url, '_blank')
}
// 监听配置更新事件
const handleConfigUpdate = async (event: CustomEvent) => {
console.log('配置已更新,刷新配置数据:', event.detail)
await loadConfigs()
}
// 组件挂载
// 获取任务状态文本(增强版,支持区分驳回、审核中和草稿)
const getTaskStatusText = (status: string, task?: any) => {
// 如果有task对象,检查approvalStatus来区分不同状态
if (task) {
// 调试日志
if (status === 'draft' && task.approvalStatus && task.approvalStatus !== 'draft') {
console.log('[状态调试] 任务ID:', task.id, '标题:', task.title, 'status:', status, 'approvalStatus:', task.approvalStatus)
}
// 1. 检查是否被驳回(无论status是draft还是rejected)
if (task.approvalStatus === 'rejected_by_demand' || task.approvalStatus === 'rejected_by_receiving') {
return '已驳回'
}
// 检查status是否是rejected
if (status === 'rejected') {
return '已驳回'
}
// 2. 已完成任务(必须优先判断,避免被其他条件覆盖)
if (status === 'completed') {
// productIdFilled为0或undefined表示未填写
if (!task.productIdFilled || task.productIdFilled === 0) {
return '待填写ID'
}
return '已完结'
}
// 3. 检查是否正在审核中
if (task.approvalStatus === 'pending_primary_approval') {
return '待一级审核'
}
if (task.approvalStatus === 'pending_detail_completion') {
return '待完善明细'
}
if (task.approvalStatus === 'pending_demand_approval') {
return '待部门审核'
}
if (task.approvalStatus === 'pending_receiving_approval') {
return '待接单审核'
}
// 4. 已审核通过的任务,根据是否有接收人显示不同状态
// 注意:isPipeline 可能是 1、true 或 "1",需要兼容处理
const isPipelineTask = task.isPipeline === 1 || task.isPipeline === true || task.isPipeline === '1'
if (task.approvalStatus === 'approved') {
if (task.receiverId) {
return '进行中'
} else {
return '待接单'
}
}
// 5. 普通任务status是pending但没有approvalStatus(兼容旧数据)
if (status === 'pending' && !task.approvalStatus) {
return '待接单'
}
}
const statusMap: Record<string, string> = {
'pending': '待接单',
'pending_audit': '待审核',
'in_progress': '进行中',
'review': '待审核',
'completed': '已完成',
'rejected': '已驳回',
'cancelled': '已取消',
'draft': '草稿'
}
return statusMap[status] || status
}
// 获取任务状态的CSS类名
const getTaskStatusClass = (task: any) => {
if (!task) return 'status-pending'
// 驳回状态
if (task.approvalStatus === 'rejected_by_demand' ||
task.approvalStatus === 'rejected_by_receiving' ||
task.status === 'rejected') {
return 'status-rejected'
}
// 已完成但未填写商品ID - 使用醒目的警告色
if (task.status === 'completed' && (!task.productIdFilled || task.productIdFilled === 0)) {
return 'status-pending-id'
}
// 已完结
if (task.status === 'completed') {
return 'status-completed'
}
// 其他状态
return `status-${task.status}`
}
// 加载所有Tab的数量统计
const loadAllTabCounts = async () => {
try {
// 并行加载所有Tab的数量统计
const promises: Promise<any>[] = []
// 获取用户信息用于判断部门视角
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const isDeptLeader = userInfo.isDepartmentLeader === 1 || managedDepartments.value.length > 0
const currentDeptType = effectiveDeptType.value
// 1. 可接任务数量
promises.push(
taskApi.getTaskHall({ page: 1, pageSize: 1 }).then(res => {
if (res.data && res.data.total !== undefined) {
total.value = res.data.total
}
}).catch(() => {})
)
// 2. 进行中任务数量 - 🔥 根据部门视角正确查询
if (isDeptLeader && currentDeptView.value !== undefined) {
// 部门负责人切换了视角
if (currentDeptType === 'demand') {
// 需求部门视角:查询该部门发起的所有处理中任务
promises.push(
taskApi.getTasks({
publisherDeptId: currentDeptView.value,
excludeStatuses: 'draft,completed,cancelled',
page: 1,
pageSize: 1
}).then(res => {
if (res.data && res.data.total !== undefined) {
inProgressTotal.value = res.data.total
}
}).catch(() => {})
)
} else {
// 接单部门视角:查询该部门接收的进行中任务(主任务+子任务)
promises.push(
Promise.all([
taskApi.getTasks({
receiverDeptId: currentDeptView.value,
status: 'in_progress',
page: 1,
pageSize: 1
}),
taskApi.getInProgressTaskItems({
deptId: currentDeptView.value,
page: 1,
pageSize: 1
})
]).then(([taskRes, itemRes]) => {
let count = 0
if (taskRes.data && taskRes.data.total !== undefined) {
count += taskRes.data.total
}
if (itemRes.data && itemRes.data.total !== undefined) {
count += itemRes.data.total
}
inProgressTotal.value = count
}).catch(() => {})
)
}
} else {
// 未切换视角或普通用户:根据部门类型查询
if (currentDeptType === 'demand' || currentDeptType === 'primary_demand') {
// 需求部门/一级需求部门用户:查询自己发起的任务 + 自己作为明细完善人的一级需求任务
promises.push(
Promise.all([
taskApi.getMyTasks({
role: 'publisher',
page: 1,
pageSize: 100 // 获取所有任务然后过滤
}),
// 🔥 同时查询明细完善人任务
taskApi.getMyTasks({
role: 'detail_completer',
page: 1,
pageSize: 100
})
]).then(([publisherRes, detailCompleterRes]) => {
let count = 0
const countedTaskIds = new Set<number>()
// 统计我发起的进行中任务
if (publisherRes.data && publisherRes.data.records) {
const inProgressTasks = publisherRes.data.records.filter((task: any) => {
if (task.isPipeline === 1) {
return (task.subTaskInProgress || 0) > 0 || task.status === 'in_progress'
}
return task.status === 'in_progress'
})
inProgressTasks.forEach((task: any) => countedTaskIds.add(task.id))
count += inProgressTasks.length
}
// 🔥 统计我作为明细完善人的进行中任务(去重)
if (detailCompleterRes.data && detailCompleterRes.data.records) {
const detailCompleterInProgress = detailCompleterRes.data.records.filter((task: any) => {
// 排除草稿和已完成状态,且不重复计算
return task.status !== 'draft' && task.status !== 'completed' && task.status !== 'cancelled' && !countedTaskIds.has(task.id)
})
count += detailCompleterInProgress.length
}
inProgressTotal.value = count
}).catch(() => {
inProgressTotal.value = 0
})
)
} else {
// 接单部门用户(设计师):查询自己接收的进行中任务
promises.push(
taskApi.getTasks({
status: 'in_progress',
role: 'receiver',
page: 1,
pageSize: 1
}).then(res => {
if (res.data && res.data.total !== undefined) {
inProgressTotal.value = res.data.total
}
}).catch(() => {})
)
}
}
// 3. 待审核任务数量(主任务 + 子任务组)
if (isDepartmentLeader.value) {
// 部门负责人:同时计算主任务和子任务组
promises.push(
Promise.all([
taskApi.getAllPendingApprovalTasks({ page: 1, pageSize: 1 }),
taskApi.getPendingApprovalTaskGroups()
]).then(([tasksRes, groupsRes]) => {
let count = 0
if (tasksRes.data && tasksRes.data.total !== undefined) {
count += tasksRes.data.total
}
if (groupsRes.code === 200 && groupsRes.data) {
count += groupsRes.data.length
}
reviewTotal.value = count
}).catch(() => {})
)
} else {
// 普通用户:只计算主任务
promises.push(
taskApi.getAllPendingApprovalTasks({
page: 1,
pageSize: 1
}).then(res => {
if (res.data && res.data.total !== undefined) {
reviewTotal.value = res.data.total
}
}).catch(() => {})
)
}
// 4. 已完成任务数量 - 🔥 根据部门视角正确查询
if (isDeptLeader && currentDeptView.value !== undefined && currentDeptType === 'receiving') {
// 接单部门负责人视角:查询子任务
promises.push(
taskApi.getCompletedTaskItems({
deptId: currentDeptView.value,
page: 1,
pageSize: 1
}).then(res => {
if (res.data && res.data.total !== undefined) {
completedTotal.value = res.data.total
}
}).catch(() => {})
)
} else if (currentDeptType === 'receiving' && !isDeptLeader) {
// 接单部门普通用户:查询自己完成的子任务
promises.push(
taskApi.getCompletedTaskItems({ page: 1, pageSize: 1 }).then(res => {
if (res.data && res.data.total !== undefined) {
completedTotal.value = res.data.total
}
}).catch(() => {})
)
} else {
// 需求部门或管理员:查询主任务
promises.push(
taskApi.getCompletedTasks({ page: 1, pageSize: 1 }).then(res => {
if (res.data && res.data.total !== undefined) {
completedTotal.value = res.data.total
}
}).catch(() => {})
)
}
// 5. 我发起的任务数量
promises.push(
taskApi.getMyTasks({ role: 'publisher', page: 1, pageSize: 1 }).then(res => {
if (res.code === 200 && res.data && res.data.total !== undefined) {
publishedTotal.value = res.data.total
}
}).catch(() => {})
)
// 6. 草稿箱数量(仅需求部门用户)
if (showDraftTab.value) {
promises.push(
taskApi.getMyTasks({ role: 'publisher', status: 'draft', page: 1, pageSize: 1 }).then(res => {
if (res.code === 200 && res.data && res.data.total !== undefined) {
draftsTotal.value = res.data.total
}
}).catch(() => {})
)
}
await Promise.all(promises)
} catch (error) {
console.error('加载Tab数量统计失败:', error)
}
}
onMounted(async () => {
await initDeptViewData() // 初始化部门视角数据(可能需要刷新用户信息)
loadConfigs()
loadDashboardStats()
loadAllTabCounts() // 加载所有Tab的数量统计
loadTasks(true) // 初始加载时强制获取数据
loadBlindBoxMode() // 加载盲盒模式配置
// 监听配置更新事件
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
})
// 组件卸载
onUnmounted(() => {
window.removeEventListener('configUpdated', handleConfigUpdate as EventListener)
})
</script>
<style lang="scss" scoped>
.task-hall-page {
min-height: 100vh;
padding: $space-xl 0;
background: $bg-primary;
}
// 数据总览样式
.overview-section {
margin-bottom: $space-xl;
.overview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-lg;
.section-title {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $text-primary;
margin-bottom: 0;
}
.overview-actions {
display: flex;
align-items: center;
gap: $space-md;
}
}
}
// 数据卡片网格布局
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
@media (max-width: 1400px) {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 992px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 576px) {
grid-template-columns: 1fr;
}
}
.stat-card {
display: flex;
align-items: center;
gap: $space-lg;
padding: $space-xl;
background: $bg-card;
border-radius: $radius-lg;
border: 1px solid $border-primary;
transition: all $transition-base $ease-out;
cursor: pointer;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(49, 240, 139, 0.1);
border-radius: $radius-md;
}
.stat-content {
flex: 1;
.stat-label {
font-size: $font-size-sm;
color: $text-secondary;
margin-bottom: $space-xs;
}
.stat-value {
font-size: 28px;
font-weight: $font-weight-bold;
color: $text-primary;
line-height: 1.2;
}
}
}
.container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $space-xl;
@media (max-width: $breakpoint-tablet) {
padding: 0 $space-lg;
}
@media (max-width: $breakpoint-mobile) {
padding: 0 $space-md;
}
}
.header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: $space-2xl;
button {
min-width: 140px;
}
// 统一下拉框、日期选择器、按钮的高度
:deep(.ant-select),
:deep(.ant-picker),
:deep(.ant-btn) {
height: 32px !important;
line-height: 30px;
}
:deep(.ant-select-selector) {
height: 32px !important;
line-height: 30px;
}
:deep(.ant-select-selection-item) {
line-height: 30px !important;
}
// 部门视角切换下拉框样式
.dept-view-select {
:deep(.ant-select-selection-item) {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
// 图表卡片 - Cursor 风格
.chart-card {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-xl;
transition: all $transition-base $ease-out;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
}
}
// 标签页样式统一
:deep(.ant-tabs) {
.ant-tabs-nav {
margin-bottom: $space-xl;
&::before {
border-bottom: 1px solid $border-primary;
}
}
.ant-tabs-tab {
padding: $space-md $space-xl;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-secondary;
transition: all $transition-base $ease-out;
&:hover {
color: $text-primary;
}
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #31f08b;
font-weight: $font-weight-semibold;
}
}
// 标签页数量样式
.tab-count {
font-size: $font-size-sm;
color: $text-tertiary;
font-weight: $font-weight-normal;
margin-left: 4px;
}
&.ant-tabs-tab-active .tab-count {
color: rgba(49, 240, 139, 0.7);
}
}
.ant-tabs-ink-bar {
background: #31f08b;
height: 3px;
}
.ant-tabs-content {
padding: 0;
}
}
// 搜索和筛选栏(合并)
.search-filter-bar {
display: flex;
flex-direction: column;
gap: $space-md;
margin-bottom: $space-xl;
padding: $space-lg;
}
// 搜索行(搜索、排序、统计)
.search-row {
display: flex;
align-items: center;
gap: $space-md;
width: 100%;
.search-input {
flex: 1;
min-width: 300px;
max-width: 500px;
}
.sort-select {
width: 150px;
flex-shrink: 0;
}
.stats-text {
font-size: $font-size-base;
color: $text-secondary;
white-space: nowrap;
margin-left: auto;
}
@media (max-width: $breakpoint-mobile) {
flex-wrap: wrap;
.stats-text {
margin-left: 0;
width: 100%;
}
}
}
// 筛选行(紧凑布局)
.filter-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $space-sm;
width: 100%;
.filter-select {
width: 140px;
flex-shrink: 0;
}
.score-range {
display: flex;
align-items: center;
gap: $space-xs;
flex-shrink: 0;
}
.score-input {
width: 100px;
}
.score-separator {
color: $text-secondary;
font-size: $font-size-base;
padding: 0 $space-xs;
}
.reset-btn {
margin-left: auto;
flex-shrink: 0;
}
@media (max-width: $breakpoint-mobile) {
.filter-select {
width: calc(50% - #{$space-sm / 2});
}
.score-range {
width: 100%;
margin-top: $space-xs;
}
.reset-btn {
margin-left: 0;
width: 100%;
margin-top: $space-xs;
}
}
}
// 加载状态
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
// 空状态
.empty-state {
text-align: center;
padding: $space-xxxl;
.empty-icon {
font-size: 64px;
margin-bottom: $space-lg;
}
h3 {
font-size: $font-size-2xl;
color: $text-primary;
margin-bottom: $space-sm;
}
p {
color: $text-secondary;
}
}
// 任务网格
.task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: $space-lg;
margin-bottom: $space-xl;
@media (max-width: $breakpoint-mobile) {
grid-template-columns: 1fr;
}
}
// 任务卡片
.task-card {
transition: all 0.2s ease-out;
cursor: pointer;
margin-bottom: 16px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
// 盲盒模式卡片样式
&.blind-box-card {
background: linear-gradient(135deg, #1f1f1f 0%, #141414 100%) !important;
border: 1px solid rgba(49, 240, 139, 0.2) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
min-height: 320px;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-4px);
border-color: rgba(49, 240, 139, 0.5) !important;
box-shadow: 0 12px 24px rgba(49, 240, 139, 0.15);
.box-icon-wrapper {
transform: scale(1.05) rotate(5deg);
.question-mark {
transform: translate(-50%, -60%) scale(1.1);
}
}
.blind-box-glow {
opacity: 0.5;
transform: scale(1.2);
}
}
.blind-box-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 30px 20px;
position: relative;
z-index: 1;
.blind-box-visual {
position: relative;
margin-bottom: 24px;
display: flex;
justify-content: center;
align-items: center;
width: 140px;
height: 140px;
.box-icon-wrapper {
position: relative;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
.main-box-icon {
width: 120px;
height: 120px;
color: #31f08b;
filter: drop-shadow(0 8px 16px rgba(49, 240, 139, 0.3));
}
.question-mark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 60px;
font-weight: 800;
color: #fff;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
font-family: 'Arial Rounded MT Bold', 'Helvetica Rounded', Arial, sans-serif;
transition: all 0.3s ease;
}
}
.blind-box-glow {
position: absolute;
width: 180px;
height: 180px;
background: radial-gradient(circle, rgba(49, 240, 139, 0.25) 0%, rgba(49, 240, 139, 0) 70%);
opacity: 0.2;
transition: all 0.4s ease;
z-index: -1;
}
}
.blind-box-info {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
flex: 1;
.blind-box-queue {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
.queue-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.45);
margin-bottom: 6px;
}
.queue-number {
font-size: 42px;
font-weight: 800;
background: linear-gradient(135deg, #fff 0%, #31f08b 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1;
filter: drop-shadow(0 2px 4px rgba(49, 240, 139, 0.2));
}
}
.blind-box-depts {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
.depts-label {
display: flex;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
margin-bottom: 10px;
}
.depts-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
.dept-tag {
padding: 4px 12px;
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.3);
border-radius: 12px;
font-size: 12px;
color: #31f08b;
transition: all 0.2s;
&:hover {
background: rgba(49, 240, 139, 0.2);
color: #52c41a;
border-color: rgba(49, 240, 139, 0.5);
}
}
}
}
}
.blind-box-action {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
.blind-box-grab-btn {
width: 80%;
height: 44px;
border-radius: 22px;
background: linear-gradient(90deg, #31f08b 0%, #52c41a 100%);
border: none;
font-weight: 600;
font-size: 15px;
box-shadow: 0 4px 12px rgba(49, 240, 139, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(49, 240, 139, 0.5);
background: linear-gradient(90deg, #52c41a 0%, #31f08b 100%);
}
&:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(49, 240, 139, 0.4);
}
}
.blind-box-hint {
display: flex;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
}
}
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
// 带预览图的卡片布局
.card-with-preview {
display: flex;
gap: 16px;
.task-preview {
flex-shrink: 0;
width: 65px;
height: 65px;
border-radius: 8px;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.card-content {
flex: 1;
min-width: 0;
&.has-preview {
.task-title {
font-size: 15px;
}
}
}
}
// 审核任务卡片特殊样式
.task-info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 16px 0;
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.info-label {
font-size: 12px;
color: #999;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
&.score-value {
color: #31f08b;
font-size: 18px;
font-weight: 600;
}
}
}
}
.task-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
// 任务编号区域样式
.task-no-section {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 0;
margin-top: 10px;
border-top: 1px solid rgba(49, 240, 139, 0.2);
font-size: 12px;
span {
color: #ffffff !important;
}
}
}
.task-header {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: $space-md;
}
// 流程阶段
.process-stage {
display: flex;
align-items: center;
gap: $space-xs;
padding: $space-sm $space-md;
margin-bottom: $space-md;
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.2);
border-radius: $radius-md;
font-size: $font-size-sm;
color: #31f08b;
.stage-icon {
display: flex;
align-items: center;
}
.stage-text {
font-weight: $font-weight-medium;
}
gap: $space-sm;
}
.task-badges {
display: flex;
flex-wrap: wrap;
gap: $space-xs;
align-items: center;
}
.task-title {
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
color: $text-primary;
margin: 0 0 8px 0;
// 限制标题最多显示2行
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em;
word-break: break-all;
}
// 状态标签行(单独一行)
.task-badges-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.badge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: $radius-small;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
white-space: nowrap;
}
.badge-subtask {
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.3);
color: #31f08b;
}
.badge-new {
background: rgba($color-success, 0.1);
border: 1px solid rgba($color-success, 0.3);
color: $color-success;
}
// 草稿标签样式
.task-badge-container {
margin-bottom: 12px;
}
.task-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: $radius-small;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
}
.draft-badge {
background: rgba(255, 193, 7, 0.15);
border: 1px solid rgba(255, 193, 7, 0.4);
color: #ffc107;
}
.badge-high-score {
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.2);
color: #31f08b;
}
.badge-urgent {
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.2);
color: #31f08b;
}
.badge-assigned {
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.2);
color: #31f08b;
}
// 待填写ID状态样式 - 醒目警告色
.status-pending-id {
background: rgba(250, 173, 20, 0.2);
border: 1px solid rgba(250, 173, 20, 0.5);
color: #faad14;
font-weight: 600;
animation: pulse-warning 2s infinite;
}
@keyframes pulse-warning {
0%, 100% {
box-shadow: 0 0 0 0 rgba(250, 173, 20, 0.4);
}
50% {
box-shadow: 0 0 8px 2px rgba(250, 173, 20, 0.3);
}
}
// 已完结状态样式
.status-finished {
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.3);
color: #31f08b;
}
// 确认完结区域样式
.confirm-complete-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid $border-primary;
}
// 所有子任务交付路径样式
.all-delivery-paths {
.delivery-paths-list {
display: flex;
flex-direction: column;
gap: 8px;
.delivery-path-item {
padding: 8px 12px;
background: $bg-tertiary;
border-radius: $radius-sm;
font-size: 13px;
.delivery-link {
color: #31f08b;
text-decoration: none;
word-break: break-all;
&:hover {
text-decoration: underline;
}
}
.delivery-text {
color: $text-secondary;
word-break: break-all;
}
}
}
}
// 驳回信息显示
.reject-info {
margin: 12px 0;
padding: 10px 12px;
background: rgba(245, 34, 45, 0.08);
border: 1px solid rgba(245, 34, 45, 0.2);
border-radius: 6px;
font-size: 13px;
.reject-label {
color: #f5222d;
font-weight: 500;
}
.reject-reason {
color: #ff7875;
margin-left: 4px;
}
}
// 交付路径
.delivery-path {
padding: 12px;
background: rgba(49, 240, 139, 0.05);
border-radius: 6px;
.delivery-link {
color: #31f08b;
text-decoration: none;
word-break: break-all;
&:hover {
text-decoration: underline;
}
}
.delivery-text {
color: #e0e0e0;
margin: 0;
word-break: break-all;
}
}
.badge-completed {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
border: 1px solid rgba(49, 240, 139, 0.2);
}
// 任务信息容器(包含预览图)
.task-info-wrapper {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: $space-md;
padding-bottom: $space-md;
border-bottom: 1px solid $border-primary;
}
// 任务信息
.task-info {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $space-md;
}
// 预览图缩略图
.preview-thumbnail {
position: relative;
width: 45px;
height: 45px;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
border: 1px solid $border-primary;
cursor: pointer;
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-popup {
display: none;
position: absolute;
top: 50%;
right: calc(100% + 10px);
transform: translateY(-50%);
width: 300px;
height: 300px;
background: $bg-card;
border: 2px solid $brand-primary;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
z-index: 1000;
padding: 4px;
img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 4px;
}
}
&:hover .preview-popup {
display: block;
}
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.label {
font-size: $font-size-xs;
color: $text-tertiary;
}
.value {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-primary;
}
}
// 任务类型明细
.type-details-section {
background: rgba($brand-primary, 0.05);
border: 1px solid rgba($brand-primary, 0.2);
border-radius: 8px;
padding: $space-sm $space-md;
margin-bottom: $space-md;
.type-details-title {
font-size: $font-size-xs;
color: $text-tertiary;
margin-bottom: $space-xs;
}
.type-details-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.type-detail-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: $font-size-sm;
padding: 2px 0;
.type-name {
color: $text-secondary;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.type-score {
color: $brand-primary;
font-weight: $font-weight-medium;
margin-left: $space-md;
white-space: nowrap;
}
}
}
// 🔥 可接单部门标签样式
.allowed-depts-section {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $space-xs;
margin-bottom: $space-md;
.allowed-depts-label {
font-size: $font-size-xs;
color: $text-tertiary;
white-space: nowrap;
}
.allowed-depts-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
.dept-tag {
display: inline-flex;
align-items: center;
padding: 2px 10px;
font-size: $font-size-xs;
color: $brand-primary;
background: rgba($brand-primary, 0.1);
border: 1px solid rgba($brand-primary, 0.3);
border-radius: 4px;
white-space: nowrap;
}
}
}
// 任务详情
.task-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $space-sm;
margin-bottom: $space-md;
// 响应式:小屏幕时改为单列
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.task-footer {
display: flex;
flex-wrap: wrap;
gap: $space-md;
padding-top: $space-sm;
border-top: 1px solid $border-primary;
margin-top: $space-sm;
.footer-item {
display: flex;
align-items: center;
gap: 4px;
font-size: $font-size-xs;
color: $text-tertiary;
&.task-no-item {
font-family: 'Monaco', 'Consolas', monospace;
color: $text-secondary;
display: flex;
align-items: center;
gap: 6px;
.copy-icon {
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s, color 0.2s;
&:hover {
opacity: 1;
color: #31f08b;
}
}
}
}
}
.detail-item {
display: flex;
align-items: center;
gap: $space-sm;
font-size: $font-size-sm;
color: $text-secondary;
min-width: 0; // 允许flex子项收缩
.icon {
font-size: $font-size-base;
flex-shrink: 0;
}
// 确保文本不会换行,保持紧凑
span:not(.icon) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
// 附件项需要跨两列(因为可能包含多个链接)
&.detail-item-attachment {
grid-column: 1 / -1;
span:not(.icon) {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
}
// 子任务进度样式(保留旧样式备用)
&.subtask-progress {
grid-column: 1 / -1;
.subtask-stats {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
white-space: nowrap;
.stat {
display: inline-flex;
align-items: center;
justify-content: center;
height: 22px;
padding: 0 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
&.completed {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
}
&.in-progress {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
}
&.pending {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
}
&.total {
color: #999;
padding: 0;
}
}
.stat-label {
color: #888;
font-size: 12px;
margin-left: 4px;
}
}
}
}
// 子任务时间线样式
.subtask-timeline-wrapper {
width: 100%;
margin: 16px 0;
padding: 16px 20px;
background: rgba(0, 0, 0, 0.25);
border-radius: 8px;
&.full-width {
// 确保横向拉满
box-sizing: border-box;
}
}
.subtask-timeline {
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
// 连接线
&::before {
content: '';
position: absolute;
top: 10px;
left: 20px;
right: 20px;
height: 2px;
background: linear-gradient(90deg, #52c41a 0%, #303030 50%, #303030 100%);
z-index: 0;
}
.timeline-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
min-width: 60px;
max-width: 100px;
.node-dot {
width: 20px;
height: 20px;
border-radius: 50%;
background: #303030;
border: 3px solid #444;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.node-label {
font-size: 12px;
color: #888;
text-align: center;
white-space: nowrap;
margin-bottom: 4px;
font-weight: 500;
}
.node-status {
font-size: 11px;
color: #666;
text-align: center;
white-space: nowrap;
}
// 待接单状态
&.pending {
.node-dot {
background: #303030;
border-color: #faad14;
}
.node-label {
color: #faad14;
}
.node-status {
color: #faad14;
opacity: 0.8;
}
}
// 进行中状态
&.in-progress {
.node-dot {
background: #31f08b;
border-color: #31f08b;
box-shadow: 0 0 10px rgba(49, 240, 139, 0.6);
animation: pulse 1.5s ease-in-out infinite;
}
.node-label {
color: #31f08b;
font-weight: 600;
}
.node-status {
color: #31f08b;
}
}
// 已完成状态
&.completed {
.node-dot {
background: #52c41a;
border-color: #52c41a;
}
.node-label {
color: #52c41a;
}
.node-status {
color: #52c41a;
}
}
// 更多节点
&.more {
flex: 0.6;
.node-dot {
width: 18px;
height: 18px;
background: #444;
border-color: #666;
}
.node-label {
color: #888;
font-size: 11px;
}
}
// 完结节点
&.final {
.node-dot {
background: #303030;
border-color: #666;
}
.node-label {
color: #888;
font-weight: 600;
}
&.completed {
.node-dot {
background: #52c41a;
border-color: #52c41a;
}
.node-label {
color: #52c41a;
}
}
}
}
}
// 脉冲动画
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 4px rgba(24, 144, 255, 0.4);
}
50% {
box-shadow: 0 0 12px rgba(24, 144, 255, 0.8);
}
}
// 任务操作
.task-actions {
display: flex;
gap: $space-sm;
}
// 任务详情抽屉
.task-detail {
.detail-section {
margin-bottom: $space-xl;
h3 {
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
color: $text-primary;
margin-bottom: $space-md;
}
h4 {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-primary;
margin-bottom: $space-md;
}
p {
color: $text-secondary;
line-height: 1.6;
}
}
.detail-badges {
display: flex;
flex-wrap: wrap;
gap: $space-xs;
margin-top: $space-sm;
}
.detail-actions {
margin-top: $space-2xl;
padding-top: $space-xl;
border-top: 1px solid $border-primary;
.action-buttons {
display: flex;
gap: $space-md;
button {
flex: 1;
}
}
}
}
.attachment-links {
display: flex;
flex-direction: column;
gap: $space-sm;
.attachment-link {
display: inline-flex;
align-items: center;
padding: $space-sm $space-md;
background: $bg-secondary;
border: 1px solid $border-primary;
border-radius: $radius-md;
color: $text-primary;
text-decoration: none;
transition: all $transition-base;
&:hover {
background: $bg-card-hover;
border-color: #31f08b;
color: #31f08b;
}
}
.attachment-locked-notice {
display: inline-flex;
align-items: center;
padding: $space-sm $space-md;
background: rgba(255, 255, 255, 0.05);
border: 1px dashed rgba(255, 255, 255, 0.15);
border-radius: $radius-md;
color: #888;
font-size: 13px;
}
}
// 分页
.pagination {
display: flex;
justify-content: center;
margin-top: $space-xl;
// 分页组件深度样式覆盖
:deep(.ant-pagination) {
.ant-pagination-item {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
a {
color: $text-secondary;
}
&:hover {
border-color: $brand-primary;
a {
color: $brand-primary;
}
}
&.ant-pagination-item-active {
background: $brand-primary;
border-color: $brand-primary;
a {
color: #000;
}
}
}
.ant-pagination-prev,
.ant-pagination-next {
.ant-pagination-item-link {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
color: $text-secondary;
}
&:hover .ant-pagination-item-link {
border-color: $brand-primary;
color: $brand-primary;
}
&.ant-pagination-disabled .ant-pagination-item-link {
color: $text-tertiary;
border-color: rgba(255, 255, 255, 0.1);
}
}
.ant-pagination-jump-prev,
.ant-pagination-jump-next {
.ant-pagination-item-container {
.ant-pagination-item-ellipsis {
color: $text-tertiary;
}
.ant-pagination-item-link-icon {
color: $brand-primary;
}
}
}
.ant-pagination-options {
.ant-select-selector {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
color: $text-secondary;
}
}
}
}
// 任务进度条
.task-progress {
margin: $space-md 0;
padding: $space-md;
background: rgba(49, 240, 139, 0.05);
border-radius: $radius-md;
.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: $space-sm;
font-size: $font-size-sm;
color: $text-secondary;
span:last-child {
font-weight: $font-weight-medium;
color: #31f08b;
}
}
}
// 任务操作按钮
.task-actions {
display: flex;
gap: $space-sm;
padding-top: $space-md;
border-top: 1px solid $border-primary;
flex-wrap: wrap;
.prev-assets-btn {
background: rgba(49, 240, 139, 0.1);
border-color: #31f08b;
color: #31f08b;
&:hover {
background: rgba(49, 240, 139, 0.2);
}
}
margin-top: $space-md;
button {
flex: 1;
}
}
// 任务卡片特定样式 - 统一简洁风格,不使用彩色边框
.task-card.in-progress,
.task-card.review,
.task-card.approval-card {
// 移除彩色边框,保持统一灰色底色
}
// badge样式补充 - 统一使用绿色主题
.status-draft {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
border: 1px solid rgba(250, 173, 20, 0.2);
}
.status-rejected {
background: rgba(245, 34, 45, 0.1);
color: #f5222d;
border: 1px solid rgba(245, 34, 45, 0.2);
}
.badge-in-progress {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
border: 1px solid rgba(49, 240, 139, 0.2);
}
.badge-review {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
border: 1px solid rgba(49, 240, 139, 0.2);
}
.badge-approval {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
border: 1px solid rgba(49, 240, 139, 0.2);
}
// 按钮样式
.btn {
padding: 8px 16px;
border: none;
border-radius: $radius-md;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
cursor: pointer;
transition: all $transition-base;
display: inline-flex;
align-items: center;
justify-content: center;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.btn-primary {
background: #31f08b;
color: #000;
&:hover:not(:disabled) {
background: #28d477;
transform: translateY(-1px);
}
}
.btn-secondary {
background: $bg-secondary;
border: 1px solid $border-primary;
color: $text-primary;
&:hover:not(:disabled) {
background: $bg-card-hover;
border-color: $border-color;
}
}
.btn-danger {
background: $bg-secondary;
border: 1px solid $border-primary;
color: $text-secondary;
&:hover:not(:disabled) {
background: $bg-card-hover;
border-color: $border-color;
color: $text-primary;
}
}
// 商品ID管理样式
.product-section {
.product-count {
font-size: $font-size-sm;
color: $text-secondary;
font-weight: normal;
}
.product-list {
margin: $space-md 0;
.product-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: $space-sm $space-md;
background: $bg-secondary;
border-radius: $radius-md;
margin-bottom: $space-xs;
.product-info {
display: flex;
align-items: center;
gap: $space-md;
.product-id {
font-family: 'SF Mono', Monaco, monospace;
font-size: $font-size-base;
color: #31f08b;
font-weight: 500;
}
.product-name {
font-size: $font-size-sm;
color: $text-secondary;
}
}
.product-meta {
display: flex;
align-items: center;
gap: $space-sm;
font-size: $font-size-xs;
color: $text-tertiary;
.creator {
&::after {
content: '·';
margin-left: $space-sm;
}
}
}
}
}
.empty-products {
text-align: center;
padding: $space-xl;
color: $text-tertiary;
p {
margin-top: $space-sm;
font-size: $font-size-sm;
}
}
.add-product-form {
margin-top: $space-md;
padding-top: $space-md;
border-top: 1px solid $border-primary;
.batch-add {
margin-top: $space-sm;
}
}
}
// 内联附件链接样式
.task-details {
.attachment-link {
color: #31f08b;
text-decoration: none;
margin-left: 4px;
margin-right: 8px;
&:hover {
text-decoration: underline;
}
}
// 锁定附件提示样式
.attachment-locked {
display: inline-flex;
align-items: center;
color: #888;
font-size: 12px;
padding: 2px 8px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
border: 1px dashed rgba(255, 255, 255, 0.15);
}
// 展开/折叠图标
.expand-subtasks {
display: inline-flex;
align-items: center;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
margin-left: 4px;
&:hover {
opacity: 1;
color: #31f08b;
}
}
// 子任务详情展开样式
.subtasks-detail {
grid-column: 1 / -1;
margin-top: $space-xs;
padding: $space-sm;
background: rgba(49, 240, 139, 0.05);
border-radius: $radius-sm;
border: 1px solid rgba(49, 240, 139, 0.1);
.subtask-item {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $space-sm;
font-size: 12px;
padding: 4px 0;
color: $text-secondary;
.subtask-label {
color: $text-primary;
font-weight: 500;
min-width: 80px;
}
.subtask-receiver {
color: $text-primary;
flex: 1;
min-width: 60px;
}
.subtask-deadline {
color: #31f08b;
font-size: 11px;
margin-left: $space-xs;
}
.subtask-status {
padding: 2px 8px;
border-radius: $radius-sm;
font-size: 11px;
font-weight: 500;
margin-left: auto;
&.status-completed {
background: rgba(49, 240, 139, 0.2);
color: #31f08b;
}
&.status-in-progress {
background: rgba(59, 130, 246, 0.2);
color: #3B82F6;
}
&.status-pending {
background: rgba(156, 163, 175, 0.2);
color: #9CA3AF;
}
&.status-locked {
background: rgba(107, 114, 128, 0.2);
color: #6B7280;
}
&.status-rejected {
background: rgba(239, 68, 68, 0.2);
color: #EF4444;
}
}
}
}
}
/* 审批弹窗暗黑主题适配 */
:deep(.approve-modal) .ant-modal-content,
:deep(.approve-modal) .ant-modal-header {
background-color: #1f1f1f !important;
color: #fff !important;
border-bottom: 1px solid #303030;
}
:deep(.approve-modal) .ant-modal-title {
color: #fff !important;
}
:deep(.approve-modal) .ant-modal-close {
color: rgba(255, 255, 255, 0.45) !important;
&:hover {
color: #fff !important;
}
}
:deep(.approve-modal) .ant-radio-wrapper {
color: #fff !important;
transition: all 0.3s;
&:hover .ant-radio-inner {
border-color: #31f08b !important;
}
}
:deep(.approve-modal) .ant-radio-checked .ant-radio-inner {
border-color: #31f08b !important;
background-color: #31f08b !important;
}
:deep(.approve-modal) .ant-radio-checked .ant-radio-inner::after {
background-color: #fff !important;
}
/* 选中状态的 Radio 卡片边框颜色 */
:deep(.approve-modal) .assign-option.ant-radio-wrapper-checked {
border-color: #31f08b !important;
background-color: rgba(49, 240, 139, 0.1) !important;
}
/* 下拉框暗黑适配 */
:deep(.approve-modal) .ant-select-selector {
background-color: #141414 !important;
border-color: #303030 !important;
color: #fff !important;
}
:deep(.approve-modal) .ant-select:not(.ant-select-disabled):hover .ant-select-selector,
:deep(.approve-modal) .ant-select-focused .ant-select-selector {
border-color: #31f08b !important;
box-shadow: 0 0 0 2px rgba(49, 240, 139, 0.2) !important;
}
:deep(.approve-modal) .ant-select-arrow {
color: rgba(255, 255, 255, 0.45) !important;
}
:deep(.approve-modal) .ant-select-item-option-selected {
background-color: rgba(49, 240, 139, 0.1) !important;
color: #fff;
font-weight: 600;
}
:deep(.approve-modal) .ant-select-item-option-active {
background-color: rgba(255, 255, 255, 0.08) !important;
}
/* 输入框适配 */
:deep(.approve-modal) .ant-input-number {
background-color: #141414;
border-color: #303030;
color: #fff;
}
:deep(.approve-modal) .ant-input-number:hover,
:deep(.approve-modal) .ant-input-number-focused {
border-color: #31f08b;
}
:deep(.approve-modal) .ant-input-number-handler-wrap {
background-color: #262626;
border-left-color: #303030;
}
:deep(.approve-modal) .ant-input-number-handler {
border-top-color: #303030;
&:hover .ant-input-number-handler-up-inner,
&:hover .ant-input-number-handler-down-inner {
color: #31f08b;
}
}
/* 按钮适配 */
:deep(.approve-modal) .ant-btn-primary {
background-color: #31f08b;
border-color: #31f08b;
color: #000;
font-weight: 500;
&:hover {
background-color: #5cf2a3;
border-color: #5cf2a3;
}
}
:deep(.approve-modal) .ant-btn-text.ant-btn-danger:hover {
background-color: rgba(255, 77, 79, 0.1);
}
/* 级联选择器深色主题 - 组件内部 */
:deep(.approve-modal) .ant-cascader {
.ant-select-selector {
background-color: #141414 !important;
border-color: #303030 !important;
color: #fff !important;
}
&:hover .ant-select-selector {
border-color: #31f08b !important;
}
.ant-select-selection-placeholder {
color: #666 !important;
}
.ant-select-selection-item {
color: #fff !important;
}
}
</style>
<!-- 全局样式:级联选择器弹窗深色主题(弹窗渲染在body上) -->
<style>
.dark-cascader-popup {
background-color: #1f1f1f !important;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5) !important;
}
.dark-cascader-popup .ant-cascader-menu {
background-color: #1f1f1f !important;
border-color: #303030 !important;
}
.dark-cascader-popup .ant-cascader-menu-item {
color: #fff !important;
padding: 8px 12px;
}
.dark-cascader-popup .ant-cascader-menu-item:hover {
background-color: rgba(49, 240, 139, 0.1) !important;
}
.dark-cascader-popup .ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled),
.dark-cascader-popup .ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled):hover {
background-color: rgba(49, 240, 139, 0.2) !important;
font-weight: 500;
}
.dark-cascader-popup .ant-cascader-menu-item-expand-icon {
color: #888 !important;
}
.dark-cascader-popup .ant-cascader-menu-item-content {
color: #fff !important;
}
/* 搜索框深色主题 */
.dark-cascader-popup .ant-cascader-input {
background-color: #262626 !important;
border-color: #303030 !important;
color: #fff !important;
}
.dark-cascader-popup .ant-cascader-input::placeholder {
color: #666 !important;
}
/* 空状态 */
.dark-cascader-popup .ant-cascader-empty {
color: #888 !important;
}
</style>