qiangdanxitong/frontend/src/views/TaskHall.vue.backup

2822 lines
91 KiB
Plaintext
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>
<a-row :gutter="[16, 16]">
<a-col
v-for="card in dataCards"
:key="card.id"
:xs="24"
:sm="12"
:md="8"
:lg="4"
>
<div 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>
</a-col>
</a-row>
</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="#F59E0B" style="margin-right: 4px; vertical-align: middle;" />
高分
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#F59E0B" style="margin-right: 4px; vertical-align: middle;" />
紧急
</span>
</div>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务类型</span>
<span class="value">{{ 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 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="inProgress" tab="进行中任务">
<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="#F59E0B" style="margin-right: 4px; vertical-align: middle;" />
进行中
</span>
</div>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.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="info-item">
<span class="label">发起人</span>
<span class="value">{{ task.publisherName || '--' }}</span>
</div>
<div class="info-item">
<span class="label">接单人</span>
<span class="value">{{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
</div>
<!-- 进度条 -->
<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="review" tab="待审核任务">
<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="#3B82F6" style="margin-right: 4px; vertical-align: middle;" />
待审核
</span>
</div>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.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="info-item">
<span class="label">发起人</span>
<span class="value">{{ task.publisherName || '--' }}</span>
</div>
<div class="info-item">
<span class="label">接单人</span>
<span class="value">{{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.deliveryPath" class="detail-item">
<span class="icon">
<Icon name="link" size="sm" color="#31f08b" />
</span>
<span>交付路径: {{ task.deliveryPath }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div 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="completed" tab="已完成任务">
<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 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="info-item">
<span class="label">发起人</span>
<span class="value">{{ task.publisherName || '--' }}</span>
</div>
<div class="info-item">
<span class="label">完成人</span>
<span class="value">{{ task.receiverName || '--' }}</span>
</div>
<div v-if="task.brandName" class="detail-item">
<span class="icon">
<Icon name="tag" size="sm" color="#31f08b" />
</span>
<span>品牌: {{ task.brandName }}</span>
</div>
<div v-if="task.categoryName" class="detail-item">
<span class="icon">
<Icon name="folder" size="sm" color="#31f08b" />
</span>
<span>类目: {{ task.categoryName }}</span>
</div>
<div v-if="task.platformName" class="detail-item">
<span class="icon">
<Icon name="monitor" size="sm" color="#31f08b" />
</span>
<span>平台: {{ task.platformName }}</span>
</div>
<div v-if="task.spuInfo" class="detail-item">
<span class="icon">
<Icon name="package" size="sm" color="#31f08b" />
</span>
<span>SPU: {{ task.spuInfo }}</span>
</div>
<div v-if="task.taskSubType" class="detail-item">
<span class="icon">
<Icon name="list" size="sm" color="#31f08b" />
</span>
<span>子类型: {{ task.taskSubType }}</span>
</div>
<div v-if="task.deliveryPath" class="detail-item">
<span class="icon">
<Icon name="link" size="sm" color="#31f08b" />
</span>
<span>交付: {{ task.deliveryPath }}</span>
</div>
</div>
<!-- 时间信息 -->
<div 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" tab="我发起的任务">
<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="#F59E0B" style="margin-right: 4px; vertical-align: middle;" />
紧急
</span>
</div>
</div>
<!-- 任务信息 -->
<div class="task-info">
<div class="info-item">
<span class="label">任务编号</span>
<span class="value">{{ task.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="info-item">
<span class="label">接单人</span>
<span class="value">{{ task.receiverName || '--' }}</span>
</div>
<div class="info-item">
<span class="label">进度</span>
<span class="value">{{ 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>
<!-- 时间信息 -->
<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-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 } 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 } 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')
// 状态
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 dataCards = ref([
{ id: 1, label: '可接任务数', value: 0, iconName: 'inbox' },
{ 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: 'plus-circle' },
{ id: 6, 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')})`
}
}
// 判断是否为新发布任务(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 {
loadTasks()
}
}
// 已完成任务分页变化
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.availableCount || 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.todayNewCount || 0 // 当日新增任务
dataCards.value[5].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,
status: 'review'
}
// 添加筛选条件
if (reviewFilters.keyword) params.keyword = reviewFilters.keyword
if (reviewFilters.sortBy) params.sortBy = reviewFilters.sortBy
if (reviewFilters.taskTypeId) params.taskTypeId = reviewFilters.taskTypeId
if (reviewFilters.brandId) params.brandId = reviewFilters.brandId
if (reviewFilters.categoryId) params.categoryId = reviewFilters.categoryId
if (reviewFilters.platformId) params.platformId = reviewFilters.platformId
if (reviewFilters.timeRange) params.timeRange = reviewFilters.timeRange
if (reviewFilters.minScore !== undefined) params.minScore = reviewFilters.minScore
if (reviewFilters.maxScore !== undefined) params.maxScore = reviewFilters.maxScore
const response = await taskApi.getTasks(params)
reviewTaskList.value = response.data.records || []
reviewTotal.value = response.data.total || 0
} 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') || '{}')
return task.publisherId === userInfo.id || userInfo.roleId === 2 // 2是部门负责人角色
}
// 判断是否可以驳回任务
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()
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()
loadDashboardStats()
} catch (error: any) {
message.error(error.message || '任务完结失败')
}
}
})
}
// 审核通过
const handleApproveTask = (task: Task) => {
Modal.confirm({
title: '审核通过',
content: `确认通过任务审核:${task.title}?`,
onOk: async () => {
try {
await taskApi.approveTask(task.id)
message.success('审核通过')
loadReviewTasks()
loadDashboardStats()
} catch (error: any) {
message.error(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 {
await taskApi.rejectTask(task.id, { reason })
message.success('任务已驳回')
loadReviewTasks()
loadDashboardStats()
} catch (error: any) {
message.error(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
}
onMounted(() => {
loadConfigs()
loadDashboardStats()
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;
}
}
}
.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;
}
}
}
.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;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $space-md;
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($color-warning, 0.1);
border: 1px solid rgba($color-warning, 0.3);
color: $color-warning;
}
.badge-urgent {
background: rgba($color-error, 0.1);
border: 1px solid rgba($color-error, 0.3);
color: $color-error;
}
.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: flex;
flex-direction: column;
gap: $space-sm;
margin-bottom: $space-md;
}
.detail-item {
display: flex;
align-items: center;
gap: $space-sm;
font-size: $font-size-sm;
color: $text-secondary;
.icon {
font-size: $font-size-base;
}
}
// 任务操作
.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 {
border-left: 3px solid #F59E0B;
}
.task-card.review {
border-left: 3px solid #3B82F6;
}
// badge样式补充
.badge-in-progress {
background: rgba(245, 158, 11, 0.1);
color: #F59E0B;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.badge-review {
background: rgba(59, 130, 246, 0.1);
color: #3B82F6;
border: 1px solid rgba(59, 130, 246, 0.2);
}
</style>