1. 后端 fillTaskRelatedInfo 填充明细完善人姓名 (detailCompleterName) 2. 前端任务卡片统一显示: - 一级需求人(橙色):一级需求部门的原始发起人 + 部门名称 - 需求部门发起人(绿色):明细完善人 - 发布人:非一级需求任务时显示 3. 需求部门负责人和成员可以看到一级需求任务(明细完善人在本部门的) 4. 后端增加 getMyDetailCompleterTasks 接口查询明细完善人任务
8143 lines
287 KiB
Vue
8143 lines
287 KiB
Vue
<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
|
||
v-model="filters"
|
||
:stats-text="`共 ${total} 个任务`"
|
||
:sort-options="availableSortOptions"
|
||
:status-options="availableStatusOptions"
|
||
status-placeholder="任务状态"
|
||
: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 in taskList"
|
||
:key="task.id"
|
||
class="task-card chart-card"
|
||
@click="handleTaskClick(task)"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span v-if="task.isSubTask" class="badge badge-subtask">
|
||
<Icon name="layers" size="sm" color="#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="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="!(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布人: {{ task.publisherName || '未知' }}</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>附件:
|
||
<span class="attachment-locked">
|
||
<Icon name="lock" size="xs" style="margin-right: 4px;" />
|
||
{{ (task.attachmentUrl || '').split(',').filter(u => u.trim()).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">
|
||
<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
|
||
: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 ? '抢单' : '不可接单' }}
|
||
</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>
|
||
</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
|
||
v-model="reviewFilters"
|
||
: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="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发起人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="!(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.publisherName || '--' }}</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 v-if="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="task.publisherName && !(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布人: {{ task.publisherName }}</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
|
||
v-model="inProgressFilters"
|
||
: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="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="task.publisherName && !(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布人: {{ task.publisherName }}</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">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">发布: {{ formatTime(task.createTime) }}</span>
|
||
</div>
|
||
<div class="footer-item" v-if="task.estimatedHours">
|
||
<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">
|
||
<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
|
||
v-model="completedFilters"
|
||
: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="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发起人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="!(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.publisherName || '--' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>
|
||
完成人: {{ task.receiverName || '--' }}
|
||
<span
|
||
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="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="task.publisherName && !(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布人: {{ task.publisherName }}</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">
|
||
<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">
|
||
<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
|
||
v-model="publishedFilters"
|
||
: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="task.isPrimaryTask === 1 && task.primaryPublisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#faad14" />
|
||
</span>
|
||
<span style="color: #faad14;">一级需求人: {{ task.primaryPublisherName }} ({{ task.primaryPublisherDeptName || '一级部门' }})</span>
|
||
</div>
|
||
<!-- 明细完善人(一级需求任务且有明细完善人) -->
|
||
<div v-if="task.isPrimaryTask === 1 && task.detailCompleterName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#52c41a" />
|
||
</span>
|
||
<span style="color: #52c41a;">需求部门发起人: {{ task.detailCompleterName }}</span>
|
||
</div>
|
||
<!-- 发布人:非一级需求任务或一级需求任务无明细完善人时显示 -->
|
||
<div v-if="task.publisherName && !(task.isPrimaryTask === 1 && task.detailCompleterName)" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布人: {{ task.publisherName }}</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">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">发布: {{ formatTime(task.createTime) }}</span>
|
||
</div>
|
||
<div class="footer-item" v-if="task.estimatedHours">
|
||
<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">
|
||
<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
|
||
v-model="draftsFilters"
|
||
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">
|
||
<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">
|
||
<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">
|
||
<!-- 可接任务:显示抢单 -->
|
||
<a-button
|
||
v-if="selectedTask.status === 'pending'"
|
||
: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
|
||
:disabled="!canGrabTask(selectedTask).canGrab"
|
||
@click="handleGrabFromDetail">
|
||
{{ canGrabTask(selectedTask).canGrab ? '确认抢单' : `不可接单(${canGrabTask(selectedTask).reason})` }}
|
||
</a-button>
|
||
|
||
<!-- 进行中任务:显示更新进度和完结 -->
|
||
<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 } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { message, Modal, InputNumber, Textarea, Radio, RadioGroup, Button, Select, Space, Tag } from 'ant-design-vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import 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 } 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')
|
||
|
||
// 是否为部门负责人(从localStorage获取用户信息)
|
||
const isDepartmentLeader = computed(() => {
|
||
try {
|
||
const userStr = localStorage.getItem('userInfo')
|
||
if (!userStr) return false
|
||
const user = JSON.parse(userStr)
|
||
return user?.isDepartmentLeader === 1 || user?.role === 'dept_leader' || user?.role === 'admin'
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
// 部门视角切换功能(用于管理多个部门的负责人)
|
||
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 = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
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 {
|
||
const userStr = localStorage.getItem('userInfo')
|
||
let user = userStr ? JSON.parse(userStr) : null
|
||
|
||
// 如果用户信息中没有 managedDepartments 字段,则调用 API 刷新
|
||
if (user && !user.managedDepartments) {
|
||
console.log('用户信息中没有 managedDepartments,刷新用户信息...')
|
||
try {
|
||
const response = await authApi.getUserInfo()
|
||
if (response.data) {
|
||
user = response.data
|
||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||
console.log('用户信息已刷新:', user)
|
||
}
|
||
} catch (e) {
|
||
console.warn('刷新用户信息失败:', e)
|
||
}
|
||
}
|
||
|
||
if (user) {
|
||
userDeptId.value = user.deptId
|
||
userDeptName.value = user.deptName || '我的部门'
|
||
userDeptType.value = user.deptType || 'demand'
|
||
|
||
// 获取用户管理的部门列表
|
||
if (user.managedDepartments && Array.isArray(user.managedDepartments)) {
|
||
managedDepartments.value = user.managedDepartments.filter(
|
||
(dept: ManagedDepartment) => dept.deptId !== user.deptId // 排除用户自己所属的部门
|
||
)
|
||
console.log('用户管理的部门:', 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 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' || 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: 'latest' as '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: '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 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 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) {
|
||
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
|
||
}
|
||
|
||
// 判断是否可以下载附件(非pending状态或管理员可下载)
|
||
const canDownloadAttachment = (task: TaskOrSubTask) => {
|
||
// 管理员可以下载
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) {
|
||
return true
|
||
}
|
||
// 非pending状态可以下载(已接单、进行中、已完成等)
|
||
if (task.status !== 'pending') {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 加载配置数据(从全局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
|
||
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 (filters.keyword) params.keyword = filters.keyword.trim()
|
||
if (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]
|
||
}
|
||
|
||
// 按创建时间排序
|
||
allTasks.sort((a, b) => {
|
||
const timeA = new Date(a.createTime || 0).getTime()
|
||
const timeB = new Date(b.createTime || 0).getTime()
|
||
return timeB - timeA // 降序
|
||
})
|
||
|
||
// 缓存所有数据
|
||
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 = 'latest'
|
||
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.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,
|
||
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 || 'pending',
|
||
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') {
|
||
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)
|
||
if (dept.deptType === '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) // 子任务总数
|
||
}
|
||
}
|
||
|
||
// 加载待审核任务(所有处于审批流程中的任务 + 部门负责人的子任务组)
|
||
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,
|
||
// 审核人信息(流程阶段显示)
|
||
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') || '{}')
|
||
return task.publisherId === userInfo.id
|
||
}
|
||
|
||
// 显示评分弹窗
|
||
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_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 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') {
|
||
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.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: '您的部门无权接收此类型的任务' }
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// ==================== 商品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
|
||
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) // 初始加载时强制获取数据
|
||
// 监听配置更新事件
|
||
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);
|
||
}
|
||
|
||
// 带预览图的卡片布局
|
||
.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>
|
||
|