Your Name 3b9fb61312 修复待审核任务审核API调用和Tab数量显示问题
1. 修复TaskHall.vue中handleApproveTask和handleRejectTask使用错误的API
   - 改为使用两级审核API:approveDemandLevel/approveReceivingLevel
   - 改为使用rejectByDemandLeader/rejectByReceivingLeader

2. 修复Tab标签数量显示为0的问题
   - 新增loadAllTabCounts函数在页面初始化时加载所有Tab数量
   - 在任务操作后刷新Tab数量统计

3. 优化通知图标类型映射
   - 支持大小写不敏感的通知类型匹配
   - 支持task_approval等下划线格式的通知类型
2026-01-02 18:42:00 +08:00

3695 lines
120 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="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 btn-primary"
style="flex: 1"
@click.stop="handleGrab(task)"
>
抢单
</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 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>
<!-- 时间信息 -->
<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 class="detail-item">
<span class="icon">
<Icon name="trending-up" size="sm" color="#31f08b" />
</span>
<span>进度: {{ task.progress || 0 }}%</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-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="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="task in pendingApprovalTasks"
:key="task.id"
class="task-card chart-card approval-card"
@click="handleTaskClick(task)"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span class="badge badge-approval">
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
{{ getApprovalStatusText(task.approvalStatus) }}
</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 class="detail-item">
<span class="icon">
<Icon name="hash" size="sm" color="#31f08b" />
</span>
<span>任务编号: {{ task.taskNo }}</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.publisherDeptName" class="detail-item">
<span class="icon">
<Icon name="users" size="sm" color="#31f08b" />
</span>
<span>发起部门: {{ task.publisherDeptName }}</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.remark" class="detail-item">
<span class="icon">
<Icon name="message-square" size="sm" color="#31f08b" />
</span>
<span>备注: {{ task.remark }}</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 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" @click.stop>
<button
class="btn btn-secondary"
style="flex: 1"
@click.stop="handleTaskClick(task)"
>
查看详情
</button>
<button
class="btn btn-primary"
style="flex: 1"
@click="handleApproveTwoLevelTask(task)"
:disabled="approvalLoading"
>
<Icon name="check" size="sm" style="margin-right: 4px;" />
审核通过
</button>
<button
class="btn btn-danger"
style="flex: 1"
@click="handleRejectTwoLevelTask(task)"
: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="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.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 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="primary"
size="large"
block
@click="handleGrabFromDetail">
确认抢单
</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, Input } from 'ant-design-vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import Icon from '@/components/Icon.vue'
import * as taskApi from '@/api/task'
import * as configApi from '@/api/config'
import type { Task, TaskApprovalDTO } from '@/api/task'
import type { DemandType, Brand, Platform, Category } from '@/api/config'
import { useConfigStore } from '@/stores/config'
import dayjs from 'dayjs'
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<Task[]>([])
const total = ref(0)
const detailDrawerVisible = ref(false)
const selectedTask = ref<Task | 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)
// 待我审核任务状态
const pendingApprovalTasks = ref<Task[]>([])
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: Task): 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' || task.status === 'cancelled') {
return '已取消'
}
// 默认返回空字符串
return ''
}
// 判断是否为新发布任务(1小时内)
const isNewTask = (task: Task) => {
if (!task.createTime) return false
const createTime = dayjs(task.createTime)
const now = dayjs()
return now.diff(createTime, 'hour') < 1
}
// 判断是否为高分任务(积分 >= 500)
const isHighScoreTask = (task: Task) => {
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 response = await taskApi.getTaskHall(params)
if (response.code === 200) {
taskList.value = response.data.records || []
total.value = response.data.total || 0
}
} 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 response = await taskApi.getPendingApprovalTasks()
if (response.code === 200) {
pendingApprovalTasks.value = response.data || []
// 如果有搜索关键词,进行本地过滤
if (approvalFilters.keyword && approvalFilters.keyword.trim()) {
const keyword = approvalFilters.keyword.trim().toLowerCase()
pendingApprovalTasks.value = pendingApprovalTasks.value.filter(task =>
task.title?.toLowerCase().includes(keyword) ||
task.taskNo?.toLowerCase().includes(keyword)
)
}
pendingApprovalTotal.value = pendingApprovalTasks.value.length
} else {
message.error(response.msg || '加载待审核任务失败')
}
} 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) => {
Modal.confirm({
title: '确认审核通过',
content: `确定通过任务"${task.title}"的审核吗?`,
okText: '确认通过',
cancelText: '取消',
onOk: async () => {
try {
approvalLoading.value = true
const approvalData: TaskApprovalDTO = {
comment: '审核通过'
}
// 根据当前审核角色调用不同的API
if (task.currentApproverRole === 'demand_leader') {
await taskApi.approveDemandLevel(task.id!, approvalData)
message.success('需求部门审核通过')
} else if (task.currentApproverRole === 'receiving_leader') {
await taskApi.approveReceivingLevel(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 handleRejectTwoLevelTask = async (task: Task) => {
let rejectReason = ''
Modal.confirm({
title: '驳回任务',
content: h('div', { style: 'padding: 10px 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
// 根据当前审核角色调用不同的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 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 response = await taskApi.getTasks(params)
inProgressTaskList.value = response.data.records || []
inProgressTotal.value = response.data.total || 0
} 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: Task) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
return task.receiverId === userInfo.id
}
// 判断是否可以完结任务(接单人)
const canCompleteTask = (task: Task) => {
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: Task) => {
let newProgress = task.progress || 0
Modal.confirm({
title: '更新任务进度',
content: h('div', [
h('div', { style: 'margin-bottom: 12px;' }, `任务:${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 {
await taskApi.updateTaskProgress(task.id, newProgress)
message.success('进度更新成功')
loadInProgressTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.message || '进度更新失败')
}
}
})
}
// 完结任务
const handleCompleteTask = (task: Task) => {
let deliveryPath = ''
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) => { deliveryPath = e.target.value }
})
]),
onOk: async () => {
if (!deliveryPath.trim()) {
message.error('请输入交付路径')
return Promise.reject()
}
try {
await taskApi.completeTask(task.id, { deliveryPath })
message.success('任务已提交审核')
loadInProgressTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.message || '任务完结失败')
}
}
})
}
// 审核通过(使用两级审核流程)
const handleApproveTask = (task: Task) => {
Modal.confirm({
title: '审核通过',
content: `确认通过任务审核:${task.title}?`,
onOk: async () => {
try {
const approvalData: TaskApprovalDTO = {
comment: '审核通过'
}
// 根据当前审核角色调用不同的API
if (task.currentApproverRole === 'demand_leader') {
await taskApi.approveDemandLevel(task.id!, approvalData)
message.success('需求部门审核通过')
} else if (task.currentApproverRole === 'receiving_leader') {
await taskApi.approveReceivingLevel(task.id!, approvalData)
message.success('接单部门审核通过,任务已发布到大厅')
} else {
message.error('无法确定审核角色')
return
}
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: Task) => {
selectedTask.value = task
detailDrawerVisible.value = true
}
// 抢单
const handleGrab = async (task: Task) => {
Modal.confirm({
title: '确认抢单',
content: `确定要抢单"${task.title}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
const response = await taskApi.grabTask(task.id!)
if (response.code === 200) {
message.success('抢单成功!')
loadTasks() // 刷新列表
}
} 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-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;
}
}
}
// 任务操作
.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>