优化内容: 1. 组件替换 ✅ 使用 Button 替换原生 button ✅ 使用 Select 替换原生 select ✅ 使用 InputNumber 替换 input[type=number] ✅ 使用 Space 组件优化布局 ✅ 使用 Tag 组件显示标签 2. 任务类型编辑区域 ✅ 改用浅灰背景 (#fafafa) ✅ 边框颜色统一 (#e8e8e8) ✅ 任务类型显示为蓝色 Tag ✅ 使用 InputNumber 组件调整数量 ✅ 删除按钮使用 text + danger 样式 ✅ 添加按钮使用 primary + 绿色样式 3. 接单方式选择 ✅ Radio 选项卡片化设计 ✅ 选中状态浅蓝背景 (#e6f7ff) ✅ 添加图标前缀 (🏢/👤) ✅ 卡片圆角 6px ✅ 过渡动画效果 4. 设计师列表 ✅ 卡片化设计,选中高亮 ✅ 使用 Tag 显示任务数量 ✅ 根据任务量显示不同颜色: - 0-3个:success (绿色) - 4-5个:warning (橙色) - 6+个:error (红色) 5. 弹窗样式 ✅ 标题添加 emoji 图标 ✅ 宽度调整为 650px ✅ 按钮尺寸统一为 large ✅ 按钮最小宽度 100px 6. 驳回弹窗 ✅ 同样优化风格 ✅ 增加内边距 ✅ 文本域高度增加到 5 行 ✅ 统一按钮样式 整体效果: - 与系统其他页面风格一致 - 使用 Ant Design 标准组件 - 视觉层次更清晰 - 交互体验更友好 - 响应式过渡动画
4573 lines
151 KiB
Vue
4573 lines
151 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="task-hall-page">
|
||
<div class="container">
|
||
|
||
<!-- 数据总览 -->
|
||
<div class="overview-section">
|
||
<div class="overview-header">
|
||
<h2 class="section-title">数据总览</h2>
|
||
<div class="overview-actions">
|
||
<a-month-picker
|
||
v-model:value="selectedMonth"
|
||
placeholder="选择月份"
|
||
format="YYYY-MM"
|
||
@change="handleMonthChange"
|
||
style="width: 150px; margin-right: 12px;"
|
||
/>
|
||
<a-button type="primary" size="large" @click="handleCreateTask">
|
||
<Icon name="plus" size="sm" style="margin-right: 6px;" />
|
||
发起任务
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
<div class="stats-grid">
|
||
<div
|
||
v-for="card in dataCards"
|
||
:key="card.id"
|
||
class="stat-card"
|
||
>
|
||
<div class="stat-icon">
|
||
<Icon :name="card.iconName" size="xl" color="#31f08b" />
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">{{ card.label }}</div>
|
||
<div class="stat-value">{{ card.value }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页切换 -->
|
||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange" class="task-tabs">
|
||
<a-tab-pane key="available" tab="可接任务">
|
||
<!-- 搜索和筛选栏(合并) -->
|
||
<div class="search-filter-bar chart-card">
|
||
<!-- 第一行:搜索、排序和统计 -->
|
||
<div class="search-row">
|
||
<a-input-search
|
||
v-model:value="filters.keyword"
|
||
placeholder="搜索任务标题、SPU信息、发布人..."
|
||
class="search-input"
|
||
@search="handleFilterChange"
|
||
@pressEnter="handleFilterChange"
|
||
/>
|
||
<a-select
|
||
v-model:value="filters.sortBy"
|
||
placeholder="排序方式"
|
||
class="sort-select"
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option value="latest">最新发布</a-select-option>
|
||
<a-select-option value="score">积分最高</a-select-option>
|
||
<a-select-option value="deadline">时效最短</a-select-option>
|
||
<a-select-option value="hot">最热门</a-select-option>
|
||
</a-select>
|
||
<span class="stats-text">共 {{ total }} 个任务</span>
|
||
</div>
|
||
|
||
<!-- 第二行:筛选条件 -->
|
||
<div class="filter-row">
|
||
<a-select
|
||
v-model:value="filters.taskTypeId"
|
||
placeholder="任务类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="filters.demandTypeId"
|
||
placeholder="需求类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="filters.brandId"
|
||
placeholder="品牌"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
>
|
||
{{ brand.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="filters.categoryId"
|
||
placeholder="类目"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="filters.platformId"
|
||
placeholder="平台"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
>
|
||
{{ platform.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="filters.timeRange"
|
||
placeholder="发布时间"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="handleFilterChange"
|
||
>
|
||
<a-select-option value="today">今天</a-select-option>
|
||
<a-select-option value="week">本周</a-select-option>
|
||
<a-select-option value="month">本月</a-select-option>
|
||
</a-select>
|
||
|
||
<div class="score-range">
|
||
<a-input
|
||
v-model:value="filters.minScore"
|
||
placeholder="最小积分"
|
||
class="score-input"
|
||
@pressEnter="handleFilterChange"
|
||
/>
|
||
<span class="score-separator">-</span>
|
||
<a-input
|
||
v-model:value="filters.maxScore"
|
||
placeholder="最大积分"
|
||
class="score-input"
|
||
@pressEnter="handleFilterChange"
|
||
/>
|
||
</div>
|
||
|
||
<a-button @click="handleReset" class="reset-btn">重置</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务列表 -->
|
||
<div v-if="loading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
|
||
<div v-else-if="taskList.length === 0" class="empty-state chart-card">
|
||
<div class="empty-icon">
|
||
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||
</div>
|
||
<h3>暂无可接任务</h3>
|
||
<p>当前没有待接单的任务,请稍后再来查看</p>
|
||
</div>
|
||
|
||
<div v-else class="task-grid">
|
||
<div
|
||
v-for="task in taskList"
|
||
:key="task.id"
|
||
class="task-card chart-card"
|
||
@click="handleTaskClick(task)"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span v-if="task.isSubTask" class="badge badge-subtask">
|
||
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
|
||
子任务
|
||
</span>
|
||
<span v-if="isNewTask(task)" class="badge badge-new">
|
||
<Icon name="sparkles" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
新发布
|
||
</span>
|
||
<span v-if="isHighScoreTask(task)" class="badge badge-high-score">
|
||
<Icon name="star" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
高分
|
||
</span>
|
||
<span v-if="task.isUrgent" class="badge badge-urgent">
|
||
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
紧急
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info">
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布人: {{ task.publisherName || '未知' }}</span>
|
||
</div>
|
||
<div v-if="task.attachmentUrl" class="detail-item detail-item-attachment">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>截止: {{ formatDeadline(task.deadline) }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布时间: {{ formatTime(task.createTime) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="task-actions">
|
||
<button
|
||
class="btn btn-secondary"
|
||
style="flex: 1"
|
||
@click.stop="handleTaskClick(task)"
|
||
>
|
||
查看详情
|
||
</button>
|
||
<button
|
||
:class="[
|
||
'btn',
|
||
canGrabTask(task).canGrab ? 'btn-primary' : 'btn-disabled'
|
||
]"
|
||
:style="{
|
||
flex: 1,
|
||
backgroundColor: canGrabTask(task).canGrab ? '#31f08b' : '#666',
|
||
cursor: canGrabTask(task).canGrab ? 'pointer' : 'not-allowed',
|
||
opacity: canGrabTask(task).canGrab ? 1 : 0.6
|
||
}"
|
||
:disabled="!canGrabTask(task).canGrab"
|
||
:title="canGrabTask(task).canGrab ? '点击抢单' : canGrabTask(task).reason"
|
||
@click.stop="handleGrab(task)"
|
||
>
|
||
{{ canGrabTask(task).canGrab ? '抢单' : '不可接单' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="total > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="total"
|
||
:show-size-changer="false"
|
||
@change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 进行中任务标签页 -->
|
||
<a-tab-pane key="review">
|
||
<template #tab>
|
||
<span>待审核任务 <span class="tab-count">({{ reviewTotal }})</span></span>
|
||
</template>
|
||
<div class="search-filter-bar chart-card">
|
||
<!-- 第一行:搜索、排序和统计 -->
|
||
<div class="search-row">
|
||
<a-input-search
|
||
v-model:value="reviewFilters.keyword"
|
||
placeholder="搜索任务标题、SPU信息、发布人..."
|
||
class="search-input"
|
||
@search="loadReviewTasks"
|
||
@pressEnter="loadReviewTasks"
|
||
/>
|
||
<a-select
|
||
v-model:value="reviewFilters.sortBy"
|
||
placeholder="排序方式"
|
||
class="sort-select"
|
||
@change="loadReviewTasks"
|
||
>
|
||
<a-select-option value="latest">最新提交</a-select-option>
|
||
<a-select-option value="score">积分最高</a-select-option>
|
||
</a-select>
|
||
<span class="stats-text">共 {{ reviewTotal }} 个待审核任务</span>
|
||
</div>
|
||
|
||
<!-- 第二行:筛选条件 -->
|
||
<div class="filter-row">
|
||
<a-select
|
||
v-model:value="reviewFilters.taskTypeId"
|
||
placeholder="任务类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadReviewTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="reviewFilters.brandId"
|
||
placeholder="品牌"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadReviewTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
>
|
||
{{ brand.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="reviewFilters.categoryId"
|
||
placeholder="类目"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadReviewTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="reviewFilters.platformId"
|
||
placeholder="平台"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadReviewTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
>
|
||
{{ platform.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="reviewFilters.timeRange"
|
||
placeholder="提交时间"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadReviewTasks"
|
||
>
|
||
<a-select-option value="today">今天</a-select-option>
|
||
<a-select-option value="week">本周</a-select-option>
|
||
<a-select-option value="month">本月</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 第三行:积分范围和重置 -->
|
||
<div class="filter-row">
|
||
<a-input-number
|
||
v-model:value="reviewFilters.minScore"
|
||
placeholder="最小积分"
|
||
class="score-input"
|
||
:min="0"
|
||
@change="loadReviewTasks"
|
||
/>
|
||
<span class="score-divider">-</span>
|
||
<a-input-number
|
||
v-model:value="reviewFilters.maxScore"
|
||
placeholder="最大积分"
|
||
class="score-input"
|
||
:min="0"
|
||
@change="loadReviewTasks"
|
||
/>
|
||
<a-button class="reset-btn" @click="handleReviewFiltersReset">
|
||
重置
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 待审核任务列表 -->
|
||
<div v-if="reviewLoading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
|
||
<div v-else-if="reviewTaskList.length === 0" class="empty-state chart-card">
|
||
<div class="empty-icon">
|
||
<Icon name="file-text" size="xl" color="#6B6B6B" />
|
||
</div>
|
||
<h3>暂无待审核任务</h3>
|
||
<p>还没有需要审核的任务</p>
|
||
</div>
|
||
|
||
<div v-else class="task-grid">
|
||
<div
|
||
v-for="task in reviewTaskList"
|
||
:key="task.id"
|
||
class="task-card chart-card review"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span class="badge badge-review">
|
||
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
待审核
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info">
|
||
<div class="info-item">
|
||
<span class="label">任务编号</span>
|
||
<span class="value">{{ task.taskNo }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.publisherName || '--' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>接单人: {{ task.receiverName || '--' }}</span>
|
||
</div>
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="package" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div v-if="task.taskSubType" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="list" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>子类型: {{ task.taskSubType }}</span>
|
||
</div>
|
||
<div v-if="task.deliveryPath" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="link" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>交付路径: {{ task.deliveryPath }}</span>
|
||
</div>
|
||
<div v-if="task.attachmentUrl" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="task-actions">
|
||
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
|
||
<a-button v-if="canApproveTask(task)" type="primary" size="small" @click="handleApproveTask(task)">
|
||
通过审核
|
||
</a-button>
|
||
<a-button v-if="canRejectTask(task)" danger size="small" @click="handleRejectTask(task)">
|
||
驳回任务
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 时间信息 -->
|
||
<div class="task-footer">
|
||
<div class="footer-item">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">提交: {{ formatDate(task.submitTime || task.updateTime) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="reviewTotal > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="reviewPagination.page"
|
||
v-model:page-size="reviewPagination.pageSize"
|
||
:total="reviewTotal"
|
||
:show-size-changer="false"
|
||
@change="handleReviewPageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 已完成任务标签页 -->
|
||
<a-tab-pane key="inProgress">
|
||
<template #tab>
|
||
<span>进行中任务 <span class="tab-count">({{ inProgressTotal }})</span></span>
|
||
</template>
|
||
<div class="search-filter-bar chart-card">
|
||
<!-- 第一行:搜索、排序和统计 -->
|
||
<div class="search-row">
|
||
<a-input-search
|
||
v-model:value="inProgressFilters.keyword"
|
||
placeholder="搜索任务标题、SPU信息、发布人..."
|
||
class="search-input"
|
||
@search="loadInProgressTasks"
|
||
@pressEnter="loadInProgressTasks"
|
||
/>
|
||
<a-select
|
||
v-model:value="inProgressFilters.sortBy"
|
||
placeholder="排序方式"
|
||
class="sort-select"
|
||
@change="loadInProgressTasks"
|
||
>
|
||
<a-select-option value="latest">最新发布</a-select-option>
|
||
<a-select-option value="deadline">截止最近</a-select-option>
|
||
<a-select-option value="progress">进度最低</a-select-option>
|
||
</a-select>
|
||
<span class="stats-text">共 {{ inProgressTotal }} 个进行中任务</span>
|
||
</div>
|
||
|
||
<!-- 第二行:筛选条件 -->
|
||
<div class="filter-row">
|
||
<a-select
|
||
v-model:value="inProgressFilters.taskTypeId"
|
||
placeholder="任务类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadInProgressTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="inProgressFilters.brandId"
|
||
placeholder="品牌"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadInProgressTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
>
|
||
{{ brand.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="inProgressFilters.categoryId"
|
||
placeholder="类目"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadInProgressTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="inProgressFilters.platformId"
|
||
placeholder="平台"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadInProgressTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
>
|
||
{{ platform.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="inProgressFilters.timeRange"
|
||
placeholder="发布时间"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadInProgressTasks"
|
||
>
|
||
<a-select-option value="today">今天</a-select-option>
|
||
<a-select-option value="week">本周</a-select-option>
|
||
<a-select-option value="month">本月</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 第三行:进度范围和重置 -->
|
||
<div class="filter-row">
|
||
<a-input-number
|
||
v-model:value="inProgressFilters.minProgress"
|
||
placeholder="最小进度%"
|
||
class="score-input"
|
||
:min="0"
|
||
:max="100"
|
||
@change="loadInProgressTasks"
|
||
/>
|
||
<span class="score-divider">-</span>
|
||
<a-input-number
|
||
v-model:value="inProgressFilters.maxProgress"
|
||
placeholder="最大进度%"
|
||
class="score-input"
|
||
:min="0"
|
||
:max="100"
|
||
@change="loadInProgressTasks"
|
||
/>
|
||
<a-button class="reset-btn" @click="handleInProgressFiltersReset">
|
||
重置
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进行中任务列表 -->
|
||
<div v-if="inProgressLoading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
|
||
<div v-else-if="inProgressTaskList.length === 0" class="empty-state chart-card">
|
||
<div class="empty-icon">
|
||
<Icon name="clock" size="xl" color="#6B6B6B" />
|
||
</div>
|
||
<h3>暂无进行中任务</h3>
|
||
<p>还没有进行中的任务</p>
|
||
</div>
|
||
|
||
<div v-else class="task-grid">
|
||
<div
|
||
v-for="task in inProgressTaskList"
|
||
:key="task.id"
|
||
class="task-card chart-card in-progress"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span v-if="task.isSubTask" class="badge badge-subtask">
|
||
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
|
||
子任务
|
||
</span>
|
||
<span class="badge badge-in-progress">
|
||
<Icon name="clock" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
进行中
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info">
|
||
<div class="info-item">
|
||
<span class="label">任务编号</span>
|
||
<span class="value">{{ task.taskNo }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.publisherName || '--' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>接单人: {{ task.receiverName || '--' }}</span>
|
||
</div>
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="package" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div v-if="task.taskSubType" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="list" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>子类型: {{ task.taskSubType }}</span>
|
||
</div>
|
||
<div v-if="task.attachmentUrl" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度条 -->
|
||
<div class="task-progress">
|
||
<div class="progress-label">
|
||
<span>任务进度</span>
|
||
<span>{{ task.progress || 0 }}%</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="task.progress || 0"
|
||
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
|
||
:show-info="false"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="task-actions">
|
||
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
|
||
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click="handleUpdateProgress(task)">
|
||
更新进度
|
||
</a-button>
|
||
<a-button v-if="canCompleteTask(task)" type="primary" size="small" @click="handleCompleteTask(task)">
|
||
完结任务
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 时间信息 -->
|
||
<div class="task-footer">
|
||
<div class="footer-item">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
|
||
</div>
|
||
<div class="footer-item" v-if="task.deadline">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">截止: {{ formatDate(task.deadline) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="inProgressTotal > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="inProgressPagination.page"
|
||
v-model:page-size="inProgressPagination.pageSize"
|
||
:total="inProgressTotal"
|
||
:show-size-changer="false"
|
||
@change="handleInProgressPageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 待审核任务标签页 -->
|
||
<a-tab-pane key="completed">
|
||
<template #tab>
|
||
<span>已完成任务 <span class="tab-count">({{ completedTotal }})</span></span>
|
||
</template>
|
||
<div class="search-filter-bar chart-card">
|
||
<!-- 第一行:搜索、排序和统计 -->
|
||
<div class="search-row">
|
||
<a-input-search
|
||
v-model:value="completedFilters.keyword"
|
||
placeholder="搜索任务标题、SPU信息、发布人..."
|
||
class="search-input"
|
||
@search="loadCompletedTasks"
|
||
@pressEnter="loadCompletedTasks"
|
||
/>
|
||
<a-select
|
||
v-model:value="completedFilters.sortBy"
|
||
placeholder="排序方式"
|
||
class="sort-select"
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option value="latest">最新完成</a-select-option>
|
||
<a-select-option value="score">积分最高</a-select-option>
|
||
</a-select>
|
||
<span class="stats-text">共 {{ completedTotal }} 个已完成任务</span>
|
||
</div>
|
||
|
||
<!-- 第二行:筛选条件 -->
|
||
<div class="filter-row">
|
||
<a-select
|
||
v-model:value="completedFilters.taskTypeId"
|
||
placeholder="任务类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="completedFilters.demandTypeId"
|
||
placeholder="需求类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="completedFilters.brandId"
|
||
placeholder="品牌"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
>
|
||
{{ brand.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="completedFilters.categoryId"
|
||
placeholder="类目"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="completedFilters.platformId"
|
||
placeholder="平台"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
>
|
||
{{ platform.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="completedFilters.timeRange"
|
||
placeholder="发布时间"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadCompletedTasks"
|
||
>
|
||
<a-select-option value="today">今天</a-select-option>
|
||
<a-select-option value="week">本周</a-select-option>
|
||
<a-select-option value="month">本月</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 第三行:积分范围和重置 -->
|
||
<div class="filter-row">
|
||
<a-input-number
|
||
v-model:value="completedFilters.minScore"
|
||
placeholder="最小积分"
|
||
class="score-input"
|
||
:min="0"
|
||
@change="loadCompletedTasks"
|
||
/>
|
||
<span class="score-divider">-</span>
|
||
<a-input-number
|
||
v-model:value="completedFilters.maxScore"
|
||
placeholder="最大积分"
|
||
class="score-input"
|
||
:min="0"
|
||
@change="loadCompletedTasks"
|
||
/>
|
||
<a-button class="reset-btn" @click="handleCompletedFiltersReset">
|
||
重置
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已完成任务列表 -->
|
||
<div v-if="completedLoading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
|
||
<div v-else-if="completedTaskList.length === 0" class="empty-state chart-card">
|
||
<div class="empty-icon">
|
||
<Icon name="check-circle" size="xl" color="#6B6B6B" />
|
||
</div>
|
||
<h3>暂无已完成任务</h3>
|
||
<p>还没有完成的任务记录</p>
|
||
</div>
|
||
|
||
<div v-else class="task-grid">
|
||
<div
|
||
v-for="task in completedTaskList"
|
||
:key="task.id"
|
||
class="task-card chart-card completed"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span class="badge badge-completed">
|
||
<Icon name="check-circle" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
已完成
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info">
|
||
<div class="info-item">
|
||
<span class="label">任务编号</span>
|
||
<span class="value">{{ task.taskNo }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">获得积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.publisherName || '--' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>完成人: {{ task.receiverName || '--' }}</span>
|
||
</div>
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="package" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div v-if="task.taskSubType" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="list" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>子类型: {{ task.taskSubType }}</span>
|
||
</div>
|
||
<div v-if="task.deliveryPath" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="link" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>交付: {{ task.deliveryPath }}</span>
|
||
</div>
|
||
<div v-if="task.attachmentUrl" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度条(已完成任务显示100%) -->
|
||
<div class="task-progress">
|
||
<div class="progress-label">
|
||
<span>任务进度</span>
|
||
<span>{{ task.progress || 100 }}%</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="task.progress || 100"
|
||
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
|
||
:show-info="false"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 时间信息 -->
|
||
<div class="task-footer">
|
||
<div class="footer-item">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
|
||
</div>
|
||
<div class="footer-item">
|
||
<Icon name="check" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">完成: {{ formatDate(task.completeTime) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="completedTotal > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="completedPagination.page"
|
||
v-model:page-size="completedPagination.pageSize"
|
||
:total="completedTotal"
|
||
:show-size-changer="false"
|
||
@change="handleCompletedPageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 我发起的任务标签页 -->
|
||
<a-tab-pane key="published">
|
||
<template #tab>
|
||
<span>我发起的任务 <span class="tab-count">({{ publishedTotal }})</span></span>
|
||
</template>
|
||
<div class="search-filter-bar chart-card">
|
||
<!-- 第一行:搜索、排序和统计 -->
|
||
<div class="search-row">
|
||
<a-input-search
|
||
v-model:value="publishedFilters.keyword"
|
||
placeholder="搜索任务标题、SPU信息、发布人..."
|
||
class="search-input"
|
||
@search="loadPublishedTasks"
|
||
@pressEnter="loadPublishedTasks"
|
||
/>
|
||
<a-select
|
||
v-model:value="publishedFilters.sortBy"
|
||
placeholder="排序方式"
|
||
class="sort-select"
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option value="latest">最新发布</a-select-option>
|
||
<a-select-option value="score">积分最高</a-select-option>
|
||
<a-select-option value="deadline">时效最短</a-select-option>
|
||
</a-select>
|
||
<span class="stats-text">共 {{ publishedTotal }} 个任务</span>
|
||
</div>
|
||
|
||
<!-- 第二行:筛选条件 -->
|
||
<div class="filter-row">
|
||
<a-select
|
||
v-model:value="publishedFilters.status"
|
||
placeholder="任务状态"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option value="">全部</a-select-option>
|
||
<a-select-option value="pending">待接单</a-select-option>
|
||
<a-select-option value="in_progress">进行中</a-select-option>
|
||
<a-select-option value="review">待审核</a-select-option>
|
||
<a-select-option value="completed">已完成</a-select-option>
|
||
<a-select-option value="rejected">已驳回</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="publishedFilters.taskTypeId"
|
||
placeholder="任务类型"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="type in demandTypes"
|
||
:key="type.id"
|
||
:value="type.id"
|
||
>
|
||
{{ type.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="publishedFilters.brandId"
|
||
placeholder="品牌"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
>
|
||
{{ brand.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="publishedFilters.categoryId"
|
||
placeholder="类目"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="publishedFilters.platformId"
|
||
placeholder="平台"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
>
|
||
{{ platform.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
|
||
<a-select
|
||
v-model:value="publishedFilters.timeRange"
|
||
placeholder="发布时间"
|
||
class="filter-select"
|
||
allowClear
|
||
@change="loadPublishedTasks"
|
||
>
|
||
<a-select-option value="today">今天</a-select-option>
|
||
<a-select-option value="week">本周</a-select-option>
|
||
<a-select-option value="month">本月</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 第三行:积分范围和重置 -->
|
||
<div class="filter-row">
|
||
<a-input-number
|
||
v-model:value="publishedFilters.minScore"
|
||
placeholder="最小积分"
|
||
class="score-input"
|
||
:min="0"
|
||
@change="loadPublishedTasks"
|
||
/>
|
||
<span class="score-divider">-</span>
|
||
<a-input-number
|
||
v-model:value="publishedFilters.maxScore"
|
||
placeholder="最大积分"
|
||
class="score-input"
|
||
:min="0"
|
||
@change="loadPublishedTasks"
|
||
/>
|
||
<a-button class="reset-btn" @click="handlePublishedFiltersReset">
|
||
重置
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 我发起的任务列表 -->
|
||
<div v-if="publishedLoading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
|
||
<div v-else-if="publishedTaskList.length === 0" class="empty-state chart-card">
|
||
<div class="empty-icon">
|
||
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||
</div>
|
||
<h3>暂无发起的任务</h3>
|
||
<p>您还没有发起任何任务</p>
|
||
<router-link
|
||
to="/tasks/create"
|
||
class="btn btn-primary"
|
||
style="margin-top: 16px"
|
||
>
|
||
+ 发起新任务
|
||
</router-link>
|
||
</div>
|
||
|
||
<div v-else class="task-grid">
|
||
<div
|
||
v-for="task in publishedTaskList"
|
||
:key="task.id"
|
||
class="task-card chart-card"
|
||
:class="[`status-${task.status}`]"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span :class="['badge', `status-${task.status}`]">
|
||
{{ getTaskStatusText(task.status) }}
|
||
</span>
|
||
<span v-if="task.isUrgent" class="badge badge-urgent">
|
||
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
紧急
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info">
|
||
<div class="info-item">
|
||
<span class="label">任务编号</span>
|
||
<span class="value">{{ task.taskNo }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>处理人: {{ task.receiverName || '待接单' }}</span>
|
||
</div>
|
||
<!-- 子任务进度(流水线任务) -->
|
||
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="detail-item subtask-progress">
|
||
<span class="icon">
|
||
<Icon name="layers" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="subtask-stats">
|
||
子任务:
|
||
<span class="stat completed">已完成 {{ task.subTaskCompleted || 0 }}</span>
|
||
<span class="stat in-progress">进行中 {{ task.subTaskInProgress || 0 }}</span>
|
||
<span class="stat pending">待接单 {{ task.subTaskPending || 0 }}</span>
|
||
<span class="stat total">/ 共 {{ task.subTaskTotal }}</span>
|
||
</span>
|
||
</div>
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="package" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div v-if="task.taskSubType" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="list" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>子类型: {{ task.taskSubType }}</span>
|
||
</div>
|
||
<div v-if="task.attachmentUrl" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度条(进行中和已完成的任务显示) -->
|
||
<div v-if="task.status === 'in_progress' || task.status === 'completed'" class="task-progress">
|
||
<div class="progress-label">
|
||
<span>任务进度</span>
|
||
<span>{{ task.progress || 0 }}%</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="task.progress || 0"
|
||
:stroke-color="{ '0%': '#31f08b', '100%': '#31f08b' }"
|
||
:show-info="false"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 时间信息 -->
|
||
<div class="task-footer">
|
||
<div class="footer-item">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">发布: {{ formatDate(task.createTime) }}</span>
|
||
</div>
|
||
<div class="footer-item" v-if="task.deadline">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">截止: {{ formatDate(task.deadline) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 被驳回任务的操作按钮 -->
|
||
<div v-if="task.status === 'rejected'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
|
||
<a-button
|
||
type="primary"
|
||
size="small"
|
||
@click.stop="handleEditRejectedTask(task)"
|
||
style="width: 100%;">
|
||
<Icon name="edit" size="sm" style="margin-right: 4px;" />
|
||
重新编辑提交
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="publishedTotal > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="publishedPagination.page"
|
||
v-model:page-size="publishedPagination.pageSize"
|
||
:total="publishedTotal"
|
||
:show-size-changer="false"
|
||
@change="handlePublishedPageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 待我审核任务标签页 -->
|
||
<a-tab-pane key="pendingApproval" v-if="isDepartmentLeader">
|
||
<template #tab>
|
||
<span>待我审核 <span class="tab-count">({{ pendingApprovalTotal }})</span></span>
|
||
</template>
|
||
<div class="search-filter-bar chart-card">
|
||
<div class="search-row">
|
||
<a-input-search
|
||
v-model:value="approvalFilters.keyword"
|
||
placeholder="搜索任务标题、任务编号..."
|
||
class="search-input"
|
||
@search="loadPendingApprovalTasks"
|
||
@pressEnter="loadPendingApprovalTasks"
|
||
/>
|
||
<span class="stats-text">共 {{ pendingApprovalTotal }} 个待审核任务</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务列表 - 使用统一的任务卡片样式 -->
|
||
<div class="task-grid">
|
||
<div
|
||
v-for="item in pendingApprovalTasks"
|
||
:key="`${item.type}-${item.id}`"
|
||
class="task-card chart-card approval-card"
|
||
@click="item.type === 'task' ? handleTaskClick(item.data) : null"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ item.title }}</h3>
|
||
<div class="task-badges">
|
||
<span v-if="item.type === 'taskGroup'" class="badge badge-subtask">
|
||
<Icon name="layers" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
子任务组
|
||
</span>
|
||
<span v-else class="badge badge-approval">
|
||
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
{{ getApprovalStatusText(item.data.approvalStatus) }}
|
||
</span>
|
||
<span v-if="item.isUrgent" class="badge badge-urgent">
|
||
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
紧急
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段(仅主任务) -->
|
||
<div v-if="item.type === 'task' && getProcessStage(item.data)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(item.data) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info">
|
||
<div v-if="item.type === 'task'" class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(item.data.taskTypeId) }}</span>
|
||
</div>
|
||
<div v-if="item.type === 'taskGroup'" class="info-item">
|
||
<span class="label">子任务数量</span>
|
||
<span class="value">{{ item.data.items?.length || 0 }} 个</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ item.score }} 分</span>
|
||
</div>
|
||
<div v-if="item.type === 'task'" class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ item.data.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="hash" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>任务编号: {{ item.taskNo }}</span>
|
||
</div>
|
||
<div v-if="item.publisherName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ item.publisherName || '未知' }}</span>
|
||
</div>
|
||
<div v-if="item.publisherDeptName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="users" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起部门: {{ item.publisherDeptName }}</span>
|
||
</div>
|
||
<template v-if="item.type === 'task'">
|
||
<div v-if="item.data.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ item.data.brandName }}</span>
|
||
</div>
|
||
<div v-if="item.data.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ item.data.categoryName }}</span>
|
||
</div>
|
||
<div v-if="item.data.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ item.data.platformName }}</span>
|
||
</div>
|
||
<div v-if="item.data.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="package" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ item.data.spuInfo }}</span>
|
||
</div>
|
||
<div v-if="item.data.remark" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="message-square" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>备注: {{ item.data.remark }}</span>
|
||
</div>
|
||
<div v-if="item.data.attachmentUrl" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (item.data.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
<div v-if="item.deadline" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>截止: {{ formatDeadline(item.deadline) }}</span>
|
||
</div>
|
||
<div v-if="item.createTime" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起时间: {{ formatTime(item.createTime) }}</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 审核操作按钮 -->
|
||
<div class="task-actions" @click.stop>
|
||
<button
|
||
v-if="item.type === 'task'"
|
||
class="btn btn-secondary"
|
||
style="flex: 1"
|
||
@click.stop="handleTaskClick(item.data)"
|
||
>
|
||
查看详情
|
||
</button>
|
||
<button
|
||
class="btn btn-primary"
|
||
style="flex: 1"
|
||
@click="item.type === 'task' ? handleApproveTwoLevelTask(item.data) : handleApproveTaskGroup(item)"
|
||
:disabled="approvalLoading"
|
||
>
|
||
<Icon name="check" size="sm" style="margin-right: 4px;" />
|
||
审核通过
|
||
</button>
|
||
<button
|
||
class="btn btn-danger"
|
||
style="flex: 1"
|
||
@click="item.type === 'task' ? handleRejectTwoLevelTask(item.data) : handleRejectTaskGroup(item)"
|
||
:disabled="approvalLoading"
|
||
>
|
||
<Icon name="x" size="sm" style="margin-right: 4px;" />
|
||
驳回
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<a-empty
|
||
v-if="!loading && pendingApprovalTasks.length === 0"
|
||
description="暂无待审核任务"
|
||
style="margin-top: 60px;"
|
||
>
|
||
<template #image>
|
||
<Icon name="inbox" size="xxl" color="#d9d9d9" />
|
||
</template>
|
||
</a-empty>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading-container">
|
||
<a-spin size="large" tip="加载中..." />
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</div>
|
||
|
||
<!-- 任务详情抽屉 -->
|
||
<a-drawer
|
||
v-model:visible="detailDrawerVisible"
|
||
title="任务详情"
|
||
:width="600"
|
||
placement="right"
|
||
>
|
||
<div v-if="selectedTask" class="task-detail">
|
||
<div class="detail-section">
|
||
<h3>{{ selectedTask.title }}</h3>
|
||
<div class="detail-badges">
|
||
<span v-if="selectedTask.isSubTask" class="badge badge-subtask">子任务</span>
|
||
<span v-if="isNewTask(selectedTask)" class="badge badge-new">新发布</span>
|
||
<span v-if="isHighScoreTask(selectedTask)" class="badge badge-high-score">高分</span>
|
||
<span v-if="selectedTask.isUrgent" class="badge badge-urgent">紧急</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h4>基本信息</h4>
|
||
<a-descriptions :column="2" bordered>
|
||
<a-descriptions-item label="任务编号">
|
||
{{ selectedTask.isSubTask ? selectedTask.subTaskNo : selectedTask.taskNo }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item v-if="selectedTask.isSubTask" label="所属主任务">
|
||
{{ selectedTask.taskNo || '--' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="任务类型">{{ getTaskTypeName(selectedTask.taskTypeId) }}</a-descriptions-item>
|
||
<a-descriptions-item label="预估积分">{{ selectedTask.score }} 分</a-descriptions-item>
|
||
<a-descriptions-item label="预估工时">{{ selectedTask.estimatedHours || '--' }} 小时</a-descriptions-item>
|
||
<a-descriptions-item v-if="!selectedTask.isSubTask" label="数量">{{ selectedTask.quantity || '--' }} 个</a-descriptions-item>
|
||
<a-descriptions-item label="截止时间">{{ formatDeadline(selectedTask.deadline) }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h4>关联信息</h4>
|
||
<a-descriptions :column="2" bordered>
|
||
<a-descriptions-item label="品牌">{{ selectedTask.brandName || '--' }}</a-descriptions-item>
|
||
<a-descriptions-item label="类目">{{ selectedTask.categoryName || '--' }}</a-descriptions-item>
|
||
<a-descriptions-item label="平台">{{ selectedTask.platformName || '--' }}</a-descriptions-item>
|
||
<a-descriptions-item label="发布人">{{ selectedTask.publisherName || '未知' }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
</div>
|
||
|
||
<div v-if="selectedTask.spuInfo" class="detail-section">
|
||
<h4>SPU信息</h4>
|
||
<p>{{ selectedTask.spuInfo }}</p>
|
||
</div>
|
||
|
||
<div v-if="selectedTask.remark" class="detail-section">
|
||
<h4>备注说明</h4>
|
||
<p>{{ selectedTask.remark }}</p>
|
||
</div>
|
||
|
||
<div v-if="selectedTask.attachmentUrl" class="detail-section">
|
||
<h4>附件</h4>
|
||
<div class="attachment-links">
|
||
<a v-for="(url, index) in (selectedTask.attachmentUrl || '').split(',')"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link">
|
||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||
附件 {{ index + 1 }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 根据任务状态显示不同的操作按钮 -->
|
||
<div class="detail-actions">
|
||
<!-- 可接任务:显示抢单 -->
|
||
<a-button
|
||
v-if="selectedTask.status === 'pending'"
|
||
:type="canGrabTask(selectedTask).canGrab ? 'primary' : 'default'"
|
||
:style="{
|
||
backgroundColor: canGrabTask(selectedTask).canGrab ? '#31f08b' : '#999',
|
||
borderColor: canGrabTask(selectedTask).canGrab ? '#31f08b' : '#999',
|
||
color: '#fff',
|
||
cursor: canGrabTask(selectedTask).canGrab ? 'pointer' : 'not-allowed',
|
||
opacity: canGrabTask(selectedTask).canGrab ? 1 : 0.6
|
||
}"
|
||
size="large"
|
||
block
|
||
:disabled="!canGrabTask(selectedTask).canGrab"
|
||
@click="handleGrabFromDetail">
|
||
{{ canGrabTask(selectedTask).canGrab ? '确认抢单' : `不可接单(${canGrabTask(selectedTask).reason})` }}
|
||
</a-button>
|
||
|
||
<!-- 进行中任务:显示更新进度和完结 -->
|
||
<div v-else-if="selectedTask.status === 'in_progress'" class="action-buttons">
|
||
<a-button
|
||
v-if="canUpdateProgress(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
@click="handleUpdateProgress(selectedTask); detailDrawerVisible = false">
|
||
更新进度
|
||
</a-button>
|
||
<a-button
|
||
v-if="canCompleteTask(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
@click="handleCompleteTask(selectedTask); detailDrawerVisible = false">
|
||
完结任务
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 待审核任务:显示审核按钮 -->
|
||
<div v-else-if="selectedTask.status === 'review'" class="action-buttons">
|
||
<a-button
|
||
v-if="canApproveTask(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
@click="handleApproveTask(selectedTask); detailDrawerVisible = false">
|
||
通过审核
|
||
</a-button>
|
||
<a-button
|
||
v-if="canRejectTask(selectedTask)"
|
||
danger
|
||
size="large"
|
||
@click="handleRejectTask(selectedTask); detailDrawerVisible = false">
|
||
驳回任务
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 已完成任务:显示评分(如果还没评分) -->
|
||
<a-button
|
||
v-else-if="selectedTask.status === 'completed' && !selectedTask.rating && canApproveTask(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
block>
|
||
评分
|
||
</a-button>
|
||
|
||
<!-- 其他状态:仅显示关闭按钮 -->
|
||
<a-button
|
||
v-else
|
||
size="large"
|
||
block
|
||
@click="detailDrawerVisible = false">
|
||
关闭
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</a-drawer>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, onUnmounted, h } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { message, Modal, InputNumber, Textarea, Radio, RadioGroup, Button, Select, Space, Tag } from 'ant-design-vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import * as taskApi from '@/api/task'
|
||
import * as userApi from '@/api/user'
|
||
import type { Task, TaskApprovalDTO } from '@/api/task'
|
||
import type { DesignerWithWorkload } from '@/api/user'
|
||
import { useConfigStore } from '@/stores/config'
|
||
import dayjs from 'dayjs'
|
||
|
||
// 扩展Task类型以支持子任务字段
|
||
interface TaskOrSubTask extends Task {
|
||
isSubTask?: boolean // 是否为子任务
|
||
itemId?: number // 子任务ID
|
||
subTaskNo?: string // 子任务编号
|
||
groupNo?: number // 子任务组号
|
||
taskDescription?: string // 主任务描述
|
||
submitTime?: string // 提交时间
|
||
}
|
||
|
||
const router = useRouter()
|
||
|
||
// 全局配置store
|
||
const configStore = useConfigStore()
|
||
|
||
// 标签页
|
||
const activeTab = ref('available')
|
||
|
||
// 是否为部门负责人(从localStorage获取用户信息)
|
||
const isDepartmentLeader = computed(() => {
|
||
try {
|
||
const userStr = localStorage.getItem('userInfo')
|
||
if (!userStr) return false
|
||
const user = JSON.parse(userStr)
|
||
return user?.isDepartmentLeader === 1 || user?.role === 'dept_leader' || user?.role === 'admin'
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
// 状态
|
||
const loading = ref(false)
|
||
const taskList = ref<TaskOrSubTask[]>([])
|
||
const total = ref(0)
|
||
const detailDrawerVisible = ref(false)
|
||
const selectedTask = ref<TaskOrSubTask | null>(null)
|
||
|
||
// 已完成任务状态
|
||
const completedLoading = ref(false)
|
||
const completedTaskList = ref<Task[]>([])
|
||
const completedTotal = ref(0)
|
||
|
||
// 我发起的任务状态
|
||
const publishedLoading = ref(false)
|
||
const publishedTaskList = ref<Task[]>([])
|
||
const publishedTotal = ref(0)
|
||
|
||
// 进行中任务状态
|
||
const inProgressLoading = ref(false)
|
||
const inProgressTaskList = ref<Task[]>([])
|
||
const inProgressTotal = ref(0)
|
||
|
||
// 待审核任务状态
|
||
const reviewLoading = ref(false)
|
||
const reviewTaskList = ref<Task[]>([])
|
||
const reviewTotal = ref(0)
|
||
|
||
// 待我审核任务状态(包含主任务和子任务组)
|
||
interface ApprovalItem {
|
||
id: number
|
||
type: 'task' | 'taskGroup' // 区分类型
|
||
data: any // 主任务或子任务组数据
|
||
title: string
|
||
taskNo: string
|
||
score: number
|
||
deadline?: string | null
|
||
createTime?: string | null
|
||
isUrgent?: boolean | number
|
||
publisherName?: string | null
|
||
publisherDeptName?: string | null
|
||
}
|
||
|
||
const pendingApprovalTasks = ref<ApprovalItem[]>([])
|
||
const pendingApprovalTotal = ref(0)
|
||
const approvalFilters = reactive({
|
||
keyword: undefined as string | undefined
|
||
})
|
||
const approvalLoading = ref(false)
|
||
|
||
// 数据总览卡片
|
||
const dataCards = ref([
|
||
{ id: 1, label: '发布的任务数', value: 0, iconName: 'send' },
|
||
{ id: 2, label: '进行中任务', value: 0, iconName: 'clock' },
|
||
{ id: 3, label: '已完成任务', value: 0, iconName: 'check-circle' },
|
||
{ id: 4, label: '待审核任务', value: 0, iconName: 'file-text' },
|
||
{ id: 5, label: '累计积分', value: 0, iconName: 'award' }
|
||
])
|
||
|
||
// 月份选择
|
||
const selectedMonth = ref(dayjs())
|
||
|
||
// 使用全局配置store的数据
|
||
const demandTypes = computed(() => configStore.demandTypes)
|
||
const brands = computed(() => configStore.brands)
|
||
const platforms = computed(() => configStore.platforms)
|
||
const categories = computed(() => configStore.categories)
|
||
|
||
// 筛选条件
|
||
const filters = reactive({
|
||
taskTypeId: undefined as number | undefined,
|
||
demandTypeId: undefined as number | undefined,
|
||
brandId: undefined as number | undefined,
|
||
categoryId: undefined as number | undefined,
|
||
platformId: undefined as number | undefined,
|
||
taskSubType: undefined as string | undefined,
|
||
minScore: undefined as number | undefined,
|
||
maxScore: undefined as number | undefined,
|
||
keyword: undefined as string | undefined,
|
||
publisherName: undefined as string | undefined,
|
||
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
|
||
sortBy: 'latest' as 'latest' | 'score' | 'deadline' | 'hot'
|
||
})
|
||
|
||
// 已完成任务筛选条件
|
||
const completedFilters = reactive({
|
||
keyword: undefined as string | undefined,
|
||
sortBy: 'latest' as 'latest' | 'score',
|
||
taskTypeId: undefined as number | undefined,
|
||
demandTypeId: undefined as number | undefined,
|
||
brandId: undefined as number | undefined,
|
||
categoryId: undefined as number | undefined,
|
||
platformId: undefined as number | undefined,
|
||
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
|
||
minScore: undefined as number | undefined,
|
||
maxScore: undefined as number | undefined
|
||
})
|
||
|
||
// 我发起的任务筛选条件
|
||
const publishedFilters = reactive({
|
||
keyword: undefined as string | undefined,
|
||
status: '' as string,
|
||
sortBy: 'latest' as 'latest' | 'score' | 'deadline',
|
||
taskTypeId: undefined as number | undefined,
|
||
brandId: undefined as number | undefined,
|
||
categoryId: undefined as number | undefined,
|
||
platformId: undefined as number | undefined,
|
||
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
|
||
minScore: undefined as number | undefined,
|
||
maxScore: undefined as number | undefined
|
||
})
|
||
|
||
// 进行中任务筛选条件
|
||
const inProgressFilters = reactive({
|
||
keyword: undefined as string | undefined,
|
||
sortBy: 'latest' as 'latest' | 'deadline' | 'progress',
|
||
taskTypeId: undefined as number | undefined,
|
||
brandId: undefined as number | undefined,
|
||
categoryId: undefined as number | undefined,
|
||
platformId: undefined as number | undefined,
|
||
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
|
||
minProgress: undefined as number | undefined,
|
||
maxProgress: undefined as number | undefined
|
||
})
|
||
|
||
// 待审核任务筛选条件
|
||
const reviewFilters = reactive({
|
||
keyword: undefined as string | undefined,
|
||
sortBy: 'latest' as 'latest' | 'score',
|
||
taskTypeId: undefined as number | undefined,
|
||
brandId: undefined as number | undefined,
|
||
categoryId: undefined as number | undefined,
|
||
platformId: undefined as number | undefined,
|
||
timeRange: undefined as 'today' | 'week' | 'month' | undefined,
|
||
minScore: undefined as number | undefined,
|
||
maxScore: undefined as number | undefined
|
||
})
|
||
|
||
// 分页
|
||
const pagination = reactive({
|
||
page: 1,
|
||
pageSize: 12
|
||
})
|
||
|
||
// 已完成任务分页
|
||
const completedPagination = reactive({
|
||
page: 1,
|
||
pageSize: 12
|
||
})
|
||
|
||
// 获取任务类型名称
|
||
const getTaskTypeName = (typeId: number) => {
|
||
const type = demandTypes.value.find(t => t.id === typeId)
|
||
return type?.name || '未知类型'
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (time: string | undefined) => {
|
||
if (!time) return '--'
|
||
return dayjs(time).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
// 格式化日期(用于已完成任务)
|
||
const formatDate = (date: string | undefined) => {
|
||
if (!date) return '--'
|
||
return dayjs(date).format('YYYY-MM-DD')
|
||
}
|
||
|
||
// 格式化截止时间
|
||
const formatDeadline = (deadline: string | undefined) => {
|
||
if (!deadline) return '--'
|
||
const deadlineTime = dayjs(deadline)
|
||
const now = dayjs()
|
||
const diffHours = deadlineTime.diff(now, 'hour')
|
||
|
||
if (diffHours < 0) {
|
||
return `已过期 (${deadlineTime.format('YYYY-MM-DD HH:mm')})`
|
||
} else if (diffHours < 24) {
|
||
return `${diffHours} 小时后截止`
|
||
} else {
|
||
const diffDays = Math.floor(diffHours / 24)
|
||
return `${diffDays} 天后截止 (${deadlineTime.format('YYYY-MM-DD HH:mm')})`
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取任务流程阶段
|
||
* @param task 任务对象
|
||
* @returns 流程阶段文本
|
||
*/
|
||
const getProcessStage = (task: TaskOrSubTask): string => {
|
||
// 1. 需求负责人审核阶段
|
||
if (task.approvalStatus === 'pending_demand_approval' && task.currentApproverRole === 'demand_leader') {
|
||
return `需求负责人:${task.currentApproverName || '未知'}审核`
|
||
}
|
||
|
||
// 2. 设计负责人审核阶段
|
||
if (task.approvalStatus === 'pending_receiving_approval' && task.currentApproverRole === 'receiving_leader') {
|
||
return `设计负责人:${task.currentApproverName || '未知'}审核`
|
||
}
|
||
|
||
// 3. 任务被接单后:开始设计中
|
||
if (task.status === 'in_progress' && task.receiverId) {
|
||
return '开始设计中'
|
||
}
|
||
|
||
// 4. 任务被接单前:任务正在被抢单中
|
||
if (task.status === 'pending' && !task.receiverId) {
|
||
return '任务正在被抢单中'
|
||
}
|
||
|
||
// 5. 其他状态
|
||
if (task.status === 'review') {
|
||
return '待审核'
|
||
}
|
||
|
||
if (task.status === 'completed') {
|
||
return '已完成'
|
||
}
|
||
|
||
if (task.status === 'rejected') {
|
||
return '已驳回'
|
||
}
|
||
|
||
if (task.status === 'cancelled') {
|
||
return '已取消'
|
||
}
|
||
|
||
// 默认返回空字符串
|
||
return ''
|
||
}
|
||
|
||
// 判断是否为新发布任务(1小时内)
|
||
const isNewTask = (task: TaskOrSubTask) => {
|
||
if (!task.createTime) return false
|
||
const createTime = dayjs(task.createTime)
|
||
const now = dayjs()
|
||
return now.diff(createTime, 'hour') < 1
|
||
}
|
||
|
||
// 判断是否为高分任务(积分 >= 500)
|
||
const isHighScoreTask = (task: TaskOrSubTask) => {
|
||
return task.score && task.score >= 500
|
||
}
|
||
|
||
// 加载配置数据(从全局store加载)
|
||
const loadConfigs = async () => {
|
||
try {
|
||
await configStore.loadAllConfigs()
|
||
} catch (error) {
|
||
console.error('加载配置数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载任务列表(同时加载普通任务和子任务)
|
||
const loadTasks = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params: any = {
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize
|
||
}
|
||
|
||
// 添加筛选条件
|
||
if (filters.taskTypeId) params.taskTypeId = filters.taskTypeId
|
||
if (filters.demandTypeId) params.demandTypeId = filters.demandTypeId
|
||
if (filters.brandId) params.brandId = filters.brandId
|
||
if (filters.categoryId) params.categoryId = filters.categoryId
|
||
if (filters.platformId) params.platformId = filters.platformId
|
||
if (filters.taskSubType) params.taskSubType = filters.taskSubType
|
||
if (filters.minScore) params.minScore = Number(filters.minScore)
|
||
if (filters.maxScore) params.maxScore = Number(filters.maxScore)
|
||
if (filters.keyword) params.keyword = filters.keyword.trim()
|
||
if (filters.publisherName) params.publisherName = filters.publisherName.trim()
|
||
if (filters.timeRange) params.timeRange = filters.timeRange
|
||
if (filters.sortBy) params.sortBy = filters.sortBy
|
||
|
||
// 同时获取普通任务和子任务
|
||
const [taskResponse, itemResponse] = await Promise.all([
|
||
taskApi.getTaskHall(params),
|
||
taskApi.getAvailableTaskItems(params)
|
||
])
|
||
|
||
let allTasks: any[] = []
|
||
let totalCount = 0
|
||
|
||
// 处理普通任务
|
||
if (taskResponse.code === 200) {
|
||
const tasks = (taskResponse.data.records || []).map((task: any) => ({
|
||
...task,
|
||
isSubTask: false // 标记为普通任务
|
||
}))
|
||
allTasks = [...allTasks, ...tasks]
|
||
totalCount += taskResponse.data.total || 0
|
||
}
|
||
|
||
// 处理子任务
|
||
if (itemResponse.code === 200) {
|
||
const items = (itemResponse.data.records || []).map((item: any) => ({
|
||
// 将子任务字段映射到任务字段
|
||
...item,
|
||
id: item.id, // 保持子任务的ID
|
||
itemId: item.id, // 额外保存子任务ID
|
||
title: item.taskTitle || item.title, // 使用主任务标题
|
||
subTaskNo: item.itemNo, // 子任务编号
|
||
isSubTask: true, // 标记为子任务
|
||
taskId: item.taskId, // 保存主任务ID
|
||
// 保留子任务的其他字段
|
||
groupNo: item.groupNo,
|
||
taskTypeId: item.taskTypeId,
|
||
score: item.score,
|
||
estimatedHours: item.estimatedHours,
|
||
description: item.description || item.taskDescription
|
||
}))
|
||
allTasks = [...allTasks, ...items]
|
||
totalCount += itemResponse.data.total || 0
|
||
}
|
||
|
||
// 按创建时间排序
|
||
allTasks.sort((a, b) => {
|
||
const timeA = new Date(a.createTime || 0).getTime()
|
||
const timeB = new Date(b.createTime || 0).getTime()
|
||
return timeB - timeA // 降序
|
||
})
|
||
|
||
taskList.value = allTasks
|
||
total.value = totalCount
|
||
|
||
} catch (error) {
|
||
console.error('加载任务失败:', error)
|
||
message.error('加载任务失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 筛选变更
|
||
const handleFilterChange = () => {
|
||
pagination.page = 1
|
||
loadTasks()
|
||
}
|
||
|
||
// 重置筛选
|
||
const handleReset = () => {
|
||
filters.taskTypeId = undefined
|
||
filters.demandTypeId = undefined
|
||
filters.brandId = undefined
|
||
filters.categoryId = undefined
|
||
filters.platformId = undefined
|
||
filters.taskSubType = undefined
|
||
filters.minScore = undefined
|
||
filters.maxScore = undefined
|
||
filters.keyword = undefined
|
||
filters.publisherName = undefined
|
||
filters.timeRange = undefined
|
||
filters.sortBy = 'latest'
|
||
pagination.page = 1
|
||
loadTasks()
|
||
}
|
||
|
||
// 页码变更
|
||
const handlePageChange = () => {
|
||
loadTasks()
|
||
}
|
||
|
||
// 加载已完成任务列表
|
||
const loadCompletedTasks = async () => {
|
||
completedLoading.value = true
|
||
try {
|
||
const params: any = {
|
||
page: completedPagination.page,
|
||
pageSize: completedPagination.pageSize,
|
||
status: 'completed'
|
||
}
|
||
|
||
// 添加筛选条件
|
||
if (completedFilters.keyword) params.keyword = completedFilters.keyword
|
||
if (completedFilters.sortBy) params.sortBy = completedFilters.sortBy
|
||
|
||
const response = await taskApi.getCompletedTasks(params)
|
||
completedTaskList.value = response.data.records || []
|
||
completedTotal.value = response.data.total || 0
|
||
} catch (error) {
|
||
console.error('加载已完成任务失败:', error)
|
||
message.error('加载已完成任务失败')
|
||
} finally {
|
||
completedLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 标签页切换
|
||
const handleTabChange = (key: string) => {
|
||
if (key === 'completed') {
|
||
loadCompletedTasks()
|
||
} else if (key === 'published') {
|
||
loadPublishedTasks()
|
||
} else if (key === 'inProgress') {
|
||
loadInProgressTasks()
|
||
} else if (key === 'review') {
|
||
loadReviewTasks()
|
||
} else if (key === 'pendingApproval') {
|
||
loadPendingApprovalTasks()
|
||
} else {
|
||
loadTasks()
|
||
}
|
||
}
|
||
|
||
// ==================== 审核相关方法 ====================
|
||
|
||
// 加载待审核任务(包含主任务和子任务组)
|
||
const loadPendingApprovalTasks = async () => {
|
||
try {
|
||
loading.value = true
|
||
|
||
// 并行加载主任务和子任务组
|
||
const [tasksResponse, groupsResponse] = await Promise.all([
|
||
taskApi.getPendingApprovalTasks(),
|
||
taskApi.getPendingApprovalTaskGroups()
|
||
])
|
||
|
||
const approvalItems: ApprovalItem[] = []
|
||
|
||
// 转换主任务为ApprovalItem
|
||
if (tasksResponse && tasksResponse.data) {
|
||
const tasks = tasksResponse.data as Task[]
|
||
tasks.forEach(task => {
|
||
if (!task.id || !task.taskNo) return // 跳过无效任务
|
||
|
||
approvalItems.push({
|
||
id: task.id,
|
||
type: 'task',
|
||
data: task,
|
||
title: task.title,
|
||
taskNo: task.taskNo,
|
||
score: task.score || 0,
|
||
deadline: task.deadline || null,
|
||
createTime: task.createTime || null,
|
||
isUrgent: !!task.isUrgent,
|
||
publisherName: task.publisherName || null,
|
||
publisherDeptName: task.publisherDeptName || null
|
||
})
|
||
})
|
||
}
|
||
|
||
// 转换子任务组为ApprovalItem
|
||
if (groupsResponse && groupsResponse.data) {
|
||
const groups = groupsResponse.data as taskApi.TaskGroup[]
|
||
groups.forEach(group => {
|
||
approvalItems.push({
|
||
id: group.groupId,
|
||
type: 'taskGroup',
|
||
data: group,
|
||
title: `${group.mainTaskTitle} - 第${group.groupId}组`,
|
||
taskNo: group.mainTaskNo,
|
||
score: group.totalScore || 0,
|
||
deadline: null, // 子任务组没有独立的截止时间
|
||
createTime: null,
|
||
isUrgent: false,
|
||
publisherName: null,
|
||
publisherDeptName: null
|
||
})
|
||
})
|
||
}
|
||
|
||
// 如果有搜索关键词,进行本地过滤
|
||
if (approvalFilters.keyword && approvalFilters.keyword.trim()) {
|
||
const keyword = approvalFilters.keyword.trim().toLowerCase()
|
||
pendingApprovalTasks.value = approvalItems.filter(item =>
|
||
item.title?.toLowerCase().includes(keyword) ||
|
||
item.taskNo?.toLowerCase().includes(keyword)
|
||
)
|
||
} else {
|
||
pendingApprovalTasks.value = approvalItems
|
||
}
|
||
|
||
pendingApprovalTotal.value = pendingApprovalTasks.value.length
|
||
} catch (error: any) {
|
||
console.error('加载待审核任务失败:', error)
|
||
message.error(error.msg || error.message || '加载待审核任务失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取审核状态颜色
|
||
const getApprovalStatusColor = (status: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
'pending_demand_approval': 'orange',
|
||
'pending_receiving_approval': 'blue',
|
||
'approved': 'green',
|
||
'rejected_by_demand': 'red',
|
||
'rejected_by_receiving': 'red'
|
||
}
|
||
return colorMap[status] || 'default'
|
||
}
|
||
|
||
// 获取审核状态文本
|
||
const getApprovalStatusText = (status: string) => {
|
||
const textMap: Record<string, string> = {
|
||
'pending_demand_approval': '需求部门待审核',
|
||
'pending_receiving_approval': '接单部门待审核',
|
||
'approved': '已通过',
|
||
'rejected_by_demand': '需求部门已驳回',
|
||
'rejected_by_receiving': '接单部门已驳回'
|
||
}
|
||
return textMap[status] || status
|
||
}
|
||
|
||
// 两级审核:审核通过
|
||
const handleApproveTwoLevelTask = async (task: Task) => {
|
||
// 如果是接单部门负责人审核,显示指定设计师选项
|
||
if (task.currentApproverRole === 'receiving_leader') {
|
||
showAssignDesignerModal(task)
|
||
} else {
|
||
// 需求部门负责人审核,直接通过
|
||
Modal.confirm({
|
||
title: '确认审核通过',
|
||
content: `确定通过任务"${task.title}"的审核吗?`,
|
||
okText: '确认通过',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
const approvalData: TaskApprovalDTO = {
|
||
comment: '审核通过'
|
||
}
|
||
|
||
await taskApi.approveDemandLevel(task.id!, approvalData)
|
||
message.success('需求部门审核通过')
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('审核失败:', error)
|
||
message.error(error.msg || error.message || '审核失败')
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 显示指定设计师弹窗(接单部门审核时)
|
||
const showAssignDesignerModal = async (task: Task) => {
|
||
try {
|
||
// 获取当前部门的设计师列表
|
||
const userInfo = localStorage.getItem('userInfo')
|
||
let deptId: number | undefined
|
||
if (userInfo) {
|
||
const user = JSON.parse(userInfo)
|
||
deptId = user.deptId
|
||
}
|
||
|
||
// 加载设计师列表及工作量
|
||
const designers = await userApi.getDesignersWithWorkload(deptId)
|
||
|
||
if (designers.length === 0) {
|
||
message.warning('当前部门没有可用的设计师')
|
||
return
|
||
}
|
||
|
||
let assignType = 'auto' // 'auto' 或 'manual'
|
||
let selectedDesignerId: number | null = null
|
||
|
||
// 解析任务类型
|
||
const taskTypeIdsStr = task.taskTypeIds || task.taskTypeId?.toString() || ''
|
||
const taskTypeIdArray = taskTypeIdsStr.split(',').filter(id => id.trim())
|
||
|
||
// 初始化任务类型列表(带数量)
|
||
let editableTaskTypes = taskTypeIdArray.map(id => ({
|
||
typeId: parseInt(id.trim()),
|
||
quantity: task.quantity || 1
|
||
}))
|
||
|
||
let newTaskTypeId: number | null = null
|
||
|
||
const modalContent = h('div', { style: 'padding: 10px 0;' }, [
|
||
// 任务类型编辑区域
|
||
h('div', {
|
||
style: {
|
||
marginBottom: '24px',
|
||
padding: '16px',
|
||
background: '#fafafa',
|
||
borderRadius: '8px',
|
||
border: '1px solid #e8e8e8'
|
||
}
|
||
}, [
|
||
h('div', { style: 'marginBottom: 16px; display: flex; alignItems: center;' }, [
|
||
h('span', {
|
||
style: {
|
||
fontSize: '14px',
|
||
fontWeight: 600,
|
||
color: '#1890ff',
|
||
marginRight: '8px'
|
||
}
|
||
}, '📝'),
|
||
h('span', {
|
||
style: {
|
||
fontSize: '14px',
|
||
fontWeight: 600,
|
||
color: '#262626'
|
||
}
|
||
}, '编辑任务类型')
|
||
]),
|
||
h(Space, {
|
||
direction: 'vertical',
|
||
size: 8,
|
||
style: { width: '100%', marginBottom: '12px' }
|
||
}, [
|
||
...editableTaskTypes.map((item, index) =>
|
||
h('div', {
|
||
key: index,
|
||
style: {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
padding: '12px',
|
||
background: 'white',
|
||
borderRadius: '6px',
|
||
border: '1px solid #e8e8e8',
|
||
transition: 'all 0.3s'
|
||
}
|
||
}, [
|
||
h(Tag, {
|
||
color: 'blue',
|
||
style: {
|
||
marginRight: '12px',
|
||
fontSize: '13px',
|
||
padding: '4px 12px'
|
||
}
|
||
}, getTaskTypeName(item.typeId)),
|
||
h('span', {
|
||
style: {
|
||
flex: 1,
|
||
fontSize: '13px',
|
||
color: '#8c8c8c'
|
||
}
|
||
}),
|
||
h('span', {
|
||
style: {
|
||
marginRight: '8px',
|
||
fontSize: '13px',
|
||
color: '#595959'
|
||
}
|
||
}, '数量:'),
|
||
h(InputNumber, {
|
||
value: item.quantity,
|
||
min: 1,
|
||
size: 'small',
|
||
style: { width: '80px', marginRight: '8px' },
|
||
onChange: (val: number) => {
|
||
item.quantity = val || 1
|
||
}
|
||
}),
|
||
h(Button, {
|
||
type: 'text',
|
||
danger: true,
|
||
size: 'small',
|
||
onClick: () => {
|
||
editableTaskTypes.splice(index, 1)
|
||
}
|
||
}, '删除')
|
||
])
|
||
)
|
||
]),
|
||
h(Space, { size: 8, style: { width: '100%' } }, [
|
||
h(Select, {
|
||
value: newTaskTypeId,
|
||
placeholder: '选择要添加的任务类型',
|
||
allowClear: true,
|
||
style: { flex: 1, minWidth: '200px' },
|
||
onChange: (val: number) => {
|
||
newTaskTypeId = val
|
||
},
|
||
options: demandTypes.value.map((type: any) => ({
|
||
value: type.id,
|
||
label: type.name
|
||
}))
|
||
}),
|
||
h(Button, {
|
||
type: 'primary',
|
||
size: 'middle',
|
||
style: { background: '#52c41a', borderColor: '#52c41a' },
|
||
onClick: () => {
|
||
if (newTaskTypeId && !editableTaskTypes.find(t => t.typeId === newTaskTypeId)) {
|
||
editableTaskTypes.push({ typeId: newTaskTypeId, quantity: 1 })
|
||
newTaskTypeId = null
|
||
} else if (newTaskTypeId) {
|
||
message.warning('该任务类型已添加')
|
||
} else {
|
||
message.warning('请先选择任务类型')
|
||
}
|
||
}
|
||
}, '+ 添加类型')
|
||
])
|
||
]),
|
||
|
||
// 选择接单方式
|
||
h('div', {
|
||
style: {
|
||
marginBottom: '24px'
|
||
}
|
||
}, [
|
||
h('div', {
|
||
style: {
|
||
marginBottom: '12px',
|
||
fontSize: '14px',
|
||
fontWeight: 500,
|
||
color: '#262626'
|
||
}
|
||
}, '选择任务接单方式'),
|
||
h(RadioGroup, {
|
||
value: assignType,
|
||
onChange: (e: any) => {
|
||
assignType = e.target.value
|
||
},
|
||
style: { width: '100%' }
|
||
}, [
|
||
h(Radio, {
|
||
value: 'auto',
|
||
style: {
|
||
display: 'block',
|
||
marginBottom: '12px',
|
||
padding: '10px 12px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '6px',
|
||
background: assignType === 'auto' ? '#e6f7ff' : 'white',
|
||
transition: 'all 0.3s'
|
||
}
|
||
}, '🏢 发布到任务大厅(设计师自由抢单)'),
|
||
h(Radio, {
|
||
value: 'manual',
|
||
style: {
|
||
display: 'block',
|
||
padding: '10px 12px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '6px',
|
||
background: assignType === 'manual' ? '#e6f7ff' : 'white',
|
||
transition: 'all 0.3s'
|
||
}
|
||
}, '👤 指定设计师接单')
|
||
])
|
||
]),
|
||
|
||
// 设计师列表(仅在指定接单时显示)
|
||
h('div', {
|
||
style: {
|
||
display: assignType === 'manual' ? 'block' : 'none'
|
||
}
|
||
}, [
|
||
h('div', {
|
||
style: {
|
||
marginBottom: '12px',
|
||
fontSize: '14px',
|
||
fontWeight: 500,
|
||
color: '#262626'
|
||
}
|
||
}, '选择设计师'),
|
||
h(RadioGroup, {
|
||
value: selectedDesignerId,
|
||
onChange: (e: any) => {
|
||
selectedDesignerId = e.target.value
|
||
},
|
||
style: { width: '100%' }
|
||
}, designers.map(designer =>
|
||
h(Radio, {
|
||
value: designer.id,
|
||
style: {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
marginBottom: '10px',
|
||
padding: '12px',
|
||
border: '1px solid #e8e8e8',
|
||
borderRadius: '6px',
|
||
background: selectedDesignerId === designer.id ? '#e6f7ff' : 'white',
|
||
transition: 'all 0.3s'
|
||
}
|
||
}, [
|
||
h('span', { style: { flex: 1, fontSize: '14px', fontWeight: 500 } }, designer.realName),
|
||
h(Tag, {
|
||
color: designer.inProgressCount > 5 ? 'error' :
|
||
designer.inProgressCount > 3 ? 'warning' : 'success'
|
||
}, `进行中: ${designer.inProgressCount}`)
|
||
])
|
||
))
|
||
])
|
||
])
|
||
|
||
Modal.confirm({
|
||
title: '✅ 接单部门审核通过',
|
||
content: modalContent,
|
||
width: 650,
|
||
okText: '确认通过',
|
||
cancelText: '取消',
|
||
okButtonProps: {
|
||
size: 'large',
|
||
style: { minWidth: '100px' }
|
||
},
|
||
cancelButtonProps: {
|
||
size: 'large',
|
||
style: { minWidth: '100px' }
|
||
},
|
||
onOk: async () => {
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
// 检查是否需要指定设计师
|
||
if (assignType === 'manual' && !selectedDesignerId) {
|
||
message.error('请选择要指定的设计师')
|
||
return Promise.reject()
|
||
}
|
||
|
||
// 检查任务类型是否为空
|
||
if (editableTaskTypes.length === 0) {
|
||
message.error('至少需要一个任务类型')
|
||
return Promise.reject()
|
||
}
|
||
|
||
// 准备任务类型更新数据
|
||
const approvalData: TaskApprovalDTO = {
|
||
comment: assignType === 'manual' ?
|
||
`指定设计师接单` : '审核通过,发布到任务大厅',
|
||
assignedReceiverId: assignType === 'manual' && selectedDesignerId ? selectedDesignerId : undefined,
|
||
taskUpdates: {
|
||
taskTypes: editableTaskTypes.map(t => ({
|
||
taskTypeId: t.typeId,
|
||
quantity: t.quantity
|
||
}))
|
||
}
|
||
}
|
||
|
||
await taskApi.approveReceivingLevel(task.id!, approvalData)
|
||
|
||
if (assignType === 'manual') {
|
||
const designer = designers.find(d => d.id === selectedDesignerId)
|
||
message.success(`已指定设计师 ${designer?.realName} 接单`)
|
||
} else {
|
||
message.success('审核通过,任务已发布到大厅')
|
||
}
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('审核失败:', error)
|
||
message.error(error.msg || error.message || '审核失败')
|
||
return Promise.reject(error)
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
} catch (error: any) {
|
||
console.error('获取设计师列表失败:', error)
|
||
message.error('获取设计师列表失败')
|
||
}
|
||
}
|
||
|
||
// 两级审核:驳回任务
|
||
const handleRejectTwoLevelTask = async (task: Task) => {
|
||
let rejectReason = ''
|
||
|
||
Modal.confirm({
|
||
title: '❌ 驳回任务',
|
||
content: h('div', { style: 'padding: 16px 0;' }, [
|
||
h('div', {
|
||
style: {
|
||
marginBottom: '12px',
|
||
fontSize: '14px',
|
||
fontWeight: 500,
|
||
color: '#262626'
|
||
}
|
||
}, '请输入驳回原因'),
|
||
h(Textarea, {
|
||
rows: 5,
|
||
placeholder: '请详细说明驳回原因,以便发起人修改...',
|
||
maxlength: 500,
|
||
showCount: true,
|
||
style: { fontSize: '14px' },
|
||
onChange: (e: any) => {
|
||
rejectReason = e.target.value
|
||
}
|
||
})
|
||
]),
|
||
width: 550,
|
||
okText: '确认驳回',
|
||
cancelText: '取消',
|
||
okType: 'danger',
|
||
okButtonProps: {
|
||
size: 'large',
|
||
style: { minWidth: '100px' }
|
||
},
|
||
cancelButtonProps: {
|
||
size: 'large',
|
||
style: { minWidth: '100px' }
|
||
},
|
||
onOk: async () => {
|
||
if (!rejectReason || !rejectReason.trim()) {
|
||
message.warning('请输入驳回原因')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
// 根据当前审核角色调用不同的API
|
||
if (task.currentApproverRole === 'demand_leader') {
|
||
await taskApi.rejectByDemandLeader(task.id!, rejectReason.trim())
|
||
message.success('已驳回,任务已退回给发起人')
|
||
} else if (task.currentApproverRole === 'receiving_leader') {
|
||
await taskApi.rejectByReceivingLeader(task.id!, rejectReason.trim())
|
||
message.success('已驳回,任务已退回给需求部门负责人')
|
||
}
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('驳回失败:', error)
|
||
message.error(error.msg || error.message || '驳回失败')
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// ==================== 子任务组审核相关方法 ====================
|
||
|
||
// 审核通过子任务组
|
||
const handleApproveTaskGroup = async (item: ApprovalItem) => {
|
||
try {
|
||
// 获取设计师列表及工作量
|
||
const designersWithWorkload = await userApi.getDesignersWithWorkload()
|
||
|
||
let assignType = ref<'auto' | 'manual'>('auto')
|
||
let selectedDesignerId = ref<number | undefined>(undefined)
|
||
|
||
Modal.confirm({
|
||
title: '审核子任务组',
|
||
width: 600,
|
||
content: h('div', { style: 'padding: 10px 0;' }, [
|
||
h('p', { style: 'margin-bottom: 16px; color: #666; font-size: 13px;' },
|
||
`即将审核通过子任务组"${item.title}",共${item.data.items?.length || 0}个子任务,总积分${item.score}分。`
|
||
),
|
||
h('p', { style: 'margin-bottom: 12px; font-weight: 500;' }, '请选择接单方式:'),
|
||
h(RadioGroup, {
|
||
value: assignType.value,
|
||
onChange: (e: any) => {
|
||
assignType.value = e.target.value
|
||
if (e.target.value === 'auto') {
|
||
selectedDesignerId.value = undefined
|
||
}
|
||
}
|
||
}, () => [
|
||
h(Radio, { value: 'auto', style: { display: 'block', marginBottom: '12px' } }, () => '发布到任务大厅(设计师抢单)'),
|
||
h(Radio, { value: 'manual', style: { display: 'block', marginBottom: '12px' } }, () => '指定设计师接单')
|
||
]),
|
||
h('div', {
|
||
style: {
|
||
display: assignType.value === 'manual' ? 'block' : 'none',
|
||
marginTop: '16px',
|
||
marginLeft: '24px'
|
||
}
|
||
}, [
|
||
h('p', { style: 'margin-bottom: 8px; font-size: 13px; color: #666;' }, '选择设计师:'),
|
||
h(RadioGroup, {
|
||
value: selectedDesignerId.value,
|
||
onChange: (e: any) => {
|
||
selectedDesignerId.value = e.target.value
|
||
},
|
||
style: { width: '100%' }
|
||
}, () => designersWithWorkload.map(designer => {
|
||
const workloadColor = designer.inProgressCount === 0 ? '#52c41a'
|
||
: designer.inProgressCount <= 3 ? '#faad14'
|
||
: '#f5222d'
|
||
|
||
return h(Radio, {
|
||
value: designer.id,
|
||
style: {
|
||
display: 'block',
|
||
marginBottom: '8px',
|
||
padding: '8px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px'
|
||
}
|
||
}, () => h('div', { style: 'display: flex; justify-content: space-between; align-items: center;' }, [
|
||
h('span', {}, `${designer.realName} (${designer.deptName || '未知部门'})`),
|
||
h('span', {
|
||
style: {
|
||
fontSize: '12px',
|
||
color: workloadColor,
|
||
fontWeight: 'bold'
|
||
}
|
||
}, `进行中: ${designer.inProgressCount}个任务`)
|
||
]))
|
||
}))
|
||
])
|
||
]),
|
||
okText: '确认审核通过',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
if (assignType.value === 'manual' && !selectedDesignerId.value) {
|
||
message.warning('请选择设计师')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
await taskApi.approveTaskGroup(item.id, {
|
||
assignedReceiverId: assignType.value === 'manual' ? selectedDesignerId.value : undefined
|
||
})
|
||
|
||
if (assignType.value === 'manual') {
|
||
const designer = designersWithWorkload.find(d => d.id === selectedDesignerId.value)
|
||
message.success(`已审核通过并指定给设计师 ${designer?.realName}`)
|
||
} else {
|
||
message.success('已审核通过,子任务组已发布到任务大厅')
|
||
}
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('审核失败:', error)
|
||
message.error(error.msg || error.message || '审核失败')
|
||
return Promise.reject(error)
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
} catch (error: any) {
|
||
console.error('获取设计师列表失败:', error)
|
||
message.error('获取设计师列表失败')
|
||
}
|
||
}
|
||
|
||
// 驳回子任务组
|
||
const handleRejectTaskGroup = async (item: ApprovalItem) => {
|
||
let rejectReason = ''
|
||
|
||
Modal.confirm({
|
||
title: '驳回子任务组',
|
||
content: h('div', { style: 'padding: 10px 0;' }, [
|
||
h('p', { style: 'margin-bottom: 12px; color: #666;' },
|
||
`即将驳回子任务组"${item.title}",共${item.data.items?.length || 0}个子任务。`
|
||
),
|
||
h('p', { style: 'margin-bottom: 10px; color: #666;' }, '请输入驳回原因:'),
|
||
h(Textarea, {
|
||
rows: 4,
|
||
placeholder: '请详细说明驳回原因',
|
||
maxlength: 500,
|
||
showCount: true,
|
||
onChange: (e: any) => {
|
||
rejectReason = e.target.value
|
||
}
|
||
})
|
||
]),
|
||
okText: '确认驳回',
|
||
cancelText: '取消',
|
||
okType: 'danger',
|
||
onOk: async () => {
|
||
if (!rejectReason || !rejectReason.trim()) {
|
||
message.warning('请输入驳回原因')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
await taskApi.rejectTaskGroup(item.id, rejectReason.trim())
|
||
message.success('已驳回子任务组')
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('驳回失败:', error)
|
||
message.error(error.msg || error.message || '驳回失败')
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 已完成任务分页变化
|
||
const handleCompletedPageChange = (page: number) => {
|
||
completedPagination.page = page
|
||
loadCompletedTasks()
|
||
}
|
||
|
||
// 我发起的任务分页
|
||
const publishedPagination = reactive({
|
||
page: 1,
|
||
pageSize: 10
|
||
})
|
||
|
||
// 进行中任务分页
|
||
const inProgressPagination = reactive({
|
||
page: 1,
|
||
pageSize: 12
|
||
})
|
||
|
||
// 待审核任务分页
|
||
const reviewPagination = reactive({
|
||
page: 1,
|
||
pageSize: 12
|
||
})
|
||
|
||
// 加载我发起的任务
|
||
const loadPublishedTasks = async () => {
|
||
publishedLoading.value = true
|
||
try {
|
||
const response = await taskApi.getMyTasks({
|
||
role: 'publisher',
|
||
status: publishedFilters.status || undefined,
|
||
keyword: publishedFilters.keyword || undefined,
|
||
page: publishedPagination.page,
|
||
pageSize: publishedPagination.pageSize
|
||
})
|
||
|
||
if (response.code === 200) {
|
||
publishedTaskList.value = response.data.records || []
|
||
publishedTotal.value = response.data.total || 0
|
||
}
|
||
} catch (error) {
|
||
console.error('加载我发起的任务失败:', error)
|
||
message.error('加载失败')
|
||
} finally {
|
||
publishedLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 我发起的任务分页变化
|
||
const handlePublishedPageChange = (page: number) => {
|
||
publishedPagination.page = page
|
||
loadPublishedTasks()
|
||
}
|
||
|
||
// 重置已完成任务筛选条件
|
||
const handleCompletedFiltersReset = () => {
|
||
completedFilters.keyword = undefined
|
||
completedFilters.sortBy = 'latest'
|
||
completedFilters.taskTypeId = undefined
|
||
completedFilters.demandTypeId = undefined
|
||
completedFilters.brandId = undefined
|
||
completedFilters.categoryId = undefined
|
||
completedFilters.platformId = undefined
|
||
completedFilters.timeRange = undefined
|
||
completedFilters.minScore = undefined
|
||
completedFilters.maxScore = undefined
|
||
completedPagination.page = 1
|
||
loadCompletedTasks()
|
||
}
|
||
|
||
// 重置我发起的任务筛选条件
|
||
const handlePublishedFiltersReset = () => {
|
||
publishedFilters.keyword = undefined
|
||
publishedFilters.status = ''
|
||
publishedFilters.sortBy = 'latest'
|
||
publishedFilters.taskTypeId = undefined
|
||
publishedFilters.brandId = undefined
|
||
publishedFilters.categoryId = undefined
|
||
publishedFilters.platformId = undefined
|
||
publishedFilters.timeRange = undefined
|
||
publishedFilters.minScore = undefined
|
||
publishedFilters.maxScore = undefined
|
||
publishedPagination.page = 1
|
||
loadPublishedTasks()
|
||
}
|
||
|
||
// 加载数据总览
|
||
const loadDashboardStats = async () => {
|
||
try {
|
||
const month = selectedMonth.value?.format('YYYY-MM')
|
||
const response = await taskApi.getDashboardStats(month ? { month } : undefined)
|
||
if (response.code === 200) {
|
||
const data = response.data
|
||
dataCards.value[0].value = data.publishedCount || 0 // 发布的任务数(当月)
|
||
dataCards.value[1].value = data.inProgressCount || 0 // 进行中任务
|
||
dataCards.value[2].value = data.completedCount || 0 // 已完成任务
|
||
dataCards.value[3].value = data.reviewCount || 0 // 待审核任务
|
||
dataCards.value[4].value = data.totalScore || 0 // 累计积分
|
||
}
|
||
} catch (error) {
|
||
console.error('加载数据总览失败:', error)
|
||
}
|
||
}
|
||
|
||
// 月份选择改变
|
||
const handleMonthChange = () => {
|
||
loadDashboardStats()
|
||
}
|
||
|
||
// 发起任务
|
||
const handleCreateTask = () => {
|
||
router.push('/tasks/create')
|
||
}
|
||
|
||
// 加载进行中任务(同时加载普通任务和子任务)
|
||
const loadInProgressTasks = async () => {
|
||
inProgressLoading.value = true
|
||
try {
|
||
const params: any = {
|
||
page: inProgressPagination.page,
|
||
pageSize: inProgressPagination.pageSize,
|
||
status: 'in_progress'
|
||
}
|
||
|
||
// 添加筛选条件
|
||
if (inProgressFilters.keyword) params.keyword = inProgressFilters.keyword
|
||
if (inProgressFilters.sortBy) params.sortBy = inProgressFilters.sortBy
|
||
if (inProgressFilters.taskTypeId) params.taskTypeId = inProgressFilters.taskTypeId
|
||
if (inProgressFilters.brandId) params.brandId = inProgressFilters.brandId
|
||
if (inProgressFilters.categoryId) params.categoryId = inProgressFilters.categoryId
|
||
if (inProgressFilters.platformId) params.platformId = inProgressFilters.platformId
|
||
if (inProgressFilters.timeRange) params.timeRange = inProgressFilters.timeRange
|
||
if (inProgressFilters.minProgress !== undefined) params.minProgress = inProgressFilters.minProgress
|
||
if (inProgressFilters.maxProgress !== undefined) params.maxProgress = inProgressFilters.maxProgress
|
||
|
||
// 同时获取普通任务和子任务
|
||
const [taskResponse, itemResponse] = await Promise.all([
|
||
taskApi.getTasks(params),
|
||
taskApi.getInProgressTaskItems(params)
|
||
])
|
||
|
||
let allTasks: any[] = []
|
||
let totalCount = 0
|
||
|
||
// 处理普通任务
|
||
if (taskResponse.data && taskResponse.data.records) {
|
||
const tasks = taskResponse.data.records.map((task: any) => ({
|
||
...task,
|
||
isSubTask: false // 标记为普通任务
|
||
}))
|
||
allTasks = [...allTasks, ...tasks]
|
||
totalCount += taskResponse.data.total || 0
|
||
}
|
||
|
||
// 处理子任务
|
||
if (itemResponse.data && itemResponse.data.records) {
|
||
const items = itemResponse.data.records.map((item: any) => ({
|
||
// 将子任务字段映射到任务字段
|
||
...item,
|
||
id: item.id, // 保持子任务的ID
|
||
itemId: item.id, // 额外保存子任务ID
|
||
title: item.taskTitle || item.title, // 使用主任务标题
|
||
subTaskNo: item.itemNo, // 子任务编号
|
||
isSubTask: true, // 标记为子任务
|
||
taskId: item.taskId, // 保存主任务ID
|
||
// 保留子任务的其他字段
|
||
groupNo: item.groupNo,
|
||
taskTypeId: item.taskTypeId,
|
||
score: item.score,
|
||
estimatedHours: item.estimatedHours,
|
||
description: item.description || item.taskDescription,
|
||
progress: item.progress,
|
||
status: 'in_progress'
|
||
}))
|
||
allTasks = [...allTasks, ...items]
|
||
totalCount += itemResponse.data.total || 0
|
||
}
|
||
|
||
// 按更新时间排序
|
||
allTasks.sort((a, b) => {
|
||
const timeA = new Date(a.updateTime || a.startTime || 0).getTime()
|
||
const timeB = new Date(b.updateTime || b.startTime || 0).getTime()
|
||
return timeB - timeA // 降序
|
||
})
|
||
|
||
inProgressTaskList.value = allTasks
|
||
inProgressTotal.value = totalCount
|
||
|
||
} catch (error) {
|
||
console.error('加载进行中任务失败:', error)
|
||
message.error('加载进行中任务失败')
|
||
} finally {
|
||
inProgressLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载待审核任务(所有处于审批流程中的任务)
|
||
const loadReviewTasks = async () => {
|
||
reviewLoading.value = true
|
||
try {
|
||
const params: any = {
|
||
page: reviewPagination.page,
|
||
pageSize: reviewPagination.pageSize
|
||
}
|
||
|
||
// 添加筛选条件
|
||
if (reviewFilters.taskTypeId) params.taskTypeId = reviewFilters.taskTypeId
|
||
|
||
// 调用新接口:获取所有处于审批流程中的任务
|
||
const response = await taskApi.getAllPendingApprovalTasks(params)
|
||
|
||
let tasks = response.data?.records || []
|
||
reviewTotal.value = response.data?.total || 0
|
||
|
||
// 前端筛选(因为新接口只支持taskTypeId筛选)
|
||
if (reviewFilters.keyword) {
|
||
const keyword = reviewFilters.keyword.trim().toLowerCase()
|
||
tasks = tasks.filter((task: Task) =>
|
||
task.title?.toLowerCase().includes(keyword) ||
|
||
task.taskNo?.toLowerCase().includes(keyword) ||
|
||
task.spuInfo?.toLowerCase().includes(keyword) ||
|
||
task.publisherName?.toLowerCase().includes(keyword)
|
||
)
|
||
}
|
||
|
||
if (reviewFilters.brandId) {
|
||
tasks = tasks.filter((task: Task) => task.brandId === reviewFilters.brandId)
|
||
}
|
||
|
||
if (reviewFilters.categoryId) {
|
||
tasks = tasks.filter((task: Task) => task.categoryId === reviewFilters.categoryId)
|
||
}
|
||
|
||
if (reviewFilters.platformId) {
|
||
tasks = tasks.filter((task: Task) => task.platformId === reviewFilters.platformId)
|
||
}
|
||
|
||
if (reviewFilters.minScore !== undefined) {
|
||
tasks = tasks.filter((task: Task) => (task.score || 0) >= reviewFilters.minScore!)
|
||
}
|
||
|
||
if (reviewFilters.maxScore !== undefined) {
|
||
tasks = tasks.filter((task: Task) => (task.score || 0) <= reviewFilters.maxScore!)
|
||
}
|
||
|
||
if (reviewFilters.timeRange) {
|
||
const now = dayjs()
|
||
let startTime: dayjs.Dayjs
|
||
if (reviewFilters.timeRange === 'today') {
|
||
startTime = now.startOf('day')
|
||
} else if (reviewFilters.timeRange === 'week') {
|
||
startTime = now.subtract(7, 'day')
|
||
} else if (reviewFilters.timeRange === 'month') {
|
||
startTime = now.subtract(30, 'day')
|
||
} else {
|
||
startTime = now.subtract(1, 'year')
|
||
}
|
||
tasks = tasks.filter((task: Task) =>
|
||
task.createTime && dayjs(task.createTime).isAfter(startTime)
|
||
)
|
||
}
|
||
|
||
// 排序
|
||
if (reviewFilters.sortBy === 'score') {
|
||
tasks.sort((a: Task, b: Task) => (b.score || 0) - (a.score || 0))
|
||
} else {
|
||
tasks.sort((a: Task, b: Task) =>
|
||
dayjs(b.createTime || 0).valueOf() - dayjs(a.createTime || 0).valueOf()
|
||
)
|
||
}
|
||
|
||
reviewTaskList.value = tasks
|
||
reviewTotal.value = tasks.length
|
||
} catch (error) {
|
||
console.error('加载待审核任务失败:', error)
|
||
message.error('加载待审核任务失败')
|
||
} finally {
|
||
reviewLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 进行中任务分页变化
|
||
const handleInProgressPageChange = (page: number) => {
|
||
inProgressPagination.page = page
|
||
loadInProgressTasks()
|
||
}
|
||
|
||
// 待审核任务分页变化
|
||
const handleReviewPageChange = (page: number) => {
|
||
reviewPagination.page = page
|
||
loadReviewTasks()
|
||
}
|
||
|
||
// 重置进行中任务筛选条件
|
||
const handleInProgressFiltersReset = () => {
|
||
inProgressFilters.keyword = undefined
|
||
inProgressFilters.sortBy = 'latest'
|
||
inProgressFilters.taskTypeId = undefined
|
||
inProgressFilters.brandId = undefined
|
||
inProgressFilters.categoryId = undefined
|
||
inProgressFilters.platformId = undefined
|
||
inProgressFilters.timeRange = undefined
|
||
inProgressFilters.minProgress = undefined
|
||
inProgressFilters.maxProgress = undefined
|
||
inProgressPagination.page = 1
|
||
loadInProgressTasks()
|
||
}
|
||
|
||
// 重置待审核任务筛选条件
|
||
const handleReviewFiltersReset = () => {
|
||
reviewFilters.keyword = undefined
|
||
reviewFilters.sortBy = 'latest'
|
||
reviewFilters.taskTypeId = undefined
|
||
reviewFilters.brandId = undefined
|
||
reviewFilters.categoryId = undefined
|
||
reviewFilters.platformId = undefined
|
||
reviewFilters.timeRange = undefined
|
||
reviewFilters.minScore = undefined
|
||
reviewFilters.maxScore = undefined
|
||
reviewPagination.page = 1
|
||
loadReviewTasks()
|
||
}
|
||
|
||
// 判断是否可以更新进度(接单人)
|
||
const canUpdateProgress = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
return task.receiverId === userInfo.id
|
||
}
|
||
|
||
// 判断是否可以完结任务(接单人)
|
||
const canCompleteTask = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
return task.receiverId === userInfo.id && (task.progress || 0) >= 100
|
||
}
|
||
|
||
// 判断是否可以审核任务
|
||
// 对于审批流程中的任务:只有当前审核人可以审核
|
||
// 对于其他情况:只有部门负责人可以审核
|
||
const canApproveTask = (task: Task) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
const currentUserId = userInfo.id
|
||
|
||
// 如果是审批流程中的任务(需求部门审核或接单部门审核)
|
||
if (task.approvalStatus === 'pending_demand_approval' ||
|
||
task.approvalStatus === 'pending_receiving_approval') {
|
||
// 只有当前审核人可以审核
|
||
return task.currentApproverId === currentUserId
|
||
}
|
||
|
||
// 其他情况(如任务完成后等待审核):只有部门负责人可以审核
|
||
return userInfo.roleId === 2 || userInfo.isDepartmentLeader === 1 ||
|
||
userInfo.role === 'dept_leader' || userInfo.role === 'admin'
|
||
}
|
||
|
||
// 判断是否可以驳回任务
|
||
const canRejectTask = (task: Task) => {
|
||
return canApproveTask(task)
|
||
}
|
||
|
||
// 更新进度(支持普通任务和子任务)
|
||
const handleUpdateProgress = (task: TaskOrSubTask) => {
|
||
let newProgress = task.progress || 0
|
||
const isSubTask = task.isSubTask
|
||
const taskType = isSubTask ? '子任务' : '任务'
|
||
|
||
Modal.confirm({
|
||
title: `更新${taskType}进度`,
|
||
content: h('div', [
|
||
h('div', { style: 'margin-bottom: 12px;' }, `${taskType}:${task.title}`),
|
||
h('div', { style: 'margin-bottom: 12px;' }, `当前进度:${task.progress || 0}%`),
|
||
h(InputNumber, {
|
||
min: 0,
|
||
max: 100,
|
||
defaultValue: task.progress || 0,
|
||
style: 'width: 100%',
|
||
placeholder: '输入新的进度(0-100)',
|
||
onChange: (value: number) => { newProgress = value }
|
||
})
|
||
]),
|
||
onOk: async () => {
|
||
try {
|
||
if (isSubTask) {
|
||
// 更新子任务进度
|
||
await taskApi.updateTaskItemProgress(task.itemId || task.id!, newProgress)
|
||
} else {
|
||
// 更新普通任务进度
|
||
await taskApi.updateTaskProgress(task.id!, newProgress)
|
||
}
|
||
message.success('进度更新成功')
|
||
loadInProgressTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
message.error(error.message || '进度更新失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 完结任务(支持普通任务和子任务)
|
||
// 编辑被驳回的任务
|
||
const handleEditRejectedTask = (task: Task) => {
|
||
// 跳转到创建任务页面,并传入任务ID作为编辑标识
|
||
router.push({
|
||
path: '/tasks/create',
|
||
query: {
|
||
editId: task.id,
|
||
mode: 'edit'
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleCompleteTask = (task: TaskOrSubTask) => {
|
||
let deliveryPath = ''
|
||
const isSubTask = task.isSubTask
|
||
const taskType = isSubTask ? '子任务' : '任务'
|
||
|
||
Modal.confirm({
|
||
title: `完结${taskType}`,
|
||
content: h('div', [
|
||
h('div', { style: 'margin-bottom: 12px;' }, `确认完结${taskType}:${task.title}`),
|
||
h('div', { style: 'margin-bottom: 12px;' }, '交付路径:'),
|
||
h(Textarea, {
|
||
rows: 4,
|
||
placeholder: '请输入交付路径或说明',
|
||
style: 'width: 100%',
|
||
onChange: (e: any) => { deliveryPath = e.target.value }
|
||
})
|
||
]),
|
||
onOk: async () => {
|
||
if (!deliveryPath.trim()) {
|
||
message.error('请输入交付路径')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
if (isSubTask) {
|
||
// 完结子任务
|
||
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath })
|
||
message.success('子任务已完成,等待下一个子任务解锁')
|
||
} else {
|
||
// 完结普通任务
|
||
await taskApi.completeTask(task.id!, { deliveryPath })
|
||
message.success('任务已提交审核')
|
||
}
|
||
loadInProgressTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
message.error(error.message || '任务完结失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 审核通过(使用两级审核流程)
|
||
const handleApproveTask = (task: Task) => {
|
||
// 如果是接单部门负责人审核,需要提供选择指派设计师的功能
|
||
if (task.currentApproverRole === 'receiving_leader') {
|
||
handleApproveTwoLevelTask(task)
|
||
return
|
||
}
|
||
|
||
// 需求部门负责人审核,直接通过即可
|
||
Modal.confirm({
|
||
title: '审核通过',
|
||
content: `确认通过任务审核:${task.title}?`,
|
||
onOk: async () => {
|
||
try {
|
||
const approvalData: TaskApprovalDTO = {
|
||
comment: '审核通过'
|
||
}
|
||
|
||
await taskApi.approveDemandLevel(task.id!, approvalData)
|
||
message.success('需求部门审核通过')
|
||
|
||
loadReviewTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
message.error(error.msg || error.message || '审核失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 驳回任务(使用两级审核流程)
|
||
const handleRejectTask = (task: Task) => {
|
||
let reason = ''
|
||
|
||
Modal.confirm({
|
||
title: '驳回任务',
|
||
content: h('div', [
|
||
h('div', { style: 'margin-bottom: 12px;' }, `驳回任务:${task.title}`),
|
||
h('div', { style: 'margin-bottom: 12px;' }, '驳回原因:'),
|
||
h(Textarea, {
|
||
rows: 4,
|
||
placeholder: '请输入驳回原因',
|
||
style: 'width: 100%',
|
||
onChange: (e: any) => { reason = e.target.value }
|
||
})
|
||
]),
|
||
onOk: async () => {
|
||
if (!reason.trim()) {
|
||
message.error('请输入驳回原因')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
// 根据当前审核角色调用不同的API
|
||
if (task.currentApproverRole === 'demand_leader') {
|
||
await taskApi.rejectByDemandLeader(task.id!, reason)
|
||
message.success('需求部门已驳回')
|
||
} else if (task.currentApproverRole === 'receiving_leader') {
|
||
await taskApi.rejectByReceivingLeader(task.id!, reason)
|
||
message.success('接单部门已驳回')
|
||
} else {
|
||
message.error('无法确定审核角色')
|
||
return Promise.reject()
|
||
}
|
||
|
||
loadReviewTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
message.error(error.msg || error.message || '驳回失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 查看任务详情(进行中/待审核)
|
||
const handleViewTaskDetail = (task: Task) => {
|
||
selectedTask.value = task
|
||
detailDrawerVisible.value = true
|
||
}
|
||
|
||
// 任务点击
|
||
const handleTaskClick = (task: TaskOrSubTask) => {
|
||
selectedTask.value = task
|
||
detailDrawerVisible.value = true
|
||
}
|
||
|
||
// 判断用户是否可以抢单
|
||
const canGrabTask = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
const userDeptId = userInfo.deptId
|
||
const userDeptType = userInfo.deptType // 'demand' 或 'receiving'
|
||
|
||
// 检查1:没有部门不能抢单
|
||
if (!userDeptId) {
|
||
return { canGrab: false, reason: '您未分配部门,无法接单' }
|
||
}
|
||
|
||
// 检查2:需求部门不能抢单
|
||
if (userDeptType === 'demand') {
|
||
return { canGrab: false, reason: '需求部门成员不能接单' }
|
||
}
|
||
|
||
// 检查3:部门负责人不能抢单
|
||
if (userInfo.isDepartmentLeader === 1 || userInfo.isDepartmentLeader === true) {
|
||
return { canGrab: false, reason: '部门负责人不能接单' }
|
||
}
|
||
|
||
// 检查4:验证部门是否允许接该需求类型(如果任务有需求类型限制)
|
||
if (task.demandTypeId) {
|
||
// 这里需要检查任务的需求类型是否允许当前用户的部门接单
|
||
// 由于前端没有完整的需求类型配置,暂时信任后端验证
|
||
}
|
||
|
||
return { canGrab: true, reason: '' }
|
||
}
|
||
|
||
// 抢单(支持普通任务和子任务)
|
||
const handleGrab = async (task: TaskOrSubTask) => {
|
||
const isSubTask = task.isSubTask
|
||
const taskType = isSubTask ? '子任务' : '任务'
|
||
const taskNo = isSubTask ? task.subTaskNo : task.taskNo
|
||
|
||
// 先检查权限
|
||
const { canGrab, reason } = canGrabTask(task)
|
||
if (!canGrab) {
|
||
message.warning(reason)
|
||
return
|
||
}
|
||
|
||
Modal.confirm({
|
||
title: `确认抢单${taskType}`,
|
||
content: `确定要抢单${taskType}"${task.title}"吗?${taskNo ? `(编号:${taskNo})` : ''}`,
|
||
okText: '确认',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
let response
|
||
if (isSubTask) {
|
||
// 抢子任务
|
||
response = await taskApi.grabTaskItem(task.itemId || task.id!)
|
||
} else {
|
||
// 抢普通任务
|
||
response = await taskApi.grabTask(task.id!)
|
||
}
|
||
|
||
if (response.code === 200) {
|
||
message.success(`${taskType}抢单成功!`)
|
||
loadTasks() // 刷新列表
|
||
loadAllTabCounts() // 刷新所有Tab数量
|
||
}
|
||
} catch (error: any) {
|
||
console.error('抢单失败:', error)
|
||
message.error(error.message || '抢单失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 从详情抽屉抢单
|
||
const handleGrabFromDetail = async () => {
|
||
if (!selectedTask.value) return
|
||
await handleGrab(selectedTask.value)
|
||
detailDrawerVisible.value = false
|
||
}
|
||
|
||
// 监听配置更新事件
|
||
const handleConfigUpdate = async (event: CustomEvent) => {
|
||
console.log('配置已更新,刷新配置数据:', event.detail)
|
||
await loadConfigs()
|
||
}
|
||
|
||
// 组件挂载
|
||
// 获取任务状态文本
|
||
const getTaskStatusText = (status: string) => {
|
||
const statusMap: Record<string, string> = {
|
||
'pending': '待接单',
|
||
'in_progress': '进行中',
|
||
'review': '待审核',
|
||
'completed': '已完成',
|
||
'rejected': '已驳回',
|
||
'cancelled': '已取消'
|
||
}
|
||
return statusMap[status] || status
|
||
}
|
||
|
||
// 加载所有Tab的数量统计
|
||
const loadAllTabCounts = async () => {
|
||
try {
|
||
// 加载进行中任务数量
|
||
const inProgressRes = await taskApi.getTasks({
|
||
status: 'in_progress',
|
||
role: 'receiver',
|
||
page: 1,
|
||
pageSize: 1
|
||
})
|
||
if (inProgressRes.data && inProgressRes.data.total !== undefined) {
|
||
inProgressTotal.value = inProgressRes.data.total
|
||
}
|
||
|
||
// 加载待审核任务数量
|
||
const reviewRes = await taskApi.getAllPendingApprovalTasks({
|
||
page: 1,
|
||
pageSize: 1
|
||
})
|
||
if (reviewRes.data && reviewRes.data.total !== undefined) {
|
||
reviewTotal.value = reviewRes.data.total
|
||
}
|
||
|
||
// 加载待我审核任务数量(仅部门负责人)
|
||
if (isDepartmentLeader.value) {
|
||
const pendingApprovalRes = await taskApi.getPendingApprovalTasks()
|
||
if (pendingApprovalRes.code === 200) {
|
||
pendingApprovalTotal.value = (pendingApprovalRes.data || []).length
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载Tab数量统计失败:', error)
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadConfigs()
|
||
loadDashboardStats()
|
||
loadAllTabCounts() // 加载所有Tab的数量统计
|
||
loadTasks()
|
||
// 监听配置更新事件
|
||
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
|
||
})
|
||
|
||
// 组件卸载
|
||
onUnmounted(() => {
|
||
window.removeEventListener('configUpdated', handleConfigUpdate as EventListener)
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.task-hall-page {
|
||
min-height: 100vh;
|
||
padding: $space-xl 0;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
// 数据总览样式
|
||
.overview-section {
|
||
margin-bottom: $space-xl;
|
||
|
||
.overview-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $space-lg;
|
||
|
||
.section-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.overview-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 数据卡片网格布局
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 16px;
|
||
|
||
@media (max-width: 1400px) {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
|
||
@media (max-width: 992px) {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
@media (max-width: 576px) {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.stat-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-lg;
|
||
padding: $space-xl;
|
||
background: $bg-card;
|
||
border-radius: $radius-lg;
|
||
border: 1px solid $border-primary;
|
||
transition: all $transition-base $ease-out;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
border-color: $border-color;
|
||
background: $bg-card-hover;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.stat-icon {
|
||
flex-shrink: 0;
|
||
width: 48px;
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border-radius: $radius-md;
|
||
}
|
||
|
||
.stat-content {
|
||
flex: 1;
|
||
|
||
.stat-label {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
line-height: 1.2;
|
||
}
|
||
}
|
||
}
|
||
|
||
.container {
|
||
max-width: $container-max-width;
|
||
margin: 0 auto;
|
||
padding: 0 $space-xl;
|
||
|
||
@media (max-width: $breakpoint-tablet) {
|
||
padding: 0 $space-lg;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
padding: 0 $space-md;
|
||
}
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: $space-2xl;
|
||
|
||
button {
|
||
min-width: 140px;
|
||
}
|
||
}
|
||
|
||
// 图表卡片 - Cursor 风格
|
||
.chart-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
padding: $space-xl;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
&:hover {
|
||
border-color: $border-color;
|
||
background: $bg-card-hover;
|
||
}
|
||
}
|
||
|
||
// 标签页样式统一
|
||
:deep(.ant-tabs) {
|
||
.ant-tabs-nav {
|
||
margin-bottom: $space-xl;
|
||
|
||
&::before {
|
||
border-bottom: 1px solid $border-primary;
|
||
}
|
||
}
|
||
|
||
.ant-tabs-tab {
|
||
padding: $space-md $space-xl;
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-medium;
|
||
color: $text-secondary;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
&:hover {
|
||
color: $text-primary;
|
||
}
|
||
|
||
&.ant-tabs-tab-active {
|
||
.ant-tabs-tab-btn {
|
||
color: #31f08b;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
}
|
||
|
||
// 标签页数量样式
|
||
.tab-count {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
font-weight: $font-weight-normal;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
&.ant-tabs-tab-active .tab-count {
|
||
color: rgba(49, 240, 139, 0.7);
|
||
}
|
||
}
|
||
|
||
.ant-tabs-ink-bar {
|
||
background: #31f08b;
|
||
height: 3px;
|
||
}
|
||
|
||
.ant-tabs-content {
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
// 搜索和筛选栏(合并)
|
||
.search-filter-bar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-md;
|
||
margin-bottom: $space-xl;
|
||
padding: $space-lg;
|
||
}
|
||
|
||
// 搜索行(搜索、排序、统计)
|
||
.search-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
width: 100%;
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
min-width: 300px;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.sort-select {
|
||
width: 150px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.stats-text {
|
||
font-size: $font-size-base;
|
||
color: $text-secondary;
|
||
white-space: nowrap;
|
||
margin-left: auto;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
flex-wrap: wrap;
|
||
|
||
.stats-text {
|
||
margin-left: 0;
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 筛选行(紧凑布局)
|
||
.filter-row {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: $space-sm;
|
||
width: 100%;
|
||
|
||
.filter-select {
|
||
width: 140px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.score-range {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-xs;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.score-input {
|
||
width: 100px;
|
||
}
|
||
|
||
.score-separator {
|
||
color: $text-secondary;
|
||
font-size: $font-size-base;
|
||
padding: 0 $space-xs;
|
||
}
|
||
|
||
.reset-btn {
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
.filter-select {
|
||
width: calc(50% - #{$space-sm / 2});
|
||
}
|
||
|
||
.score-range {
|
||
width: 100%;
|
||
margin-top: $space-xs;
|
||
}
|
||
|
||
.reset-btn {
|
||
margin-left: 0;
|
||
width: 100%;
|
||
margin-top: $space-xs;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载状态
|
||
.loading-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 400px;
|
||
}
|
||
|
||
// 空状态
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: $space-xxxl;
|
||
|
||
.empty-icon {
|
||
font-size: 64px;
|
||
margin-bottom: $space-lg;
|
||
}
|
||
|
||
h3 {
|
||
font-size: $font-size-2xl;
|
||
color: $text-primary;
|
||
margin-bottom: $space-sm;
|
||
}
|
||
|
||
p {
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
// 任务网格
|
||
.task-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||
gap: $space-lg;
|
||
margin-bottom: $space-xl;
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
// 任务卡片
|
||
.task-card {
|
||
transition: all 0.2s ease-out;
|
||
cursor: pointer;
|
||
margin-bottom: 16px;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
// 审核任务卡片特殊样式
|
||
.task-info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
margin: 16px 0;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.info-label {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
|
||
&.score-value {
|
||
color: #31f08b;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.task-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
}
|
||
|
||
.task-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
// 流程阶段
|
||
.process-stage {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-xs;
|
||
padding: $space-sm $space-md;
|
||
margin-bottom: $space-md;
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
border-radius: $radius-md;
|
||
font-size: $font-size-sm;
|
||
color: #31f08b;
|
||
|
||
.stage-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.stage-text {
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
gap: $space-sm;
|
||
}
|
||
|
||
.task-badges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: $space-xs;
|
||
align-items: center;
|
||
}
|
||
|
||
.task-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4px 8px;
|
||
border-radius: $radius-small;
|
||
font-size: $font-size-xs;
|
||
font-weight: $font-weight-medium;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.badge-subtask {
|
||
background: rgba(24, 144, 255, 0.1);
|
||
border: 1px solid rgba(24, 144, 255, 0.3);
|
||
color: #1890ff;
|
||
}
|
||
|
||
.badge-new {
|
||
background: rgba($color-success, 0.1);
|
||
border: 1px solid rgba($color-success, 0.3);
|
||
color: $color-success;
|
||
}
|
||
|
||
.badge-high-score {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
color: #31f08b;
|
||
}
|
||
|
||
.badge-urgent {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
color: #31f08b;
|
||
}
|
||
|
||
.badge-completed {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
// 任务信息
|
||
.task-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: $space-md;
|
||
margin-bottom: $space-md;
|
||
padding-bottom: $space-md;
|
||
border-bottom: 1px solid $border-primary;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.label {
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
.value {
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-medium;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
// 任务详情
|
||
.task-details {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: $space-sm;
|
||
margin-bottom: $space-md;
|
||
|
||
// 响应式:小屏幕时改为单列
|
||
@media (max-width: 768px) {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
min-width: 0; // 允许flex子项收缩
|
||
|
||
.icon {
|
||
font-size: $font-size-base;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
// 确保文本不会换行,保持紧凑
|
||
span:not(.icon) {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
// 附件项需要跨两列(因为可能包含多个链接)
|
||
&.detail-item-attachment {
|
||
grid-column: 1 / -1;
|
||
|
||
span:not(.icon) {
|
||
white-space: normal;
|
||
overflow: visible;
|
||
text-overflow: clip;
|
||
}
|
||
}
|
||
|
||
// 子任务进度样式
|
||
&.subtask-progress {
|
||
grid-column: 1 / -1;
|
||
|
||
.subtask-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
white-space: normal;
|
||
overflow: visible;
|
||
|
||
.stat {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
|
||
&.completed {
|
||
background: rgba(82, 196, 26, 0.1);
|
||
color: #52c41a;
|
||
}
|
||
|
||
&.in-progress {
|
||
background: rgba(24, 144, 255, 0.1);
|
||
color: #1890ff;
|
||
}
|
||
|
||
&.pending {
|
||
background: rgba(250, 173, 20, 0.1);
|
||
color: #faad14;
|
||
}
|
||
|
||
&.total {
|
||
color: #999;
|
||
padding: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 任务操作
|
||
.task-actions {
|
||
display: flex;
|
||
gap: $space-sm;
|
||
}
|
||
|
||
// 任务详情抽屉
|
||
.task-detail {
|
||
.detail-section {
|
||
margin-bottom: $space-xl;
|
||
|
||
h3 {
|
||
font-size: $font-size-2xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
h4 {
|
||
font-size: $font-size-lg;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
p {
|
||
color: $text-secondary;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.detail-badges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: $space-xs;
|
||
margin-top: $space-sm;
|
||
}
|
||
|
||
.detail-actions {
|
||
margin-top: $space-2xl;
|
||
padding-top: $space-xl;
|
||
border-top: 1px solid $border-primary;
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: $space-md;
|
||
|
||
button {
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.attachment-links {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-sm;
|
||
|
||
.attachment-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: $space-sm $space-md;
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-md;
|
||
color: $text-primary;
|
||
text-decoration: none;
|
||
transition: all $transition-base;
|
||
|
||
&:hover {
|
||
background: $bg-card-hover;
|
||
border-color: #31f08b;
|
||
color: #31f08b;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分页
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: $space-xl;
|
||
}
|
||
|
||
// 任务进度条
|
||
.task-progress {
|
||
margin: $space-md 0;
|
||
padding: $space-md;
|
||
background: rgba(49, 240, 139, 0.05);
|
||
border-radius: $radius-md;
|
||
|
||
.progress-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: $space-sm;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
|
||
span:last-child {
|
||
font-weight: $font-weight-medium;
|
||
color: #31f08b;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 任务操作按钮
|
||
.task-actions {
|
||
display: flex;
|
||
gap: $space-sm;
|
||
padding-top: $space-md;
|
||
border-top: 1px solid $border-primary;
|
||
margin-top: $space-md;
|
||
|
||
button {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
// 任务卡片特定样式 - 统一简洁风格,不使用彩色边框
|
||
.task-card.in-progress,
|
||
.task-card.review,
|
||
.task-card.approval-card {
|
||
// 移除彩色边框,保持统一灰色底色
|
||
}
|
||
|
||
// badge样式补充 - 统一使用绿色主题
|
||
.badge-in-progress {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
.badge-review {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
.badge-approval {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
// 按钮样式
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: $radius-md;
|
||
font-size: $font-size-sm;
|
||
font-weight: $font-weight-medium;
|
||
cursor: pointer;
|
||
transition: all $transition-base;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #31f08b;
|
||
color: #000;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: #28d477;
|
||
transform: translateY(-1px);
|
||
}
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
color: $text-primary;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: $bg-card-hover;
|
||
border-color: $border-color;
|
||
}
|
||
}
|
||
|
||
.btn-danger {
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
color: $text-secondary;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: $bg-card-hover;
|
||
border-color: $border-color;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
// 内联附件链接样式
|
||
.task-details {
|
||
.attachment-link {
|
||
color: #31f08b;
|
||
text-decoration: none;
|
||
margin-left: 4px;
|
||
margin-right: 8px;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|