Your Name 7e970fa614 style: 优化审核弹窗UI风格,统一使用Ant Design组件
优化内容:

1. 组件替换
   ✅ 使用 Button 替换原生 button
   ✅ 使用 Select 替换原生 select
   ✅ 使用 InputNumber 替换 input[type=number]
   ✅ 使用 Space 组件优化布局
   ✅ 使用 Tag 组件显示标签

2. 任务类型编辑区域
   ✅ 改用浅灰背景 (#fafafa)
   ✅ 边框颜色统一 (#e8e8e8)
   ✅ 任务类型显示为蓝色 Tag
   ✅ 使用 InputNumber 组件调整数量
   ✅ 删除按钮使用 text + danger 样式
   ✅ 添加按钮使用 primary + 绿色样式

3. 接单方式选择
   ✅ Radio 选项卡片化设计
   ✅ 选中状态浅蓝背景 (#e6f7ff)
   ✅ 添加图标前缀 (🏢/👤)
   ✅ 卡片圆角 6px
   ✅ 过渡动画效果

4. 设计师列表
   ✅ 卡片化设计,选中高亮
   ✅ 使用 Tag 显示任务数量
   ✅ 根据任务量显示不同颜色:
      - 0-3个:success (绿色)
      - 4-5个:warning (橙色)
      - 6+个:error (红色)

5. 弹窗样式
   ✅ 标题添加 emoji 图标
   ✅ 宽度调整为 650px
   ✅ 按钮尺寸统一为 large
   ✅ 按钮最小宽度 100px

6. 驳回弹窗
   ✅ 同样优化风格
   ✅ 增加内边距
   ✅ 文本域高度增加到 5 行
   ✅ 统一按钮样式

整体效果:
- 与系统其他页面风格一致
- 使用 Ant Design 标准组件
- 视觉层次更清晰
- 交互体验更友好
- 响应式过渡动画
2026-01-03 12:07:54 +08:00

4573 lines
151 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<DefaultLayout>
<div class="task-hall-page">
<div class="container">
<!-- 数据总览 -->
<div class="overview-section">
<div class="overview-header">
<h2 class="section-title">数据总览</h2>
<div class="overview-actions">
<a-month-picker
v-model:value="selectedMonth"
placeholder="选择月份"
format="YYYY-MM"
@change="handleMonthChange"
style="width: 150px; margin-right: 12px;"
/>
<a-button type="primary" size="large" @click="handleCreateTask">
<Icon name="plus" size="sm" style="margin-right: 6px;" />
发起任务
</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="可接任务">
<!-- 搜索和筛选栏(合并) -->
<div class="search-filter-bar chart-card">
<!-- 第一行:搜索、排序和统计 -->
<div class="search-row">
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索任务标题、SPU信息、发布人..."
class="search-input"
@search="handleFilterChange"
@pressEnter="handleFilterChange"
/>
<a-select
v-model:value="filters.sortBy"
placeholder="排序方式"
class="sort-select"
@change="handleFilterChange"
>
<a-select-option value="latest">最新发布</a-select-option>
<a-select-option value="score">积分最高</a-select-option>
<a-select-option value="deadline">时效最短</a-select-option>
<a-select-option value="hot">最热门</a-select-option>
</a-select>
<span class="stats-text">共 {{ total }} 个任务</span>
</div>
<!-- 第二行:筛选条件 -->
<div class="filter-row">
<a-select
v-model:value="filters.taskTypeId"
placeholder="任务类型"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.demandTypeId"
placeholder="需求类型"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.brandId"
placeholder="品牌"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="brand in brands"
:key="brand.id"
:value="brand.id"
>
{{ brand.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.categoryId"
placeholder="类目"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.platformId"
placeholder="平台"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="platform in platforms"
:key="platform.id"
:value="platform.id"
>
{{ platform.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.timeRange"
placeholder="发布时间"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option value="today">今天</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
<div class="score-range">
<a-input
v-model:value="filters.minScore"
placeholder="最小积分"
class="score-input"
@pressEnter="handleFilterChange"
/>
<span class="score-separator">-</span>
<a-input
v-model:value="filters.maxScore"
placeholder="最大积分"
class="score-input"
@pressEnter="handleFilterChange"
/>
</div>
<a-button @click="handleReset" class="reset-btn">重置</a-button>
</div>
</div>
<!-- 任务列表 -->
<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="#1890ff" 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">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div 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 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>附件:
<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 class="detail-item">
<span class="icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span>截止: {{ formatDeadline(task.deadline) }}</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>
</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>
<div class="search-filter-bar chart-card">
<!-- 第一行:搜索、排序和统计 -->
<div class="search-row">
<a-input-search
v-model:value="reviewFilters.keyword"
placeholder="搜索任务标题、SPU信息、发布人..."
class="search-input"
@search="loadReviewTasks"
@pressEnter="loadReviewTasks"
/>
<a-select
v-model:value="reviewFilters.sortBy"
placeholder="排序方式"
class="sort-select"
@change="loadReviewTasks"
>
<a-select-option value="latest">最新提交</a-select-option>
<a-select-option value="score">积分最高</a-select-option>
</a-select>
<span class="stats-text">共 {{ reviewTotal }} 个待审核任务</span>
</div>
<!-- 第二行:筛选条件 -->
<div class="filter-row">
<a-select
v-model:value="reviewFilters.taskTypeId"
placeholder="任务类型"
class="filter-select"
allowClear
@change="loadReviewTasks"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="reviewFilters.brandId"
placeholder="品牌"
class="filter-select"
allowClear
@change="loadReviewTasks"
>
<a-select-option
v-for="brand in brands"
:key="brand.id"
:value="brand.id"
>
{{ brand.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="reviewFilters.categoryId"
placeholder="类目"
class="filter-select"
allowClear
@change="loadReviewTasks"
>
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="reviewFilters.platformId"
placeholder="平台"
class="filter-select"
allowClear
@change="loadReviewTasks"
>
<a-select-option
v-for="platform in platforms"
:key="platform.id"
:value="platform.id"
>
{{ platform.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="reviewFilters.timeRange"
placeholder="提交时间"
class="filter-select"
allowClear
@change="loadReviewTasks"
>
<a-select-option value="today">今天</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
</div>
<!-- 第三行:积分范围和重置 -->
<div class="filter-row">
<a-input-number
v-model:value="reviewFilters.minScore"
placeholder="最小积分"
class="score-input"
:min="0"
@change="loadReviewTasks"
/>
<span class="score-divider">-</span>
<a-input-number
v-model:value="reviewFilters.maxScore"
placeholder="最大积分"
class="score-input"
:min="0"
@change="loadReviewTasks"
/>
<a-button class="reset-btn" @click="handleReviewFiltersReset">
重置
</a-button>
</div>
</div>
<!-- 待审核任务列表 -->
<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 class="badge badge-review">
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
待审核
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>发起人: {{ task.publisherName || '--' }}</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>接单人: {{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.deliveryPath" class="detail-item">
<span class="icon">
<Icon name="link" size="sm" color="#31f08b" />
</span>
<span>交付路径: {{ task.deliveryPath }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
<a-button v-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>
</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) }}</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>
<div class="search-filter-bar chart-card">
<!-- 第一行:搜索、排序和统计 -->
<div class="search-row">
<a-input-search
v-model:value="inProgressFilters.keyword"
placeholder="搜索任务标题、SPU信息、发布人..."
class="search-input"
@search="loadInProgressTasks"
@pressEnter="loadInProgressTasks"
/>
<a-select
v-model:value="inProgressFilters.sortBy"
placeholder="排序方式"
class="sort-select"
@change="loadInProgressTasks"
>
<a-select-option value="latest">最新发布</a-select-option>
<a-select-option value="deadline">截止最近</a-select-option>
<a-select-option value="progress">进度最低</a-select-option>
</a-select>
<span class="stats-text">共 {{ inProgressTotal }} 个进行中任务</span>
</div>
<!-- 第二行:筛选条件 -->
<div class="filter-row">
<a-select
v-model:value="inProgressFilters.taskTypeId"
placeholder="任务类型"
class="filter-select"
allowClear
@change="loadInProgressTasks"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="inProgressFilters.brandId"
placeholder="品牌"
class="filter-select"
allowClear
@change="loadInProgressTasks"
>
<a-select-option
v-for="brand in brands"
:key="brand.id"
:value="brand.id"
>
{{ brand.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="inProgressFilters.categoryId"
placeholder="类目"
class="filter-select"
allowClear
@change="loadInProgressTasks"
>
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="inProgressFilters.platformId"
placeholder="平台"
class="filter-select"
allowClear
@change="loadInProgressTasks"
>
<a-select-option
v-for="platform in platforms"
:key="platform.id"
:value="platform.id"
>
{{ platform.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="inProgressFilters.timeRange"
placeholder="发布时间"
class="filter-select"
allowClear
@change="loadInProgressTasks"
>
<a-select-option value="today">今天</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
</div>
<!-- 第三行:进度范围和重置 -->
<div class="filter-row">
<a-input-number
v-model:value="inProgressFilters.minProgress"
placeholder="最小进度%"
class="score-input"
:min="0"
:max="100"
@change="loadInProgressTasks"
/>
<span class="score-divider">-</span>
<a-input-number
v-model:value="inProgressFilters.maxProgress"
placeholder="最大进度%"
class="score-input"
:min="0"
:max="100"
@change="loadInProgressTasks"
/>
<a-button class="reset-btn" @click="handleInProgressFiltersReset">
重置
</a-button>
</div>
</div>
<!-- 进行中任务列表 -->
<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 in-progress"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span v-if="task.isSubTask" class="badge badge-subtask">
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
子任务
</span>
<span class="badge badge-in-progress">
<Icon name="clock" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
进行中
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>发起人: {{ task.publisherName || '--' }}</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>接单人: {{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 进度条 -->
<div class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 0 }}%</span>
</div>
<a-progress
:percent="task.progress || 0"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click="handleUpdateProgress(task)">
更新进度
</a-button>
<a-button v-if="canCompleteTask(task)" type="primary" size="small" @click="handleCompleteTask(task)">
完结任务
</a-button>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.deadline">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">截止: {{ formatDate(task.deadline) }}</span>
</div>
</div>
</div>
</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>
<div class="search-filter-bar chart-card">
<!-- 第一行:搜索、排序和统计 -->
<div class="search-row">
<a-input-search
v-model:value="completedFilters.keyword"
placeholder="搜索任务标题、SPU信息、发布人..."
class="search-input"
@search="loadCompletedTasks"
@pressEnter="loadCompletedTasks"
/>
<a-select
v-model:value="completedFilters.sortBy"
placeholder="排序方式"
class="sort-select"
@change="loadCompletedTasks"
>
<a-select-option value="latest">最新完成</a-select-option>
<a-select-option value="score">积分最高</a-select-option>
</a-select>
<span class="stats-text">共 {{ completedTotal }} 个已完成任务</span>
</div>
<!-- 第二行:筛选条件 -->
<div class="filter-row">
<a-select
v-model:value="completedFilters.taskTypeId"
placeholder="任务类型"
class="filter-select"
allowClear
@change="loadCompletedTasks"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="completedFilters.demandTypeId"
placeholder="需求类型"
class="filter-select"
allowClear
@change="loadCompletedTasks"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="completedFilters.brandId"
placeholder="品牌"
class="filter-select"
allowClear
@change="loadCompletedTasks"
>
<a-select-option
v-for="brand in brands"
:key="brand.id"
:value="brand.id"
>
{{ brand.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="completedFilters.categoryId"
placeholder="类目"
class="filter-select"
allowClear
@change="loadCompletedTasks"
>
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="completedFilters.platformId"
placeholder="平台"
class="filter-select"
allowClear
@change="loadCompletedTasks"
>
<a-select-option
v-for="platform in platforms"
:key="platform.id"
:value="platform.id"
>
{{ platform.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="completedFilters.timeRange"
placeholder="发布时间"
class="filter-select"
allowClear
@change="loadCompletedTasks"
>
<a-select-option value="today">今天</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
</div>
<!-- 第三行:积分范围和重置 -->
<div class="filter-row">
<a-input-number
v-model:value="completedFilters.minScore"
placeholder="最小积分"
class="score-input"
:min="0"
@change="loadCompletedTasks"
/>
<span class="score-divider">-</span>
<a-input-number
v-model:value="completedFilters.maxScore"
placeholder="最大积分"
class="score-input"
:min="0"
@change="loadCompletedTasks"
/>
<a-button class="reset-btn" @click="handleCompletedFiltersReset">
重置
</a-button>
</div>
</div>
<!-- 已完成任务列表 -->
<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"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span class="badge badge-completed">
<Icon name="check-circle" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
已完成
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">获得积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>发起人: {{ task.publisherName || '--' }}</span>
</div>
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>完成人: {{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.deliveryPath" class="detail-item">
<span class="icon">
<Icon name="link" size="sm" color="#31f08b" />
</span>
<span>交付: {{ task.deliveryPath }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 进度条(已完成任务显示100%) -->
<div class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 100 }}%</span>
</div>
<a-progress
:percent="task.progress || 100"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
</div>
<div class="footer-item">
<Icon name="check" size="sm" color="#31f08b" />
<span style="color: #ffffff;">完成: {{ formatDate(task.completeTime) }}</span>
</div>
</div>
</div>
</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>
<div class="search-filter-bar chart-card">
<!-- 第一行:搜索、排序和统计 -->
<div class="search-row">
<a-input-search
v-model:value="publishedFilters.keyword"
placeholder="搜索任务标题、SPU信息、发布人..."
class="search-input"
@search="loadPublishedTasks"
@pressEnter="loadPublishedTasks"
/>
<a-select
v-model:value="publishedFilters.sortBy"
placeholder="排序方式"
class="sort-select"
@change="loadPublishedTasks"
>
<a-select-option value="latest">最新发布</a-select-option>
<a-select-option value="score">积分最高</a-select-option>
<a-select-option value="deadline">时效最短</a-select-option>
</a-select>
<span class="stats-text">共 {{ publishedTotal }} 个任务</span>
</div>
<!-- 第二行:筛选条件 -->
<div class="filter-row">
<a-select
v-model:value="publishedFilters.status"
placeholder="任务状态"
class="filter-select"
allowClear
@change="loadPublishedTasks"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="pending">待接单</a-select-option>
<a-select-option value="in_progress">进行中</a-select-option>
<a-select-option value="review">待审核</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="rejected">已驳回</a-select-option>
</a-select>
<a-select
v-model:value="publishedFilters.taskTypeId"
placeholder="任务类型"
class="filter-select"
allowClear
@change="loadPublishedTasks"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="publishedFilters.brandId"
placeholder="品牌"
class="filter-select"
allowClear
@change="loadPublishedTasks"
>
<a-select-option
v-for="brand in brands"
:key="brand.id"
:value="brand.id"
>
{{ brand.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="publishedFilters.categoryId"
placeholder="类目"
class="filter-select"
allowClear
@change="loadPublishedTasks"
>
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="publishedFilters.platformId"
placeholder="平台"
class="filter-select"
allowClear
@change="loadPublishedTasks"
>
<a-select-option
v-for="platform in platforms"
:key="platform.id"
:value="platform.id"
>
{{ platform.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="publishedFilters.timeRange"
placeholder="发布时间"
class="filter-select"
allowClear
@change="loadPublishedTasks"
>
<a-select-option value="today">今天</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
</div>
<!-- 第三行:积分范围和重置 -->
<div class="filter-row">
<a-input-number
v-model:value="publishedFilters.minScore"
placeholder="最小积分"
class="score-input"
:min="0"
@change="loadPublishedTasks"
/>
<span class="score-divider">-</span>
<a-input-number
v-model:value="publishedFilters.maxScore"
placeholder="最大积分"
class="score-input"
:min="0"
@change="loadPublishedTasks"
/>
<a-button class="reset-btn" @click="handlePublishedFiltersReset">
重置
</a-button>
</div>
</div>
<!-- 我发起的任务列表 -->
<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}`]"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span :class="['badge', `status-${task.status}`]">
{{ getTaskStatusText(task.status) }}
</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">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.taskNo }}</span>
</div>
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
</div>
<div class="info-item">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }} 分</span>
</div>
<div class="info-item">
<span class="label">数量</span>
<span class="value">{{ task.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>处理人: {{ task.receiverName || '待接单' }}</span>
</div>
<!-- 子任务进度(流水线任务) -->
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="detail-item subtask-progress">
<span class="icon">
<Icon name="layers" size="sm" color="#31f08b" />
</span>
<span class="subtask-stats">
子任务:
<span class="stat completed">已完成 {{ task.subTaskCompleted || 0 }}</span>
<span class="stat in-progress">进行中 {{ task.subTaskInProgress || 0 }}</span>
<span class="stat pending">待接单 {{ task.subTaskPending || 0 }}</span>
<span class="stat total">/ 共 {{ task.subTaskTotal }}</span>
</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link"
@click.stop
>
附件{{ index + 1 }}
</a>
</span>
</div>
</div>
<!-- 进度条(进行中和已完成的任务显示) -->
<div v-if="task.status === 'in_progress' || task.status === 'completed'" class="task-progress">
<div class="progress-label">
<span>任务进度</span>
<span>{{ task.progress || 0 }}%</span>
</div>
<a-progress
:percent="task.progress || 0"
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
:show-info="false"
/>
</div>
<!-- 时间信息 -->
<div class="task-footer">
<div class="footer-item">
<Icon name="calendar" size="sm" color="#31f08b" />
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
</div>
<div class="footer-item" v-if="task.deadline">
<Icon name="clock" size="sm" color="#31f08b" />
<span style="color: #ffffff;">截止: {{ formatDate(task.deadline) }}</span>
</div>
</div>
<!-- 被驳回任务的操作按钮 -->
<div v-if="task.status === '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>
</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="pendingApproval" v-if="isDepartmentLeader">
<template #tab>
<span>待我审核 <span class="tab-count">({{ pendingApprovalTotal }})</span></span>
</template>
<div class="search-filter-bar chart-card">
<div class="search-row">
<a-input-search
v-model:value="approvalFilters.keyword"
placeholder="搜索任务标题、任务编号..."
class="search-input"
@search="loadPendingApprovalTasks"
@pressEnter="loadPendingApprovalTasks"
/>
<span class="stats-text">共 {{ pendingApprovalTotal }} 个待审核任务</span>
</div>
</div>
<!-- 任务列表 - 使用统一的任务卡片样式 -->
<div class="task-grid">
<div
v-for="item in pendingApprovalTasks"
:key="`${item.type}-${item.id}`"
class="task-card chart-card approval-card"
@click="item.type === 'task' ? handleTaskClick(item.data) : null"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ item.title }}</h3>
<div class="task-badges">
<span v-if="item.type === 'taskGroup'" class="badge badge-subtask">
<Icon name="layers" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
子任务组
</span>
<span v-else class="badge badge-approval">
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
{{ getApprovalStatusText(item.data.approvalStatus) }}
</span>
<span v-if="item.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="item.type === 'task' && getProcessStage(item.data)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(item.data) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div v-if="item.type === 'task'" class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ getTaskTypeName(item.data.taskTypeId) }}</span>
</div>
<div v-if="item.type === 'taskGroup'" class="info-item">
<span class="label">子任务数量</span>
<span class="value">{{ item.data.items?.length || 0 }} 个</span>
</div>
<div class="info-item">
<span class="label">预估积分</span>
<span class="value text-brand">{{ item.score }} 分</span>
</div>
<div v-if="item.type === 'task'" class="info-item">
<span class="label">数量</span>
<span class="value">{{ item.data.quantity || '--' }} 个</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-details">
<div class="detail-item">
<span class="icon">
<Icon name="hash" size="sm" color="#31f08b" />
</span>
<span>任务编号: {{ item.taskNo }}</span>
</div>
<div v-if="item.publisherName" class="detail-item">
<span class="icon">
<Icon name="user" size="sm" color="#31f08b" />
</span>
<span>发起人: {{ item.publisherName || '未知' }}</span>
</div>
<div v-if="item.publisherDeptName" class="detail-item">
<span class="icon">
<Icon name="users" size="sm" color="#31f08b" />
</span>
<span>发起部门: {{ item.publisherDeptName }}</span>
</div>
<template v-if="item.type === 'task'">
<div v-if="item.data.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ item.data.brandName }}</span>
</div>
<div v-if="item.data.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ item.data.categoryName }}</span>
</div>
<div v-if="item.data.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ item.data.platformName }}</span>
</div>
<div v-if="item.data.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ item.data.spuInfo }}</span>
</div>
<div v-if="item.data.remark" class="detail-item">
<span class="icon">
<Icon name="message-square" size="sm" color="#31f08b" />
</span>
<span>备注: {{ item.data.remark }}</span>
</div>
<div v-if="item.data.attachmentUrl" class="detail-item">
<span class="icon">
<Icon name="paperclip" size="sm" color="#31f08b" />
</span>
<span>附件:
<a
v-for="(url, index) in (item.data.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="item.deadline" class="detail-item">
<span class="icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span>截止: {{ formatDeadline(item.deadline) }}</span>
</div>
<div v-if="item.createTime" class="detail-item">
<span class="icon">
<Icon name="calendar" size="sm" color="#31f08b" />
</span>
<span>发起时间: {{ formatTime(item.createTime) }}</span>
</div>
</template>
</div>
<!-- 审核操作按钮 -->
<div class="task-actions" @click.stop>
<button
v-if="item.type === 'task'"
class="btn btn-secondary"
style="flex: 1"
@click.stop="handleTaskClick(item.data)"
>
查看详情
</button>
<button
class="btn btn-primary"
style="flex: 1"
@click="item.type === 'task' ? handleApproveTwoLevelTask(item.data) : handleApproveTaskGroup(item)"
:disabled="approvalLoading"
>
<Icon name="check" size="sm" style="margin-right: 4px;" />
审核通过
</button>
<button
class="btn btn-danger"
style="flex: 1"
@click="item.type === 'task' ? handleRejectTwoLevelTask(item.data) : handleRejectTaskGroup(item)"
:disabled="approvalLoading"
>
<Icon name="x" size="sm" style="margin-right: 4px;" />
驳回
</button>
</div>
</div>
<!-- 空状态 -->
<a-empty
v-if="!loading && pendingApprovalTasks.length === 0"
description="暂无待审核任务"
style="margin-top: 60px;"
>
<template #image>
<Icon name="inbox" size="xxl" color="#d9d9d9" />
</template>
</a-empty>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" tip="加载中..." />
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 任务详情抽屉 -->
<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="预估工时">{{ selectedTask.estimatedHours || '--' }} 小时</a-descriptions-item>
<a-descriptions-item v-if="!selectedTask.isSubTask" label="数量">{{ selectedTask.quantity || '--' }} 个</a-descriptions-item>
<a-descriptions-item label="截止时间">{{ formatDeadline(selectedTask.deadline) }}</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">
<a v-for="(url, index) in (selectedTask.attachmentUrl || '').split(',')"
:key="index"
:href="url.trim()"
target="_blank"
class="attachment-link">
<Icon name="download" size="sm" style="margin-right: 4px;" />
附件 {{ index + 1 }}
</a>
</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 && canApproveTask(selectedTask)"
type="primary"
size="large"
block>
评分
</a-button>
<!-- 其他状态:仅显示关闭按钮 -->
<a-button
v-else
size="large"
block
@click="detailDrawerVisible = false">
关闭
</a-button>
</div>
</div>
</a-drawer>
</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 * as taskApi from '@/api/task'
import * as userApi from '@/api/user'
import type { Task, TaskApprovalDTO } from '@/api/task'
import type { DesignerWithWorkload } from '@/api/user'
import { useConfigStore } from '@/stores/config'
import dayjs from 'dayjs'
// 扩展Task类型以支持子任务字段
interface TaskOrSubTask extends Task {
isSubTask?: boolean // 是否为子任务
itemId?: number // 子任务ID
subTaskNo?: string // 子任务编号
groupNo?: number // 子任务组号
taskDescription?: string // 主任务描述
submitTime?: string // 提交时间
}
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
}
})
// 状态
const loading = ref(false)
const taskList = ref<TaskOrSubTask[]>([])
const total = ref(0)
const detailDrawerVisible = ref(false)
const selectedTask = ref<TaskOrSubTask | null>(null)
// 已完成任务状态
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 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 filters = reactive({
taskTypeId: undefined as number | undefined,
demandTypeId: undefined as number | undefined,
brandId: undefined as number | undefined,
categoryId: undefined as number | undefined,
platformId: undefined as number | undefined,
taskSubType: undefined as string | 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'
})
// 已完成任务筛选条件
const completedFilters = reactive({
keyword: undefined as string | undefined,
sortBy: 'latest' as 'latest' | 'score',
taskTypeId: undefined as number | undefined,
demandTypeId: 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 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 inProgressFilters = reactive({
keyword: undefined as string | undefined,
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,
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 pagination = reactive({
page: 1,
pageSize: 12
})
// 已完成任务分页
const completedPagination = reactive({
page: 1,
pageSize: 12
})
// 获取任务类型名称
const getTaskTypeName = (typeId: number) => {
const type = demandTypes.value.find(t => t.id === typeId)
return type?.name || '未知类型'
}
// 格式化时间
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 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')})`
}
}
/**
* 获取任务流程阶段
* @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 ''
}
// 判断是否为新发布任务(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
}
// 加载配置数据(从全局store加载)
const loadConfigs = async () => {
try {
await configStore.loadAllConfigs()
} catch (error) {
console.error('加载配置数据失败:', error)
}
}
// 加载任务列表(同时加载普通任务和子任务)
const loadTasks = async () => {
loading.value = true
try {
const params: any = {
page: pagination.page,
pageSize: pagination.pageSize
}
// 添加筛选条件
if (filters.taskTypeId) params.taskTypeId = filters.taskTypeId
if (filters.demandTypeId) params.demandTypeId = filters.demandTypeId
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
// 同时获取普通任务和子任务
const [taskResponse, itemResponse] = await Promise.all([
taskApi.getTaskHall(params),
taskApi.getAvailableTaskItems(params)
])
let allTasks: any[] = []
let totalCount = 0
// 处理普通任务
if (taskResponse.code === 200) {
const tasks = (taskResponse.data.records || []).map((task: any) => ({
...task,
isSubTask: false // 标记为普通任务
}))
allTasks = [...allTasks, ...tasks]
totalCount += taskResponse.data.total || 0
}
// 处理子任务
if (itemResponse.code === 200) {
const items = (itemResponse.data.records || []).map((item: any) => ({
// 将子任务字段映射到任务字段
...item,
id: item.id, // 保持子任务的ID
itemId: item.id, // 额外保存子任务ID
title: item.taskTitle || item.title, // 使用主任务标题
subTaskNo: item.itemNo, // 子任务编号
isSubTask: true, // 标记为子任务
taskId: item.taskId, // 保存主任务ID
// 保留子任务的其他字段
groupNo: item.groupNo,
taskTypeId: item.taskTypeId,
score: item.score,
estimatedHours: item.estimatedHours,
description: item.description || item.taskDescription
}))
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
}
// 按创建时间排序
allTasks.sort((a, b) => {
const timeA = new Date(a.createTime || 0).getTime()
const timeB = new Date(b.createTime || 0).getTime()
return timeB - timeA // 降序
})
taskList.value = allTasks
total.value = totalCount
} catch (error) {
console.error('加载任务失败:', error)
message.error('加载任务失败')
} finally {
loading.value = false
}
}
// 筛选变更
const handleFilterChange = () => {
pagination.page = 1
loadTasks()
}
// 重置筛选
const handleReset = () => {
filters.taskTypeId = undefined
filters.demandTypeId = 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()
}
// 页码变更
const handlePageChange = () => {
loadTasks()
}
// 加载已完成任务列表
const loadCompletedTasks = async () => {
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
const response = await taskApi.getCompletedTasks(params)
completedTaskList.value = response.data.records || []
completedTotal.value = response.data.total || 0
} 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 === 'pendingApproval') {
loadPendingApprovalTasks()
} else {
loadTasks()
}
}
// ==================== 审核相关方法 ====================
// 加载待审核任务(包含主任务和子任务组)
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_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_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 showAssignDesignerModal = async (task: Task) => {
try {
// 获取当前部门的设计师列表
const userInfo = localStorage.getItem('userInfo')
let deptId: number | undefined
if (userInfo) {
const user = JSON.parse(userInfo)
deptId = user.deptId
}
// 加载设计师列表及工作量
const designers = await userApi.getDesignersWithWorkload(deptId)
if (designers.length === 0) {
message.warning('当前部门没有可用的设计师')
return
}
let assignType = 'auto' // 'auto' 或 'manual'
let selectedDesignerId: number | null = null
// 解析任务类型
const taskTypeIdsStr = task.taskTypeIds || task.taskTypeId?.toString() || ''
const taskTypeIdArray = taskTypeIdsStr.split(',').filter(id => id.trim())
// 初始化任务类型列表(带数量)
let editableTaskTypes = taskTypeIdArray.map(id => ({
typeId: parseInt(id.trim()),
quantity: task.quantity || 1
}))
let newTaskTypeId: number | null = null
const modalContent = h('div', { style: 'padding: 10px 0;' }, [
// 任务类型编辑区域
h('div', {
style: {
marginBottom: '24px',
padding: '16px',
background: '#fafafa',
borderRadius: '8px',
border: '1px solid #e8e8e8'
}
}, [
h('div', { style: 'marginBottom: 16px; display: flex; alignItems: center;' }, [
h('span', {
style: {
fontSize: '14px',
fontWeight: 600,
color: '#1890ff',
marginRight: '8px'
}
}, '📝'),
h('span', {
style: {
fontSize: '14px',
fontWeight: 600,
color: '#262626'
}
}, '编辑任务类型')
]),
h(Space, {
direction: 'vertical',
size: 8,
style: { width: '100%', marginBottom: '12px' }
}, [
...editableTaskTypes.map((item, index) =>
h('div', {
key: index,
style: {
display: 'flex',
alignItems: 'center',
padding: '12px',
background: 'white',
borderRadius: '6px',
border: '1px solid #e8e8e8',
transition: 'all 0.3s'
}
}, [
h(Tag, {
color: 'blue',
style: {
marginRight: '12px',
fontSize: '13px',
padding: '4px 12px'
}
}, getTaskTypeName(item.typeId)),
h('span', {
style: {
flex: 1,
fontSize: '13px',
color: '#8c8c8c'
}
}),
h('span', {
style: {
marginRight: '8px',
fontSize: '13px',
color: '#595959'
}
}, '数量:'),
h(InputNumber, {
value: item.quantity,
min: 1,
size: 'small',
style: { width: '80px', marginRight: '8px' },
onChange: (val: number) => {
item.quantity = val || 1
}
}),
h(Button, {
type: 'text',
danger: true,
size: 'small',
onClick: () => {
editableTaskTypes.splice(index, 1)
}
}, '删除')
])
)
]),
h(Space, { size: 8, style: { width: '100%' } }, [
h(Select, {
value: newTaskTypeId,
placeholder: '选择要添加的任务类型',
allowClear: true,
style: { flex: 1, minWidth: '200px' },
onChange: (val: number) => {
newTaskTypeId = val
},
options: demandTypes.value.map((type: any) => ({
value: type.id,
label: type.name
}))
}),
h(Button, {
type: 'primary',
size: 'middle',
style: { background: '#52c41a', borderColor: '#52c41a' },
onClick: () => {
if (newTaskTypeId && !editableTaskTypes.find(t => t.typeId === newTaskTypeId)) {
editableTaskTypes.push({ typeId: newTaskTypeId, quantity: 1 })
newTaskTypeId = null
} else if (newTaskTypeId) {
message.warning('该任务类型已添加')
} else {
message.warning('请先选择任务类型')
}
}
}, '+ 添加类型')
])
]),
// 选择接单方式
h('div', {
style: {
marginBottom: '24px'
}
}, [
h('div', {
style: {
marginBottom: '12px',
fontSize: '14px',
fontWeight: 500,
color: '#262626'
}
}, '选择任务接单方式'),
h(RadioGroup, {
value: assignType,
onChange: (e: any) => {
assignType = e.target.value
},
style: { width: '100%' }
}, [
h(Radio, {
value: 'auto',
style: {
display: 'block',
marginBottom: '12px',
padding: '10px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
background: assignType === 'auto' ? '#e6f7ff' : 'white',
transition: 'all 0.3s'
}
}, '🏢 发布到任务大厅(设计师自由抢单)'),
h(Radio, {
value: 'manual',
style: {
display: 'block',
padding: '10px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
background: assignType === 'manual' ? '#e6f7ff' : 'white',
transition: 'all 0.3s'
}
}, '👤 指定设计师接单')
])
]),
// 设计师列表(仅在指定接单时显示)
h('div', {
style: {
display: assignType === 'manual' ? 'block' : 'none'
}
}, [
h('div', {
style: {
marginBottom: '12px',
fontSize: '14px',
fontWeight: 500,
color: '#262626'
}
}, '选择设计师'),
h(RadioGroup, {
value: selectedDesignerId,
onChange: (e: any) => {
selectedDesignerId = e.target.value
},
style: { width: '100%' }
}, designers.map(designer =>
h(Radio, {
value: designer.id,
style: {
display: 'flex',
alignItems: 'center',
marginBottom: '10px',
padding: '12px',
border: '1px solid #e8e8e8',
borderRadius: '6px',
background: selectedDesignerId === designer.id ? '#e6f7ff' : 'white',
transition: 'all 0.3s'
}
}, [
h('span', { style: { flex: 1, fontSize: '14px', fontWeight: 500 } }, designer.realName),
h(Tag, {
color: designer.inProgressCount > 5 ? 'error' :
designer.inProgressCount > 3 ? 'warning' : 'success'
}, `进行中: ${designer.inProgressCount}`)
])
))
])
])
Modal.confirm({
title: '✅ 接单部门审核通过',
content: modalContent,
width: 650,
okText: '确认通过',
cancelText: '取消',
okButtonProps: {
size: 'large',
style: { minWidth: '100px' }
},
cancelButtonProps: {
size: 'large',
style: { minWidth: '100px' }
},
onOk: async () => {
try {
approvalLoading.value = true
// 检查是否需要指定设计师
if (assignType === 'manual' && !selectedDesignerId) {
message.error('请选择要指定的设计师')
return Promise.reject()
}
// 检查任务类型是否为空
if (editableTaskTypes.length === 0) {
message.error('至少需要一个任务类型')
return Promise.reject()
}
// 准备任务类型更新数据
const approvalData: TaskApprovalDTO = {
comment: assignType === 'manual' ?
`指定设计师接单` : '审核通过,发布到任务大厅',
assignedReceiverId: assignType === 'manual' && selectedDesignerId ? selectedDesignerId : undefined,
taskUpdates: {
taskTypes: editableTaskTypes.map(t => ({
taskTypeId: t.typeId,
quantity: t.quantity
}))
}
}
await taskApi.approveReceivingLevel(task.id!, approvalData)
if (assignType === 'manual') {
const designer = designers.find(d => d.id === selectedDesignerId)
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 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
// 根据当前审核角色调用不同的API
if (task.currentApproverRole === 'demand_leader') {
await taskApi.rejectByDemandLeader(task.id!, rejectReason.trim())
message.success('已驳回,任务已退回给发起人')
} else if (task.currentApproverRole === 'receiving_leader') {
await taskApi.rejectByReceivingLeader(task.id!, rejectReason.trim())
message.success('已驳回,任务已退回给需求部门负责人')
}
// 刷新列表和数量
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
await taskApi.approveTaskGroup(item.id, {
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.id, rejectReason.trim())
message.success('已驳回子任务组')
// 刷新列表和数量
loadPendingApprovalTasks()
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 inProgressPagination = reactive({
page: 1,
pageSize: 12
})
// 待审核任务分页
const reviewPagination = reactive({
page: 1,
pageSize: 12
})
// 加载我发起的任务
const loadPublishedTasks = async () => {
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 handleCompletedFiltersReset = () => {
completedFilters.keyword = undefined
completedFilters.sortBy = 'latest'
completedFilters.taskTypeId = undefined
completedFilters.demandTypeId = 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 response = await taskApi.getDashboardStats(month ? { month } : 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 loadInProgressTasks = async () => {
inProgressLoading.value = true
try {
const params: any = {
page: inProgressPagination.page,
pageSize: inProgressPagination.pageSize,
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
// 同时获取普通任务和子任务
const [taskResponse, itemResponse] = await Promise.all([
taskApi.getTasks(params),
taskApi.getInProgressTaskItems(params)
])
let allTasks: any[] = []
let totalCount = 0
// 处理普通任务
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) => ({
// 将子任务字段映射到任务字段
...item,
id: item.id, // 保持子任务的ID
itemId: item.id, // 额外保存子任务ID
title: item.taskTitle || item.title, // 使用主任务标题
subTaskNo: item.itemNo, // 子任务编号
isSubTask: true, // 标记为子任务
taskId: item.taskId, // 保存主任务ID
// 保留子任务的其他字段
groupNo: item.groupNo,
taskTypeId: item.taskTypeId,
score: item.score,
estimatedHours: item.estimatedHours,
description: item.description || item.taskDescription,
progress: item.progress,
status: 'in_progress'
}))
allTasks = [...allTasks, ...items]
totalCount += itemResponse.data.total || 0
}
// 按更新时间排序
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 loadReviewTasks = async () => {
reviewLoading.value = true
try {
const params: any = {
page: reviewPagination.page,
pageSize: reviewPagination.pageSize
}
// 添加筛选条件
if (reviewFilters.taskTypeId) params.taskTypeId = reviewFilters.taskTypeId
// 调用新接口:获取所有处于审批流程中的任务
const response = await taskApi.getAllPendingApprovalTasks(params)
let tasks = response.data?.records || []
reviewTotal.value = response.data?.total || 0
// 前端筛选(因为新接口只支持taskTypeId筛选)
if (reviewFilters.keyword) {
const keyword = reviewFilters.keyword.trim().toLowerCase()
tasks = tasks.filter((task: Task) =>
task.title?.toLowerCase().includes(keyword) ||
task.taskNo?.toLowerCase().includes(keyword) ||
task.spuInfo?.toLowerCase().includes(keyword) ||
task.publisherName?.toLowerCase().includes(keyword)
)
}
if (reviewFilters.brandId) {
tasks = tasks.filter((task: Task) => task.brandId === reviewFilters.brandId)
}
if (reviewFilters.categoryId) {
tasks = tasks.filter((task: Task) => task.categoryId === reviewFilters.categoryId)
}
if (reviewFilters.platformId) {
tasks = tasks.filter((task: Task) => task.platformId === reviewFilters.platformId)
}
if (reviewFilters.minScore !== undefined) {
tasks = tasks.filter((task: Task) => (task.score || 0) >= reviewFilters.minScore!)
}
if (reviewFilters.maxScore !== undefined) {
tasks = tasks.filter((task: Task) => (task.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')
}
tasks = tasks.filter((task: Task) =>
task.createTime && dayjs(task.createTime).isAfter(startTime)
)
}
// 排序
if (reviewFilters.sortBy === 'score') {
tasks.sort((a: Task, b: Task) => (b.score || 0) - (a.score || 0))
} else {
tasks.sort((a: Task, b: Task) =>
dayjs(b.createTime || 0).valueOf() - dayjs(a.createTime || 0).valueOf()
)
}
reviewTaskList.value = tasks
reviewTotal.value = tasks.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 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 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 handleEditRejectedTask = (task: Task) => {
// 跳转到创建任务页面,并传入任务ID作为编辑标识
router.push({
path: '/tasks/create',
query: {
editId: task.id,
mode: 'edit'
}
})
}
const handleCompleteTask = (task: TaskOrSubTask) => {
let deliveryPath = ''
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;' }, '交付路径:'),
h(Textarea, {
rows: 4,
placeholder: '请输入交付路径或说明',
style: 'width: 100%',
onChange: (e: any) => { deliveryPath = e.target.value }
})
]),
onOk: async () => {
if (!deliveryPath.trim()) {
message.error('请输入交付路径')
return Promise.reject()
}
try {
if (isSubTask) {
// 完结子任务
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) {
message.error(error.message || '任务完结失败')
}
}
})
}
// 审核通过(使用两级审核流程)
const handleApproveTask = (task: Task) => {
// 如果是接单部门负责人审核,需要提供选择指派设计师的功能
if (task.currentApproverRole === 'receiving_leader') {
handleApproveTwoLevelTask(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 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 {
// 根据当前审核角色调用不同的API
if (task.currentApproverRole === 'demand_leader') {
await taskApi.rejectByDemandLeader(task.id!, reason)
message.success('需求部门已驳回')
} else if (task.currentApproverRole === 'receiving_leader') {
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 = (task: Task) => {
selectedTask.value = task
detailDrawerVisible.value = true
}
// 任务点击
const handleTaskClick = (task: TaskOrSubTask) => {
selectedTask.value = task
detailDrawerVisible.value = true
}
// 判断用户是否可以抢单
const canGrabTask = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const userDeptId = userInfo.deptId
const userDeptType = userInfo.deptType // 'demand' 或 'receiving'
// 检查1:没有部门不能抢单
if (!userDeptId) {
return { canGrab: false, reason: '您未分配部门,无法接单' }
}
// 检查2:需求部门不能抢单
if (userDeptType === 'demand') {
return { canGrab: false, reason: '需求部门成员不能接单' }
}
// 检查3:部门负责人不能抢单
if (userInfo.isDepartmentLeader === 1 || userInfo.isDepartmentLeader === true) {
return { canGrab: false, reason: '部门负责人不能接单' }
}
// 检查4:验证部门是否允许接该需求类型(如果任务有需求类型限制)
if (task.demandTypeId) {
// 这里需要检查任务的需求类型是否允许当前用户的部门接单
// 由于前端没有完整的需求类型配置,暂时信任后端验证
}
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() // 刷新列表
loadAllTabCounts() // 刷新所有Tab数量
}
} catch (error: any) {
console.error('抢单失败:', error)
message.error(error.message || '抢单失败')
}
}
})
}
// 从详情抽屉抢单
const handleGrabFromDetail = async () => {
if (!selectedTask.value) return
await handleGrab(selectedTask.value)
detailDrawerVisible.value = false
}
// 监听配置更新事件
const handleConfigUpdate = async (event: CustomEvent) => {
console.log('配置已更新,刷新配置数据:', event.detail)
await loadConfigs()
}
// 组件挂载
// 获取任务状态文本
const getTaskStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'pending': '待接单',
'in_progress': '进行中',
'review': '待审核',
'completed': '已完成',
'rejected': '已驳回',
'cancelled': '已取消'
}
return statusMap[status] || status
}
// 加载所有Tab的数量统计
const loadAllTabCounts = async () => {
try {
// 加载进行中任务数量
const inProgressRes = await taskApi.getTasks({
status: 'in_progress',
role: 'receiver',
page: 1,
pageSize: 1
})
if (inProgressRes.data && inProgressRes.data.total !== undefined) {
inProgressTotal.value = inProgressRes.data.total
}
// 加载待审核任务数量
const reviewRes = await taskApi.getAllPendingApprovalTasks({
page: 1,
pageSize: 1
})
if (reviewRes.data && reviewRes.data.total !== undefined) {
reviewTotal.value = reviewRes.data.total
}
// 加载待我审核任务数量(仅部门负责人)
if (isDepartmentLeader.value) {
const pendingApprovalRes = await taskApi.getPendingApprovalTasks()
if (pendingApprovalRes.code === 200) {
pendingApprovalTotal.value = (pendingApprovalRes.data || []).length
}
}
} catch (error) {
console.error('加载Tab数量统计失败:', error)
}
}
onMounted(() => {
loadConfigs()
loadDashboardStats()
loadAllTabCounts() // 加载所有Tab的数量统计
loadTasks()
// 监听配置更新事件
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;
justify-content: flex-end;
margin-bottom: $space-2xl;
button {
min-width: 140px;
}
}
// 图表卡片 - 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);
}
// 审核任务卡片特殊样式
.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-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
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;
flex: 1;
}
.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(24, 144, 255, 0.1);
border: 1px solid rgba(24, 144, 255, 0.3);
color: #1890ff;
}
.badge-new {
background: rgba($color-success, 0.1);
border: 1px solid rgba($color-success, 0.3);
color: $color-success;
}
.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-completed {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
border: 1px solid rgba(49, 240, 139, 0.2);
}
// 任务信息
.task-info {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $space-md;
margin-bottom: $space-md;
padding-bottom: $space-md;
border-bottom: 1px solid $border-primary;
}
.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;
}
}
// 任务详情
.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;
}
}
.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: 8px;
flex-wrap: wrap;
white-space: normal;
overflow: visible;
.stat {
padding: 2px 8px;
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(24, 144, 255, 0.1);
color: #1890ff;
}
&.pending {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
}
&.total {
color: #999;
padding: 0;
}
}
}
}
}
// 任务操作
.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;
}
}
}
// 分页
.pagination {
display: flex;
justify-content: center;
margin-top: $space-xl;
}
// 任务进度条
.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;
margin-top: $space-md;
button {
flex: 1;
}
}
// 任务卡片特定样式 - 统一简洁风格,不使用彩色边框
.task-card.in-progress,
.task-card.review,
.task-card.approval-card {
// 移除彩色边框,保持统一灰色底色
}
// badge样式补充 - 统一使用绿色主题
.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;
}
}
// 内联附件链接样式
.task-details {
.attachment-link {
color: #31f08b;
text-decoration: none;
margin-left: 4px;
margin-right: 8px;
&:hover {
text-decoration: underline;
}
}
}
</style>