6288 lines
205 KiB
Vue
6288 lines
205 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-wrapper">
|
||
<div class="task-info">
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ task.totalScore || task.score || '--' }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || 1 }} 个</span>
|
||
</div>
|
||
</div>
|
||
<!-- 预览图 -->
|
||
<div v-if="task.previewImage" class="preview-thumbnail">
|
||
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
|
||
<div class="preview-popup">
|
||
<img :src="task.previewImage" alt="预览图" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务类型明细 -->
|
||
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
|
||
<div class="type-details-title">任务类型明细</div>
|
||
<div class="type-details-list">
|
||
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
|
||
<span class="type-name">└ {{ detail.typeName }}</span>
|
||
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div 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>时效要求: {{ formatEstimatedTime(task.estimatedHours) }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发布时间: {{ formatTime(task.createTime) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="task-actions">
|
||
<button
|
||
class="btn btn-secondary"
|
||
style="flex: 1"
|
||
@click.stop="handleTaskClick(task)"
|
||
>
|
||
查看详情
|
||
</button>
|
||
<button
|
||
:class="[
|
||
'btn',
|
||
canGrabTask(task).canGrab ? 'btn-primary' : 'btn-disabled'
|
||
]"
|
||
:style="{
|
||
flex: 1,
|
||
backgroundColor: canGrabTask(task).canGrab ? '#31f08b' : '#666',
|
||
cursor: canGrabTask(task).canGrab ? 'pointer' : 'not-allowed',
|
||
opacity: canGrabTask(task).canGrab ? 1 : 0.6
|
||
}"
|
||
:disabled="!canGrabTask(task).canGrab"
|
||
:title="canGrabTask(task).canGrab ? '点击抢单' : canGrabTask(task).reason"
|
||
@click.stop="handleGrab(task)"
|
||
>
|
||
{{ canGrabTask(task).canGrab ? '抢单' : '不可接单' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 任务编号 -->
|
||
<div class="task-footer">
|
||
<div class="footer-item task-no-item">
|
||
<Icon name="hash" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">编号: {{ task.isSubTask ? task.itemNo : task.taskNo }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="total > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="total"
|
||
:show-size-changer="false"
|
||
@change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 进行中任务标签页 -->
|
||
<a-tab-pane key="review">
|
||
<template #tab>
|
||
<span>待审核任务 <span class="tab-count">({{ reviewTotal }})</span></span>
|
||
</template>
|
||
<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 v-if="task._itemType === 'taskGroup'" class="badge badge-subtask">
|
||
<Icon name="layers" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
子任务组 - 第{{ task._groupData?.groupNo }}组
|
||
</span>
|
||
<span v-else class="badge badge-review">
|
||
<Icon name="file-text" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
待审核
|
||
</span>
|
||
<!-- 加急图标 -->
|
||
<span v-if="task.isUrgent" class="badge badge-urgent">
|
||
<Icon name="fire" size="sm" color="#ff4d4f" style="margin-right: 4px; vertical-align: middle;" />
|
||
加急
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStageForReview(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStageForReview(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info-wrapper">
|
||
<div class="task-info task-info-3col">
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ task.totalScore || task.score || 0 }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">{{ task._itemType === 'taskGroup' ? '子任务数' : '数量' }}</span>
|
||
<span class="value">{{ task._itemType === 'taskGroup' ? (task._groupData?.itemCount || 1) : (task.quantity || '--') }} 个</span>
|
||
</div>
|
||
</div>
|
||
<!-- 预览图 -->
|
||
<div v-if="task.previewImage" class="preview-thumbnail">
|
||
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
|
||
<div class="preview-popup">
|
||
<img :src="task.previewImage" alt="预览图" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务类型明细 -->
|
||
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
|
||
<div class="type-details-title">任务类型明细</div>
|
||
<div class="type-details-list">
|
||
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
|
||
<span class="type-name">└ {{ detail.typeName }}</span>
|
||
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.publisherName || '--' }}</span>
|
||
</div>
|
||
<div v-if="task._itemType !== 'taskGroup'" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>接单人: {{ task.receiverName || '--' }}</span>
|
||
</div>
|
||
<div v-if="task.brandName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="tag" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>品牌: {{ task.brandName }}</span>
|
||
</div>
|
||
<div v-if="task.categoryName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="folder" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>类目: {{ task.categoryName }}</span>
|
||
</div>
|
||
<div v-if="task.platformName" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="monitor" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>平台: {{ task.platformName }}</span>
|
||
</div>
|
||
<div v-if="task.spuInfo" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="package" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>SPU: {{ task.spuInfo }}</span>
|
||
</div>
|
||
<div v-if="task.taskSubType" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="list" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>子类型: {{ task.taskSubType }}</span>
|
||
</div>
|
||
<div v-if="task.deliveryPath" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="link" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>交付路径: {{ task.deliveryPath }}</span>
|
||
</div>
|
||
<div v-if="task.attachmentUrl" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="paperclip" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>附件:
|
||
<a
|
||
v-for="(url, index) in (task.attachmentUrl || '').split(',').filter(u => u.trim())"
|
||
:key="index"
|
||
:href="url.trim()"
|
||
target="_blank"
|
||
class="attachment-link"
|
||
@click.stop
|
||
>
|
||
附件{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
<!-- 前置素材下载(流水线继承) -->
|
||
<div v-if="task._itemType === 'taskGroup' && task.prevDownloadUrls && task.prevDownloadUrls.length > 0" class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="download" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>前置素材:
|
||
<a
|
||
v-for="(url, index) in task.prevDownloadUrls"
|
||
:key="index"
|
||
:href="url"
|
||
target="_blank"
|
||
class="attachment-link download-link"
|
||
@click.stop
|
||
>
|
||
下载{{ index + 1 }}
|
||
</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务编号(底部) -->
|
||
<div class="task-no-section">
|
||
<Icon name="hash" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">{{ task.taskNo || '--' }}</span>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="task-actions">
|
||
<template v-if="task._itemType === 'taskGroup'">
|
||
<a-button type="primary" size="small" @click="handleApproveTaskGroupFromReview(task)">
|
||
<Icon name="check" size="sm" style="margin-right: 4px;" />
|
||
审核通过
|
||
</a-button>
|
||
<a-button danger size="small" @click="handleRejectTaskGroupFromReview(task)">
|
||
<Icon name="x" size="sm" style="margin-right: 4px;" />
|
||
驳回
|
||
</a-button>
|
||
</template>
|
||
<template v-else>
|
||
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
|
||
<a-button v-if="canApproveTask(task)" type="primary" size="small" @click="handleApproveTask(task)">
|
||
通过审核
|
||
</a-button>
|
||
<a-button v-if="canRejectTask(task)" danger size="small" @click="handleRejectTask(task)">
|
||
驳回任务
|
||
</a-button>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 时间信息 -->
|
||
<div class="task-footer">
|
||
<div class="footer-item">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">提交: {{ formatDate(task.submitTime || task.updateTime || task.createTime) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="reviewTotal > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="reviewPagination.page"
|
||
v-model:page-size="reviewPagination.pageSize"
|
||
:total="reviewTotal"
|
||
:show-size-changer="false"
|
||
@change="handleReviewPageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 已完成任务标签页 -->
|
||
<a-tab-pane key="inProgress">
|
||
<template #tab>
|
||
<span>进行中任务 <span class="tab-count">({{ inProgressTotal }})</span></span>
|
||
</template>
|
||
<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 v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
|
||
<Icon name="target" 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-wrapper">
|
||
<div class="task-info task-info-3col">
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">预估积分</span>
|
||
<span class="value text-brand">{{ task.totalScore || task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
<!-- 预览图 -->
|
||
<div v-if="task.previewImage" class="preview-thumbnail">
|
||
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
|
||
<div class="preview-popup">
|
||
<img :src="task.previewImage" alt="预览图" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务类型明细 -->
|
||
<div v-if="parseTypeDetails(task.typeDetails).length > 0" class="type-details-section">
|
||
<div class="type-details-title">任务类型明细</div>
|
||
<div class="type-details-list">
|
||
<div v-for="(detail, idx) in parseTypeDetails(task.typeDetails)" :key="idx" class="type-detail-item">
|
||
<span class="type-name">└ {{ detail.typeName }}</span>
|
||
<span class="type-score">x{{ detail.quantity }} = {{ detail.totalScore }}分</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div class="task-details">
|
||
<div class="detail-item">
|
||
<span class="icon">
|
||
<Icon name="user" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>发起人: {{ task.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="task.prevDownloadUrl"
|
||
type="default"
|
||
size="small"
|
||
@click.stop="handleDownloadPrevAssets(task)"
|
||
class="prev-assets-btn"
|
||
>
|
||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||
上一环节素材
|
||
</a-button>
|
||
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click="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 task-no-item">
|
||
<Icon name="hash" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">编号: {{ task.isSubTask ? task.itemNo : task.taskNo }}</span>
|
||
</div>
|
||
<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.estimatedHours">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">时效要求: {{ formatEstimatedTime(task.estimatedHours) }}</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"
|
||
@click="handleTaskClick(task)"
|
||
style="cursor: pointer;"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span class="badge badge-completed">
|
||
<Icon name="check-circle" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||
已完成
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info-wrapper">
|
||
<div class="task-info task-info-3col">
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">获得积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
<!-- 预览图 -->
|
||
<div v-if="task.previewImage" class="preview-thumbnail">
|
||
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
|
||
<div class="preview-popup">
|
||
<img :src="task.previewImage" alt="预览图" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div 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}`]"
|
||
@click="handleTaskClick(task)"
|
||
style="cursor: pointer;"
|
||
>
|
||
<!-- 任务标题 -->
|
||
<div class="task-header">
|
||
<h3 class="task-title">{{ task.title }}</h3>
|
||
<div class="task-badges">
|
||
<span :class="['badge', getTaskStatusClass(task)]">
|
||
{{ getTaskStatusText(task.status, task) }}
|
||
</span>
|
||
<span v-if="task.releaseType === 'assigned'" class="badge badge-assigned">
|
||
<Icon name="target" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
|
||
指定
|
||
</span>
|
||
<span v-if="task.isUrgent" class="badge badge-urgent">
|
||
<Icon name="fire" size="sm" color="#ff4d4f" style="margin-right: 4px; vertical-align: middle;" />
|
||
紧急
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 驳回原因显示 -->
|
||
<div v-if="task.approvalStatus?.includes('rejected') && task.rejectReason" class="reject-info">
|
||
<span class="reject-label">驳回原因:</span>
|
||
<span class="reject-reason">{{ task.rejectReason }}</span>
|
||
</div>
|
||
|
||
<!-- 流程阶段 -->
|
||
<div v-if="getProcessStage(task)" class="process-stage">
|
||
<span class="stage-icon">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span class="stage-text">{{ getProcessStage(task) }}</span>
|
||
</div>
|
||
|
||
<!-- 任务信息 -->
|
||
<div class="task-info-wrapper">
|
||
<div class="task-info task-info-3col">
|
||
<div class="info-item">
|
||
<span class="label">任务类型</span>
|
||
<span class="value">{{ getTaskTypeName(task.taskTypeId, task.taskTypeName, task.taskSubType) }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">积分</span>
|
||
<span class="value text-brand">{{ task.score }} 分</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">数量</span>
|
||
<span class="value">{{ task.quantity || '--' }} 个</span>
|
||
</div>
|
||
</div>
|
||
<!-- 预览图 -->
|
||
<div v-if="task.previewImage" class="preview-thumbnail">
|
||
<img :src="task.previewImage" alt="预览图" class="thumbnail-img" />
|
||
<div class="preview-popup">
|
||
<img :src="task.previewImage" alt="预览图" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 子任务进度时间线(流水线任务)- 横向拉满 -->
|
||
<div v-if="task.isPipeline === 1 && task.subTaskTotal" class="subtask-timeline-wrapper full-width">
|
||
<div class="subtask-timeline">
|
||
<div
|
||
v-for="(subTask, index) in (task.subTasks || []).slice(0, 5)"
|
||
:key="subTask.id || index"
|
||
class="timeline-node"
|
||
:class="getSubTaskStatusClass(subTask.status)"
|
||
>
|
||
<div class="node-dot"></div>
|
||
<div class="node-label">{{ getSubTaskLabel(subTask, index) }}</div>
|
||
<div class="node-status">{{ getSubTaskStatusText(subTask.status) }}</div>
|
||
</div>
|
||
<!-- 显示更多子任务提示 -->
|
||
<div v-if="(task.subTasks || []).length > 5" class="timeline-node more">
|
||
<div class="node-dot"></div>
|
||
<div class="node-label">+{{ (task.subTasks || []).length - 5 }}</div>
|
||
</div>
|
||
<!-- 完结节点 -->
|
||
<div class="timeline-node final" :class="{ completed: task.status === 'completed' }">
|
||
<div class="node-dot"></div>
|
||
<div class="node-label">完结</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务详情 -->
|
||
<div 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.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;">发布: {{ formatTime(task.createTime) }}</span>
|
||
</div>
|
||
<div class="footer-item" v-if="task.estimatedHours">
|
||
<Icon name="clock" size="sm" color="#31f08b" />
|
||
<span style="color: #ffffff;">时效要求: {{ formatEstimatedTime(task.estimatedHours) }}</span>
|
||
</div>
|
||
<!-- 驳回时间 -->
|
||
<div class="footer-item" v-if="task.approvalStatus?.includes('rejected') && task.updateTime">
|
||
<Icon name="x-circle" size="sm" color="#f5222d" />
|
||
<span style="color: #f5222d;">驳回: {{ formatTime(task.updateTime) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下载素材按钮(已完成任务) -->
|
||
<div v-if="task.status === 'completed'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
|
||
<a-button
|
||
v-if="task.downloadUrl || task.deliveryPath"
|
||
type="primary"
|
||
size="small"
|
||
@click.stop="handleDownloadAssets(task)"
|
||
style="width: 100%;">
|
||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||
下载素材
|
||
</a-button>
|
||
<a-button
|
||
v-else
|
||
type="default"
|
||
size="small"
|
||
disabled
|
||
style="width: 100%; opacity: 0.5;">
|
||
<Icon name="download" size="sm" style="margin-right: 4px;" />
|
||
暂无素材
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 被驳回或草稿任务的操作按钮 -->
|
||
<div v-if="['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
|
||
<a-button
|
||
type="primary"
|
||
size="small"
|
||
@click.stop="handleEditRejectedTask(task)"
|
||
style="width: 100%;">
|
||
<Icon name="edit" size="sm" style="margin-right: 4px;" />
|
||
重新编辑提交
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 已完成任务:查看详情和交付信息 -->
|
||
<div v-if="task.status === 'completed'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
|
||
<a-button
|
||
type="primary"
|
||
size="small"
|
||
@click.stop="handleTaskClick(task)"
|
||
style="width: 100%;">
|
||
<Icon name="eye" size="sm" style="margin-right: 4px;" />
|
||
查看详情
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="publishedTotal > 0" class="pagination">
|
||
<a-pagination
|
||
v-model:current="publishedPagination.page"
|
||
v-model:page-size="publishedPagination.pageSize"
|
||
:total="publishedTotal"
|
||
:show-size-changer="false"
|
||
@change="handlePublishedPageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</div>
|
||
|
||
<!-- 审核通过弹窗 -->
|
||
<a-modal
|
||
v-model:visible="approveModalVisible"
|
||
title="接单部门审核通过"
|
||
:width="600"
|
||
:confirm-loading="approvalLoading"
|
||
@ok="handleApproveConfirm"
|
||
class="approve-modal"
|
||
:body-style="{ padding: '24px', background: '#1f1f1f' }"
|
||
>
|
||
<!-- 任务类型编辑 -->
|
||
<div class="modal-section">
|
||
<div class="section-header" style="margin-bottom: 12px;">
|
||
<span class="section-title" style="font-weight: 600; color: #fff;">编辑任务类型</span>
|
||
</div>
|
||
<div class="task-type-list" style="background: #141414; padding: 12px; border-radius: 8px;">
|
||
<div v-for="(item, index) in approveTaskTypes" :key="index" class="task-type-item" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; padding: 12px; background: #262626; border-radius: 6px;">
|
||
<div class="type-info" style="flex: 1; min-width: 0;">
|
||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||
<span style="color: #fff; font-weight: 500; font-size: 14px;">{{ getTaskTypeFullPath(item.typeId)?.name || getTaskTypeName(item.typeId) }}</span>
|
||
<span style="color: #888; font-size: 12px;">{{ getTaskTypeFullPath(item.typeId)?.fullPath || '' }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="type-controls" style="display: flex; align-items: center; gap: 12px;">
|
||
<div style="display: flex; align-items: center; background: #31f08b22; padding: 4px 10px; border-radius: 4px;">
|
||
<span style="color: #31f08b; font-weight: 600; font-size: 14px;">{{ getTaskTypeUnitScore(item.typeId) }} 分/个</span>
|
||
</div>
|
||
<div style="display: flex; align-items: center;">
|
||
<span class="label" style="color: #888; margin-right: 8px; font-size: 12px;">数量</span>
|
||
<a-input-number
|
||
v-model:value="item.quantity"
|
||
:min="1"
|
||
size="small"
|
||
style="width: 70px;"
|
||
/>
|
||
</div>
|
||
<a-button
|
||
type="text"
|
||
danger
|
||
size="small"
|
||
@click="approveTaskTypes.splice(index, 1)"
|
||
>
|
||
<Icon name="trash-2" size="sm" />
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加类型(使用级联选择器) -->
|
||
<div class="add-type-row" style="margin-top: 12px; display: flex; gap: 8px;">
|
||
<a-cascader
|
||
v-model:value="newTaskTypePath"
|
||
:options="taskTypeCascaderOptions"
|
||
placeholder="选择要添加的任务类型"
|
||
style="flex: 1"
|
||
allow-clear
|
||
change-on-select
|
||
:show-search="{ filter: cascaderFilter }"
|
||
popup-class-name="dark-cascader-popup"
|
||
/>
|
||
<a-button type="primary" ghost style="border-color: #31f08b; color: #31f08b;" @click="handleAddApproveTaskType">
|
||
<Icon name="plus" size="sm" style="margin-right: 4px" /> 添加
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 预估积分显示 -->
|
||
<div class="score-summary" style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #1a3a2a 0%, #0d1f16 100%); border-radius: 8px; border: 1px solid #31f08b33;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<span style="color: #888; font-size: 14px;">预估总积分</span>
|
||
<span style="color: #31f08b; font-size: 24px; font-weight: 600;">{{ approveEstimatedScore }} 分</span>
|
||
</div>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #666;">
|
||
<span v-for="(item, index) in approveTaskTypes" :key="index" style="margin-right: 12px;">
|
||
{{ getTaskTypeName(item.typeId) }}: {{ getTaskTypeUnitScore(item.typeId) }} × {{ item.quantity }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 接单方式 -->
|
||
<div class="modal-section" style="margin-top: 24px;">
|
||
<div class="section-header" style="margin-bottom: 12px;">
|
||
<span class="section-title" style="font-weight: 600; color: #fff;">选择接单方式</span>
|
||
<span v-if="isTaskPreAssigned" style="margin-left: 8px; color: #faad14; font-size: 12px;">
|
||
(任务已预先指定设计师,不可修改)
|
||
</span>
|
||
</div>
|
||
<!-- 已预先指定设计师,显示只读信息 -->
|
||
<div v-if="isTaskPreAssigned" class="pre-assigned-info" style="padding: 16px; border: 1px solid #52c41a; border-radius: 8px; background: rgba(82, 196, 26, 0.1);">
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<Icon name="check-circle" size="lg" color="#52c41a" />
|
||
<span style="color: #fff; font-weight: 500;">已预先指定设计师接单</span>
|
||
</div>
|
||
<div style="margin-top: 12px; color: #888;">
|
||
<span>指定设计师:</span>
|
||
<span style="color: #31f08b; font-weight: 600;">{{ preAssignedDesignerName }}</span>
|
||
</div>
|
||
</div>
|
||
<!-- 未预先指定,显示选择界面 -->
|
||
<a-radio-group v-else v-model:value="approveAssignType" class="assign-type-group" style="width: 100%; display: flex; gap: 12px;">
|
||
<a-radio value="auto" class="assign-option" style="flex: 1; display: flex; padding: 16px; border: 1px solid #303030; border-radius: 8px; background: #141414; align-items: center; margin-right: 0;">
|
||
<div class="option-content" style="display: inline-flex; flex-direction: column; margin-left: 8px;">
|
||
<span class="option-title" style="font-weight: 500; color: #fff; font-size: 14px;">发布到任务大厅</span>
|
||
<span class="option-desc" style="font-size: 12px; color: #888; margin-top: 4px;">所有设计师均可抢单</span>
|
||
</div>
|
||
</a-radio>
|
||
<a-radio value="manual" class="assign-option" style="flex: 1; display: flex; padding: 16px; border: 1px solid #303030; border-radius: 8px; background: #141414; align-items: center;">
|
||
<div class="option-content" style="display: inline-flex; flex-direction: column; margin-left: 8px;">
|
||
<span class="option-title" style="font-weight: 500; color: #fff; font-size: 14px;">指定设计师接单</span>
|
||
<span class="option-desc" style="font-size: 12px; color: #888; margin-top: 4px;">直接分配给特定设计师</span>
|
||
</div>
|
||
</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
<!-- 选择设计师 -->
|
||
<div v-if="approveAssignType === 'manual' && !isTaskPreAssigned" class="modal-section" style="margin-top: 24px;">
|
||
<div class="section-header" style="margin-bottom: 12px;">
|
||
<span class="section-title" style="font-weight: 600; color: #fff;">选择设计师</span>
|
||
</div>
|
||
<a-select
|
||
v-model:value="approveDesignerId"
|
||
placeholder="请选择设计师"
|
||
style="width: 100%"
|
||
size="large"
|
||
option-label-prop="label"
|
||
:dropdown-style="{ background: '#1f1f1f', borderColor: '#303030' }"
|
||
>
|
||
<a-select-option
|
||
v-for="designer in designersList"
|
||
:key="designer.id"
|
||
:value="designer.id"
|
||
:label="designer.realName"
|
||
>
|
||
<div class="designer-option" style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0;">
|
||
<span class="designer-name" style="color: #fff; font-weight: 500;">{{ designer.realName }}</span>
|
||
<div class="designer-stats">
|
||
<a-tag :color="designer.inProgressCount === 0 ? 'success' : designer.inProgressCount > 5 ? 'error' : 'warning'">
|
||
进行中: {{ designer.inProgressCount }}
|
||
</a-tag>
|
||
<span v-if="designer.earliestDeadline" style="font-size: 12px; color: #888; margin-left: 8px;">
|
||
最早完成: {{ formatDeadlineRelative(designer.earliestDeadline) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<!-- 任务详情抽屉 -->
|
||
<a-drawer
|
||
v-model:visible="detailDrawerVisible"
|
||
title="任务详情"
|
||
:width="600"
|
||
placement="right"
|
||
>
|
||
<div v-if="selectedTask" class="task-detail">
|
||
<div class="detail-section">
|
||
<h3>{{ selectedTask.title }}</h3>
|
||
<div class="detail-badges">
|
||
<span v-if="selectedTask.isSubTask" class="badge badge-subtask">子任务</span>
|
||
<span v-if="isNewTask(selectedTask)" class="badge badge-new">新发布</span>
|
||
<span v-if="isHighScoreTask(selectedTask)" class="badge badge-high-score">高分</span>
|
||
<span v-if="selectedTask.isUrgent" class="badge badge-urgent">紧急</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h4>基本信息</h4>
|
||
<a-descriptions :column="2" bordered>
|
||
<a-descriptions-item label="任务编号">
|
||
{{ selectedTask.isSubTask ? selectedTask.subTaskNo : selectedTask.taskNo }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item v-if="selectedTask.isSubTask" label="所属主任务">
|
||
{{ selectedTask.taskNo || '--' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="任务类型">{{ getTaskTypeName(selectedTask.taskTypeId) }}</a-descriptions-item>
|
||
<a-descriptions-item label="预估积分">{{ selectedTask.score }} 分</a-descriptions-item>
|
||
<a-descriptions-item label="时效要求">{{ formatEstimatedTime(selectedTask.estimatedHours) }}</a-descriptions-item>
|
||
<a-descriptions-item v-if="!selectedTask.isSubTask" label="数量">{{ selectedTask.quantity || '--' }} 个</a-descriptions-item>
|
||
</a-descriptions>
|
||
</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 v-if="selectedTask.deliveryPath && (selectedTask.status === 'completed' || selectedTask.status === 'review')" class="detail-section">
|
||
<h4>
|
||
<Icon name="link" size="sm" style="margin-right: 6px;" />
|
||
交付路径
|
||
</h4>
|
||
<div class="delivery-path">
|
||
<a v-if="selectedTask.deliveryPath.startsWith('http')"
|
||
:href="selectedTask.deliveryPath"
|
||
target="_blank"
|
||
class="delivery-link">
|
||
<Icon name="external-link" size="sm" style="margin-right: 4px;" />
|
||
{{ selectedTask.deliveryPath }}
|
||
</a>
|
||
<p v-else class="delivery-text">{{ selectedTask.deliveryPath }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品ID管理(已完成任务显示) -->
|
||
<div v-if="selectedTask.status === 'completed'" class="detail-section product-section">
|
||
<h4>
|
||
<Icon name="tag" size="sm" style="margin-right: 6px;" />
|
||
关联商品ID
|
||
<span class="product-count">({{ taskProducts.length }})</span>
|
||
</h4>
|
||
|
||
<!-- 商品ID列表 -->
|
||
<div v-if="taskProducts.length > 0" class="product-list">
|
||
<div v-for="product in taskProducts" :key="product.id" class="product-item">
|
||
<div class="product-info">
|
||
<span class="product-id">{{ product.productId }}</span>
|
||
<span v-if="product.productName" class="product-name">{{ product.productName }}</span>
|
||
</div>
|
||
<div class="product-meta">
|
||
<span class="creator">{{ product.creatorName }}</span>
|
||
<span class="time">{{ formatTime(product.createTime) }}</span>
|
||
</div>
|
||
<a-button
|
||
v-if="canManageProducts(selectedTask)"
|
||
type="text"
|
||
danger
|
||
size="small"
|
||
@click="handleDeleteProduct(product.id)">
|
||
<Icon name="trash" size="sm" />
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
<div v-else class="empty-products">
|
||
<Icon name="inbox" size="lg" style="opacity: 0.3;" />
|
||
<p>暂无关联商品ID</p>
|
||
</div>
|
||
|
||
<!-- 添加商品ID -->
|
||
<div v-if="canManageProducts(selectedTask)" class="add-product-form">
|
||
<a-input-group compact>
|
||
<a-input
|
||
v-model:value="newProductId"
|
||
placeholder="输入商品ID(数字)"
|
||
style="width: calc(100% - 80px);"
|
||
@pressEnter="handleAddProduct"
|
||
/>
|
||
<a-button type="primary" @click="handleAddProduct" :loading="addingProduct">
|
||
添加
|
||
</a-button>
|
||
</a-input-group>
|
||
<div class="batch-add">
|
||
<a-textarea
|
||
v-model:value="batchProductIds"
|
||
placeholder="批量添加:一行一个商品ID,或用逗号分隔"
|
||
:rows="2"
|
||
style="margin-top: 8px;"
|
||
/>
|
||
<a-button
|
||
type="default"
|
||
size="small"
|
||
style="margin-top: 8px;"
|
||
@click="handleBatchAddProducts"
|
||
:loading="addingProduct">
|
||
批量添加
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 确认完结按钮:只有添加了商品ID且未确认时显示 -->
|
||
<div v-if="canManageProducts(selectedTask) && taskProducts.length > 0 && !selectedTask.productIdFilled" class="confirm-complete-section">
|
||
<a-alert
|
||
message="请确认商品ID填写完成"
|
||
description="确认后任务状态将从「待填写ID」变更为「已完结」"
|
||
type="warning"
|
||
show-icon
|
||
style="margin-bottom: 12px;"
|
||
/>
|
||
<a-button
|
||
type="primary"
|
||
block
|
||
@click="handleConfirmProductIdFilled"
|
||
:loading="confirmingProductId">
|
||
<Icon name="checkCircle" size="sm" style="margin-right: 6px;" />
|
||
确认完结任务
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流水线任务:显示所有子任务交付路径 -->
|
||
<div v-if="selectedTask.isPipeline === 1 && selectedTask.allDeliveryPaths && selectedTask.allDeliveryPaths.length > 0" class="detail-section all-delivery-paths">
|
||
<h4>
|
||
<Icon name="folder" size="sm" style="margin-right: 6px;" />
|
||
所有子任务交付路径
|
||
</h4>
|
||
<div class="delivery-paths-list">
|
||
<div v-for="(path, index) in selectedTask.allDeliveryPaths" :key="index" class="delivery-path-item">
|
||
<a v-if="isUrl(path)" :href="extractUrl(path)" target="_blank" class="delivery-link">
|
||
<Icon name="link" size="sm" style="margin-right: 4px;" />
|
||
{{ path }}
|
||
</a>
|
||
<span v-else class="delivery-text">{{ path }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 根据任务状态显示不同的操作按钮 -->
|
||
<div class="detail-actions">
|
||
<!-- 可接任务:显示抢单 -->
|
||
<a-button
|
||
v-if="selectedTask.status === 'pending'"
|
||
:type="canGrabTask(selectedTask).canGrab ? 'primary' : 'default'"
|
||
:style="{
|
||
backgroundColor: canGrabTask(selectedTask).canGrab ? '#31f08b' : '#999',
|
||
borderColor: canGrabTask(selectedTask).canGrab ? '#31f08b' : '#999',
|
||
color: '#fff',
|
||
cursor: canGrabTask(selectedTask).canGrab ? 'pointer' : 'not-allowed',
|
||
opacity: canGrabTask(selectedTask).canGrab ? 1 : 0.6
|
||
}"
|
||
size="large"
|
||
block
|
||
:disabled="!canGrabTask(selectedTask).canGrab"
|
||
@click="handleGrabFromDetail">
|
||
{{ canGrabTask(selectedTask).canGrab ? '确认抢单' : `不可接单(${canGrabTask(selectedTask).reason})` }}
|
||
</a-button>
|
||
|
||
<!-- 进行中任务:显示更新进度和完结 -->
|
||
<div v-else-if="selectedTask.status === 'in_progress'" class="action-buttons">
|
||
<a-button
|
||
v-if="canUpdateProgress(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
@click="handleUpdateProgress(selectedTask); detailDrawerVisible = false">
|
||
更新进度
|
||
</a-button>
|
||
<a-button
|
||
v-if="canCompleteTask(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
@click="handleCompleteTask(selectedTask); detailDrawerVisible = false">
|
||
完结任务
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 待审核任务:显示审核按钮 -->
|
||
<div v-else-if="selectedTask.status === 'review'" class="action-buttons">
|
||
<a-button
|
||
v-if="canApproveTask(selectedTask)"
|
||
type="primary"
|
||
size="large"
|
||
@click="handleApproveTask(selectedTask); detailDrawerVisible = false">
|
||
通过审核
|
||
</a-button>
|
||
<a-button
|
||
v-if="canRejectTask(selectedTask)"
|
||
danger
|
||
size="large"
|
||
@click="handleRejectTask(selectedTask); detailDrawerVisible = false">
|
||
驳回任务
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 已完成任务:显示评分(如果还没评分) -->
|
||
<a-button
|
||
v-else-if="selectedTask.status === 'completed' && !selectedTask.rating && 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 * as configApi from '@/api/config'
|
||
import type { Task, TaskApprovalDTO, TaskProduct } from '@/api/task'
|
||
import type { DesignerWithWorkload } from '@/api/user'
|
||
import { useConfigStore } from '@/stores/config'
|
||
import dayjs from 'dayjs'
|
||
|
||
// 扩展Task类型以支持子任务字段
|
||
interface TaskTypeDetail {
|
||
typeId: number
|
||
typeName: string
|
||
quantity: number
|
||
unitScore: number
|
||
totalScore: number
|
||
}
|
||
|
||
interface TaskOrSubTask extends Task {
|
||
isSubTask?: boolean // 是否为子任务
|
||
itemId?: number // 子任务ID
|
||
itemNo?: string // 子任务编号
|
||
subTaskNo?: string // 子任务编号
|
||
groupNo?: number // 子任务组号
|
||
taskDescription?: string // 主任务描述
|
||
submitTime?: string // 提交时间
|
||
taskTypeName?: string // 任务类型名称(子任务直接返回)
|
||
prevDownloadUrl?: string // 前置子任务的素材下载链接
|
||
typeDetails?: string | TaskTypeDetail[] // 任务类型明细(JSON字符串或解析后的数组)
|
||
}
|
||
|
||
const router = useRouter()
|
||
|
||
// 全局配置store
|
||
const configStore = useConfigStore()
|
||
|
||
// 标签页
|
||
const activeTab = ref('available')
|
||
|
||
// 是否为部门负责人(从localStorage获取用户信息)
|
||
const isDepartmentLeader = computed(() => {
|
||
try {
|
||
const userStr = localStorage.getItem('userInfo')
|
||
if (!userStr) return false
|
||
const user = JSON.parse(userStr)
|
||
return user?.isDepartmentLeader === 1 || user?.role === 'dept_leader' || user?.role === 'admin'
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
// 状态
|
||
const loading = ref(false)
|
||
const taskList = ref<TaskOrSubTask[]>([])
|
||
const total = ref(0)
|
||
const detailDrawerVisible = ref(false)
|
||
const selectedTask = ref<TaskOrSubTask | null>(null)
|
||
|
||
// 商品ID管理
|
||
const taskProducts = ref<TaskProduct[]>([])
|
||
const newProductId = ref('')
|
||
const batchProductIds = ref('')
|
||
const addingProduct = ref(false)
|
||
const confirmingProductId = ref(false)
|
||
|
||
// 已完成任务状态
|
||
const completedLoading = ref(false)
|
||
const completedTaskList = ref<Task[]>([])
|
||
const completedTotal = ref(0)
|
||
|
||
// 我发起的任务状态
|
||
const publishedLoading = ref(false)
|
||
const publishedTaskList = ref<Task[]>([])
|
||
const publishedTotal = ref(0)
|
||
|
||
// 进行中任务状态
|
||
const 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 hierarchicalTaskTypes = ref<any[]>([])
|
||
|
||
// 递归查找任务类型(从层级数据中查找,支持三级分类)
|
||
const findTaskTypeById = (id: number, items: any[] = hierarchicalTaskTypes.value): any => {
|
||
for (const item of items) {
|
||
if (item.id === id) return item
|
||
if (item.children) {
|
||
const found = findTaskTypeById(id, item.children)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 获取任务类型的完整路径信息(包含父级名称和积分)
|
||
const getTaskTypeFullPath = (id: number, items: any[] = hierarchicalTaskTypes.value, path: string[] = []): { name: string, fullPath: string, unitScore: number } | null => {
|
||
for (const item of items) {
|
||
const currentPath = [...path, item.name]
|
||
if (item.id === id) {
|
||
return {
|
||
name: item.name,
|
||
fullPath: currentPath.join(' > '),
|
||
unitScore: item.unitScore || 0
|
||
}
|
||
}
|
||
if (item.children) {
|
||
const found = getTaskTypeFullPath(id, item.children, currentPath)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 构建级联选择器选项
|
||
const taskTypeCascaderOptions = computed(() => {
|
||
const buildOptions = (items: any[]): any[] => {
|
||
return items.map(item => {
|
||
const option: any = {
|
||
value: item.id,
|
||
label: item.name
|
||
}
|
||
if (item.children && item.children.length > 0) {
|
||
option.children = buildOptions(item.children)
|
||
}
|
||
return option
|
||
})
|
||
}
|
||
return buildOptions(hierarchicalTaskTypes.value)
|
||
})
|
||
|
||
// 筛选条件
|
||
const filters = reactive({
|
||
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, taskTypeName?: string, taskSubType?: string) => {
|
||
// 优先显示任务子类型(如:新品、优化、视频、实拍)
|
||
if (taskSubType) return taskSubType
|
||
// 如果有直接传入的类型名称,使用它
|
||
if (taskTypeName) return taskTypeName
|
||
// 从层级数据中递归查找(支持三级分类)
|
||
const typeInfo = findTaskTypeById(typeId)
|
||
if (typeInfo) return typeInfo.name
|
||
// 兼容:从平铺列表中查找
|
||
const type = demandTypes.value.find(t => t.id === typeId)
|
||
return type?.name || '未知类型'
|
||
}
|
||
|
||
// 解析任务类型明细
|
||
const parseTypeDetails = (typeDetails: string | TaskTypeDetail[] | undefined): TaskTypeDetail[] => {
|
||
if (!typeDetails) return []
|
||
if (Array.isArray(typeDetails)) return typeDetails
|
||
try {
|
||
return JSON.parse(typeDetails) as TaskTypeDetail[]
|
||
} catch (e) {
|
||
console.warn('解析任务类型明细失败:', e)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取任务类型单位积分
|
||
const getTaskTypeUnitScore = (typeId: number) => {
|
||
// 从层级数据中递归查找(支持三级分类)
|
||
const typeInfo = findTaskTypeById(typeId)
|
||
if (typeInfo && typeInfo.unitScore) return typeInfo.unitScore
|
||
// 兼容:从平铺列表中查找
|
||
const type = demandTypes.value.find(t => t.id === typeId)
|
||
return type?.unitScore || 0
|
||
}
|
||
|
||
// 计算审核弹窗中的预估总积分
|
||
const approveEstimatedScore = computed(() => {
|
||
return approveTaskTypes.value.reduce((total, item) => {
|
||
const unitScore = getTaskTypeUnitScore(item.typeId)
|
||
return total + (unitScore * item.quantity)
|
||
}, 0)
|
||
})
|
||
|
||
// 格式化时间
|
||
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')})`
|
||
}
|
||
}
|
||
|
||
// 格式化时效要求(工时转换为天+小时,8小时=1天)
|
||
const formatEstimatedTime = (hours: number | undefined) => {
|
||
if (!hours) return '--'
|
||
const WORK_HOURS_PER_DAY = 8
|
||
const days = Math.floor(hours / WORK_HOURS_PER_DAY)
|
||
const remainingHours = Math.round((hours % WORK_HOURS_PER_DAY) * 100) / 100
|
||
|
||
if (days === 0) {
|
||
return Number.isInteger(remainingHours) ? `${remainingHours}小时` : `${remainingHours.toFixed(1)}小时`
|
||
} else if (remainingHours === 0) {
|
||
return `${days}天`
|
||
} else {
|
||
const hourStr = Number.isInteger(remainingHours) ? `${remainingHours}小时` : `${remainingHours.toFixed(1)}小时`
|
||
return `${days}天${hourStr}`
|
||
}
|
||
}
|
||
|
||
// 子任务时间线辅助函数
|
||
const getSubTaskStatusClass = (status: string | undefined) => {
|
||
switch (status) {
|
||
case 'completed': return 'completed'
|
||
case 'in_progress': return 'in-progress'
|
||
default: return 'pending'
|
||
}
|
||
}
|
||
|
||
const getSubTaskLabel = (subTask: any, index: number) => {
|
||
// 如果有子任务编号,显示子任务编号后两位;否则显示序号
|
||
if (subTask.itemNo) {
|
||
const parts = subTask.itemNo.split('-')
|
||
return parts.length > 1 ? `子${parts[parts.length - 1]}` : `子${index + 1}`
|
||
}
|
||
return `子${index + 1}`
|
||
}
|
||
|
||
const getSubTaskStatusText = (status: string | undefined) => {
|
||
switch (status) {
|
||
case 'completed': return '已完成'
|
||
case 'in_progress': return '进行中'
|
||
default: return '待接单'
|
||
}
|
||
}
|
||
|
||
// 判断任务是否有子任务在进行中
|
||
const hasInProgressSubTask = (task: any) => {
|
||
if (task.isPipeline !== 1) return false
|
||
return (task.subTaskInProgress || 0) > 0
|
||
}
|
||
|
||
/**
|
||
* 获取任务流程阶段
|
||
* @param task 任务对象
|
||
* @returns 流程阶段文本
|
||
*/
|
||
const getProcessStage = (task: TaskOrSubTask): string => {
|
||
// 1. 需求负责人审核阶段
|
||
if (task.approvalStatus === 'pending_demand_approval' && task.currentApproverRole === 'demand_leader') {
|
||
return `需求负责人:${task.currentApproverName || '未知'}审核`
|
||
}
|
||
|
||
// 2. 设计负责人审核阶段
|
||
if (task.approvalStatus === 'pending_receiving_approval' && task.currentApproverRole === 'receiving_leader') {
|
||
return `设计负责人:${task.currentApproverName || '未知'}审核`
|
||
}
|
||
|
||
// 3. 任务被接单后:开始设计中
|
||
if (task.status === 'in_progress' && task.receiverId) {
|
||
return '开始设计中'
|
||
}
|
||
|
||
// 4. 任务被接单前:任务正在被抢单中
|
||
if (task.status === 'pending' && !task.receiverId) {
|
||
return '任务正在被抢单中'
|
||
}
|
||
|
||
// 5. 其他状态
|
||
if (task.status === 'review') {
|
||
return '待审核'
|
||
}
|
||
|
||
if (task.status === 'completed') {
|
||
return '已完成'
|
||
}
|
||
|
||
if (task.status === 'rejected') {
|
||
return '已驳回'
|
||
}
|
||
|
||
if (task.status === 'cancelled') {
|
||
return '已取消'
|
||
}
|
||
|
||
// 默认返回空字符串
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* 获取待审核页面的流程阶段(支持子任务组)
|
||
*/
|
||
const getProcessStageForReview = (task: any): string => {
|
||
// 子任务组:使用后端返回的审核人信息
|
||
if (task._itemType === 'taskGroup') {
|
||
if (task.currentApproverName) {
|
||
return `设计负责人:${task.currentApproverName}审核`
|
||
}
|
||
return '设计负责人审核'
|
||
}
|
||
|
||
// 主任务:使用原有逻辑
|
||
return getProcessStage(task)
|
||
}
|
||
|
||
// 判断是否为新发布任务(1小时内)
|
||
const isNewTask = (task: TaskOrSubTask) => {
|
||
if (!task.createTime) return false
|
||
const createTime = dayjs(task.createTime)
|
||
const now = dayjs()
|
||
return now.diff(createTime, 'hour') < 1
|
||
}
|
||
|
||
// 判断是否为高分任务(积分 >= 500)
|
||
const isHighScoreTask = (task: TaskOrSubTask) => {
|
||
return task.score && task.score >= 500
|
||
}
|
||
|
||
// 加载配置数据(从全局store加载)
|
||
const loadConfigs = async () => {
|
||
try {
|
||
await configStore.loadAllConfigs()
|
||
// 加载层级任务类型数据(用于级联选择器和递归查找)
|
||
const response = await configApi.getDemandTypesTree()
|
||
if (response && response.data) {
|
||
hierarchicalTaskTypes.value = response.data
|
||
}
|
||
} 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 userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
const userDeptType = userInfo.deptType
|
||
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
|
||
|
||
let allTasks: any[] = []
|
||
let totalCount = 0
|
||
|
||
// 需求部门用户不应看到任务大厅的任务
|
||
if (userDeptType === 'demand' && !isAdmin) {
|
||
taskList.value = []
|
||
total.value = 0
|
||
return
|
||
}
|
||
|
||
// 只加载子任务(可接单的任务都是子任务)
|
||
const itemResponse = await taskApi.getAvailableTaskItems(params)
|
||
|
||
if (itemResponse.code === 200) {
|
||
const items = (itemResponse.data.records || []).map((item: any) => ({
|
||
...item,
|
||
id: item.id,
|
||
itemId: item.id,
|
||
itemNo: item.itemNo,
|
||
title: item.mainTaskTitle || item.taskTitle || item.title,
|
||
subTaskNo: item.itemNo,
|
||
isSubTask: true,
|
||
taskId: item.taskId,
|
||
groupNo: item.groupNo,
|
||
taskTypeId: item.taskTypeId,
|
||
taskTypeName: item.taskTypeName,
|
||
taskSubType: item.taskSubType,
|
||
score: item.totalScore || item.score,
|
||
estimatedHours: item.estimatedHours,
|
||
quantity: item.quantity || 1,
|
||
previewImage: item.previewImage,
|
||
prevDownloadUrl: item.prevDownloadUrl,
|
||
description: item.description || item.taskDescription
|
||
}))
|
||
allTasks = 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 userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
const userDeptType = userInfo.deptType
|
||
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
|
||
|
||
let allTasks: any[] = []
|
||
let totalCount = 0
|
||
|
||
if (isAdmin) {
|
||
// 管理员:看到所有已完成任务(母任务和子任务)
|
||
const [taskResponse, itemResponse] = await Promise.all([
|
||
taskApi.getCompletedTasks(params),
|
||
taskApi.getCompletedTaskItems(params)
|
||
])
|
||
|
||
if (taskResponse.data && taskResponse.data.records) {
|
||
const tasks = taskResponse.data.records.map((task: any) => ({
|
||
...task,
|
||
isSubTask: false
|
||
}))
|
||
allTasks = [...allTasks, ...tasks]
|
||
totalCount += taskResponse.data.total || 0
|
||
}
|
||
|
||
if (itemResponse.data && itemResponse.data.records) {
|
||
const items = itemResponse.data.records.map((item: any) => ({
|
||
...mapTaskItemToTask(item),
|
||
status: 'completed'
|
||
}))
|
||
allTasks = [...allTasks, ...items]
|
||
totalCount += itemResponse.data.total || 0
|
||
}
|
||
} else if (userDeptType === 'demand') {
|
||
// 需求部门用户:看到自己发起的已完成的母任务
|
||
const response = await taskApi.getCompletedTasks(params)
|
||
if (response.data && response.data.records) {
|
||
allTasks = response.data.records.map((task: any) => ({
|
||
...task,
|
||
isSubTask: false
|
||
}))
|
||
totalCount = response.data.total || 0
|
||
}
|
||
} else {
|
||
// 接单部门用户:看到自己完成的子任务
|
||
const response = await taskApi.getCompletedTaskItems(params)
|
||
if (response.data && response.data.records) {
|
||
allTasks = response.data.records.map((item: any) => ({
|
||
...mapTaskItemToTask(item),
|
||
status: 'completed'
|
||
}))
|
||
totalCount = response.data.total || 0
|
||
}
|
||
}
|
||
|
||
// 按完成时间排序
|
||
allTasks.sort((a, b) => {
|
||
const timeA = new Date(a.completeTime || a.updateTime || 0).getTime()
|
||
const timeB = new Date(b.completeTime || b.updateTime || 0).getTime()
|
||
return timeB - timeA // 降序
|
||
})
|
||
|
||
completedTaskList.value = allTasks
|
||
completedTotal.value = totalCount
|
||
} catch (error) {
|
||
console.error('加载已完成任务失败:', error)
|
||
message.error('加载已完成任务失败')
|
||
} finally {
|
||
completedLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 标签页切换
|
||
const handleTabChange = (key: string) => {
|
||
if (key === 'completed') {
|
||
loadCompletedTasks()
|
||
} else if (key === 'published') {
|
||
loadPublishedTasks()
|
||
} else if (key === 'inProgress') {
|
||
loadInProgressTasks()
|
||
} else if (key === 'review') {
|
||
loadReviewTasks()
|
||
} else {
|
||
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 approveModalVisible = ref(false)
|
||
const approveTaskData = ref<Task | null>(null)
|
||
const approveAssignType = ref<'auto' | 'manual'>('auto')
|
||
const approveTaskTypes = ref<Array<{typeId: number, quantity: number}>>([])
|
||
const approveDesignerId = ref<number | undefined>(undefined)
|
||
const designersList = ref<DesignerWithWorkload[]>([])
|
||
const newTaskTypePath = ref<(string | number)[] | undefined>(undefined)
|
||
const preAssignedDesignerName = ref<string>('') // 预先指定的设计师姓名
|
||
|
||
// 判断任务是否已预先指定设计师
|
||
const isTaskPreAssigned = computed(() => {
|
||
if (!approveTaskData.value) return false
|
||
const task = approveTaskData.value as any
|
||
return task.releaseType === 'assigned' && task.receiverId != null
|
||
})
|
||
|
||
// 级联选择器搜索过滤
|
||
const cascaderFilter = (inputValue: string, path: any[]) => {
|
||
return path.some(option =>
|
||
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
|
||
)
|
||
}
|
||
|
||
// 添加审核弹窗中的任务类型
|
||
const handleAddApproveTaskType = () => {
|
||
if (!newTaskTypePath.value || newTaskTypePath.value.length === 0) {
|
||
message.warning('请选择任务类型')
|
||
return
|
||
}
|
||
// 获取最后一个ID(即最深层级的类型ID)
|
||
const typeId = newTaskTypePath.value[newTaskTypePath.value.length - 1] as number
|
||
if (approveTaskTypes.value.find(t => t.typeId === typeId)) {
|
||
message.warning('该任务类型已添加')
|
||
return
|
||
}
|
||
approveTaskTypes.value.push({ typeId, quantity: 1 })
|
||
newTaskTypePath.value = undefined
|
||
}
|
||
|
||
// 格式化相对时间
|
||
const formatDeadlineRelative = (deadline: string) => {
|
||
if (!deadline) return ''
|
||
try {
|
||
const date = new Date(deadline)
|
||
const now = new Date()
|
||
const diff = date.getTime() - now.getTime()
|
||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||
|
||
if (diff < 0) return '已逾期'
|
||
if (days === 0) return `${hours}小时后`
|
||
return `${days}天${hours}小时后`
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
// 显示指定设计师弹窗(接单部门审核时)
|
||
const showAssignDesignerModal = async (task: Task) => {
|
||
try {
|
||
// 解析任务类型 - 优先从 typeDetails 获取(包含每个类型的实际数量)
|
||
let typeDetailsArray: TaskTypeDetail[] = []
|
||
if ((task as any).typeDetails) {
|
||
try {
|
||
const details = (task as any).typeDetails
|
||
typeDetailsArray = typeof details === 'string' ? JSON.parse(details) : details
|
||
} catch (e) {
|
||
console.warn('解析 typeDetails 失败:', e)
|
||
}
|
||
}
|
||
|
||
// 获取任务类型ID用于查询设计师
|
||
// 优先从 typeDetails 获取第一个任务类型ID,否则使用 taskTypeId
|
||
let targetTaskTypeId: number | undefined = task.taskTypeId
|
||
if (typeDetailsArray.length > 0 && typeDetailsArray[0].typeId) {
|
||
targetTaskTypeId = typeDetailsArray[0].typeId
|
||
}
|
||
|
||
console.log('获取设计师列表,任务类型ID:', targetTaskTypeId, 'typeDetails:', typeDetailsArray)
|
||
|
||
// 加载设计师列表及工作量(根据任务类型的接单部门配置)
|
||
const designers = await userApi.getDesignersWithWorkload(undefined, targetTaskTypeId)
|
||
|
||
if (designers.length === 0) {
|
||
message.warning('未找到对应接单部门的设计师')
|
||
return
|
||
}
|
||
|
||
designersList.value = designers
|
||
approveTaskData.value = task
|
||
newTaskTypePath.value = undefined
|
||
|
||
// 检查任务是否已预先指定设计师
|
||
const taskAny = task as any
|
||
if (taskAny.releaseType === 'assigned' && taskAny.receiverId) {
|
||
// 已预先指定设计师,设置相关状态
|
||
approveAssignType.value = 'manual'
|
||
approveDesignerId.value = taskAny.receiverId
|
||
// 从设计师列表中查找预先指定的设计师姓名
|
||
const preAssigned = designers.find(d => d.id === taskAny.receiverId)
|
||
preAssignedDesignerName.value = preAssigned?.realName || taskAny.receiverName || '未知'
|
||
console.log('任务已预先指定设计师:', preAssignedDesignerName.value)
|
||
} else {
|
||
// 未预先指定,重置状态
|
||
approveAssignType.value = 'auto'
|
||
approveDesignerId.value = undefined
|
||
preAssignedDesignerName.value = ''
|
||
}
|
||
|
||
// 如果有 typeDetails,从中获取任务类型和数量
|
||
if (typeDetailsArray.length > 0) {
|
||
approveTaskTypes.value = typeDetailsArray.map(detail => ({
|
||
typeId: detail.typeId,
|
||
quantity: detail.quantity || 1
|
||
}))
|
||
} else {
|
||
// 兼容旧数据:从 taskTypeIds 获取
|
||
const taskTypeIdsStr = task.taskTypeIds || task.taskTypeId?.toString() || ''
|
||
const taskTypeIdArray = taskTypeIdsStr.split(',').filter(id => id.trim())
|
||
approveTaskTypes.value = taskTypeIdArray.map(id => ({
|
||
typeId: parseInt(id.trim()),
|
||
quantity: task.quantity || 1
|
||
}))
|
||
}
|
||
|
||
approveModalVisible.value = true
|
||
} catch (error: any) {
|
||
console.error('获取设计师列表失败:', error)
|
||
message.error('获取设计师列表失败')
|
||
}
|
||
}
|
||
|
||
// 确认审核通过
|
||
const handleApproveConfirm = async () => {
|
||
if (!approveTaskData.value) return
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
// 检查是否需要指定设计师
|
||
if (approveAssignType.value === 'manual' && !approveDesignerId.value) {
|
||
message.error('请选择要指定的设计师')
|
||
return
|
||
}
|
||
|
||
// 检查任务类型是否为空
|
||
if (approveTaskTypes.value.length === 0) {
|
||
message.error('至少需要一个任务类型')
|
||
return
|
||
}
|
||
|
||
// 判断是子任务组还是主任务
|
||
const taskData = approveTaskData.value as any
|
||
const isTaskGroup = taskData._isTaskGroup === true
|
||
|
||
if (isTaskGroup) {
|
||
// 子任务组审核
|
||
await taskApi.approveTaskGroup(taskData.id, {
|
||
groupNo: taskData._groupNo,
|
||
assignedReceiverId: approveAssignType.value === 'manual' ? approveDesignerId.value : undefined
|
||
})
|
||
} else {
|
||
// 主任务审核
|
||
const approvalData: TaskApprovalDTO = {
|
||
comment: approveAssignType.value === 'manual' ?
|
||
`指定设计师接单` : '审核通过,发布到任务大厅',
|
||
assignedReceiverId: approveAssignType.value === 'manual' ? approveDesignerId.value : undefined,
|
||
taskUpdates: {
|
||
taskTypes: approveTaskTypes.value.map(t => ({
|
||
taskTypeId: t.typeId,
|
||
quantity: t.quantity
|
||
}))
|
||
}
|
||
}
|
||
await taskApi.approveReceivingLevel(taskData.id!, approvalData)
|
||
}
|
||
|
||
if (approveAssignType.value === 'manual') {
|
||
const designer = designersList.value.find(d => d.id === approveDesignerId.value)
|
||
message.success(`已指定设计师 ${designer?.realName} 接单`)
|
||
} else {
|
||
message.success('审核通过,任务已发布到大厅')
|
||
}
|
||
|
||
approveModalVisible.value = false
|
||
// 刷新列表和数量
|
||
loadReviewTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('审核失败:', error)
|
||
message.error(error.msg || error.message || '审核失败')
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 两级审核:驳回任务
|
||
const handleRejectTwoLevelTask = async (task: Task) => {
|
||
let rejectReason = ''
|
||
|
||
Modal.confirm({
|
||
title: '❌ 驳回任务',
|
||
content: h('div', { style: 'padding: 16px 0;' }, [
|
||
h('div', {
|
||
style: {
|
||
marginBottom: '12px',
|
||
fontSize: '14px',
|
||
fontWeight: 500,
|
||
color: '#262626'
|
||
}
|
||
}, '请输入驳回原因'),
|
||
h(Textarea, {
|
||
rows: 5,
|
||
placeholder: '请详细说明驳回原因,以便发起人修改...',
|
||
maxlength: 500,
|
||
showCount: true,
|
||
style: { fontSize: '14px' },
|
||
onChange: (e: any) => {
|
||
rejectReason = e.target.value
|
||
}
|
||
})
|
||
]),
|
||
width: 550,
|
||
okText: '确认驳回',
|
||
cancelText: '取消',
|
||
okType: 'danger',
|
||
okButtonProps: {
|
||
size: 'large',
|
||
style: { minWidth: '100px' }
|
||
},
|
||
cancelButtonProps: {
|
||
size: 'large',
|
||
style: { minWidth: '100px' }
|
||
},
|
||
onOk: async () => {
|
||
if (!rejectReason || !rejectReason.trim()) {
|
||
message.warning('请输入驳回原因')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
// 根据当前审核角色调用不同的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
|
||
|
||
// 从 item.data 获取 taskId 和 groupNo
|
||
const taskId = item.data?.taskId || item.id
|
||
const groupNo = item.data?.groupNo || item.data?.groupId || 1
|
||
|
||
await taskApi.approveTaskGroup(taskId, {
|
||
groupNo,
|
||
assignedReceiverId: assignType.value === 'manual' ? selectedDesignerId.value : undefined
|
||
})
|
||
|
||
if (assignType.value === 'manual') {
|
||
const designer = designersWithWorkload.find(d => d.id === selectedDesignerId.value)
|
||
message.success(`已审核通过并指定给设计师 ${designer?.realName}`)
|
||
} else {
|
||
message.success('已审核通过,子任务组已发布到任务大厅')
|
||
}
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('审核失败:', error)
|
||
message.error(error.msg || error.message || '审核失败')
|
||
return Promise.reject(error)
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
} catch (error: any) {
|
||
console.error('获取设计师列表失败:', error)
|
||
message.error('获取设计师列表失败')
|
||
}
|
||
}
|
||
|
||
// 驳回子任务组
|
||
const handleRejectTaskGroup = async (item: ApprovalItem) => {
|
||
let rejectReason = ''
|
||
|
||
Modal.confirm({
|
||
title: '驳回子任务组',
|
||
content: h('div', { style: 'padding: 10px 0;' }, [
|
||
h('p', { style: 'margin-bottom: 12px; color: #666;' },
|
||
`即将驳回子任务组"${item.title}",共${item.data.items?.length || 0}个子任务。`
|
||
),
|
||
h('p', { style: 'margin-bottom: 10px; color: #666;' }, '请输入驳回原因:'),
|
||
h(Textarea, {
|
||
rows: 4,
|
||
placeholder: '请详细说明驳回原因',
|
||
maxlength: 500,
|
||
showCount: true,
|
||
onChange: (e: any) => {
|
||
rejectReason = e.target.value
|
||
}
|
||
})
|
||
]),
|
||
okText: '确认驳回',
|
||
cancelText: '取消',
|
||
okType: 'danger',
|
||
onOk: async () => {
|
||
if (!rejectReason || !rejectReason.trim()) {
|
||
message.warning('请输入驳回原因')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
await taskApi.rejectTaskGroup(item.data.taskId, item.data.groupNo, rejectReason.trim())
|
||
message.success('已驳回子任务组')
|
||
|
||
// 刷新列表和数量
|
||
loadPendingApprovalTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('驳回失败:', error)
|
||
message.error(error.msg || error.message || '驳回失败')
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 从待审核任务页面审核子任务组
|
||
const handleApproveTaskGroupFromReview = async (task: any) => {
|
||
const groupData = task._groupData
|
||
if (!groupData) {
|
||
message.error('子任务组数据不完整')
|
||
return
|
||
}
|
||
|
||
// 将子任务组数据转换为与主任务相同的格式,复用审核弹窗
|
||
const taskForApproval = {
|
||
id: groupData.taskId,
|
||
title: task.title,
|
||
taskNo: task.taskNo,
|
||
typeDetails: task.typeDetails,
|
||
taskTypeId: task.taskTypeId,
|
||
currentApproverRole: 'receiving_leader',
|
||
// 标记这是子任务组审核
|
||
_isTaskGroup: true,
|
||
_groupNo: groupData.groupNo,
|
||
_groupData: groupData
|
||
} as any
|
||
|
||
// 调用与第一轮相同的审核弹窗
|
||
showAssignDesignerModal(taskForApproval)
|
||
}
|
||
|
||
// 从待审核任务页面驳回子任务组
|
||
const handleRejectTaskGroupFromReview = async (task: any) => {
|
||
const groupData = task._groupData
|
||
if (!groupData) {
|
||
message.error('子任务组数据不完整')
|
||
return
|
||
}
|
||
|
||
let rejectReason = ''
|
||
|
||
Modal.confirm({
|
||
title: '驳回子任务组',
|
||
content: h('div', { style: 'padding: 10px 0;' }, [
|
||
h('p', { style: 'margin-bottom: 12px; color: #666;' },
|
||
`即将驳回子任务组"${task.title} - 第${groupData.groupNo}组",共${groupData.itemCount || 0}个子任务。`
|
||
),
|
||
h('p', { style: 'margin-bottom: 10px; color: #666;' }, '请输入驳回原因:'),
|
||
h(Textarea, {
|
||
rows: 4,
|
||
placeholder: '请详细说明驳回原因',
|
||
maxlength: 500,
|
||
showCount: true,
|
||
onChange: (e: any) => {
|
||
rejectReason = e.target.value
|
||
}
|
||
})
|
||
]),
|
||
okText: '确认驳回',
|
||
cancelText: '取消',
|
||
okType: 'danger',
|
||
onOk: async () => {
|
||
if (!rejectReason || !rejectReason.trim()) {
|
||
message.warning('请输入驳回原因')
|
||
return Promise.reject()
|
||
}
|
||
|
||
try {
|
||
approvalLoading.value = true
|
||
|
||
await taskApi.rejectTaskGroup(groupData.taskId, groupData.groupNo, rejectReason.trim())
|
||
message.success('已驳回子任务组')
|
||
|
||
// 刷新列表和数量
|
||
loadReviewTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
console.error('驳回失败:', error)
|
||
message.error(error.msg || error.message || '驳回失败')
|
||
} finally {
|
||
approvalLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 已完成任务分页变化
|
||
const handleCompletedPageChange = (page: number) => {
|
||
completedPagination.page = page
|
||
loadCompletedTasks()
|
||
}
|
||
|
||
// 我发起的任务分页
|
||
const publishedPagination = reactive({
|
||
page: 1,
|
||
pageSize: 10
|
||
})
|
||
|
||
// 进行中任务分页
|
||
const 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 userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
const userDeptType = userInfo.deptType // 'demand' 或 'receiving'
|
||
const isAdmin = userInfo.isAdmin === 1 || userInfo.isAdmin === true
|
||
|
||
let allTasks: any[] = []
|
||
let totalCount = 0
|
||
|
||
// 根据部门类型决定加载哪些任务
|
||
if (isAdmin) {
|
||
// 管理员:看到所有任务(母任务和子任务)
|
||
const [taskResponse, itemResponse] = await Promise.all([
|
||
taskApi.getTasks(params),
|
||
taskApi.getInProgressTaskItems(params)
|
||
])
|
||
|
||
// 处理普通任务
|
||
if (taskResponse.data && taskResponse.data.records) {
|
||
const tasks = taskResponse.data.records.map((task: any) => ({
|
||
...task,
|
||
isSubTask: false
|
||
}))
|
||
allTasks = [...allTasks, ...tasks]
|
||
totalCount += taskResponse.data.total || 0
|
||
}
|
||
|
||
// 处理子任务
|
||
if (itemResponse.data && itemResponse.data.records) {
|
||
const items = itemResponse.data.records.map((item: any) => mapTaskItemToTask(item))
|
||
allTasks = [...allTasks, ...items]
|
||
totalCount += itemResponse.data.total || 0
|
||
}
|
||
} else if (userDeptType === 'demand') {
|
||
// 需求部门用户:看自己发起的任务中有子任务在进行中的
|
||
// 调用 getMyTasks 获取自己发起的所有任务(包含子任务进度信息)
|
||
const taskResponse = await taskApi.getMyTasks({
|
||
role: 'publisher',
|
||
page: inProgressPagination.page,
|
||
pageSize: inProgressPagination.pageSize,
|
||
keyword: inProgressFilters.keyword
|
||
})
|
||
if (taskResponse.code === 200 && taskResponse.data && taskResponse.data.records) {
|
||
// 过滤出有子任务在进行中的任务
|
||
const tasksWithInProgress = taskResponse.data.records.filter((task: any) => {
|
||
// 流水线任务:检查是否有子任务在进行中
|
||
if (task.isPipeline === 1) {
|
||
return (task.subTaskInProgress || 0) > 0 || task.status === 'in_progress'
|
||
}
|
||
// 普通任务:状态为进行中
|
||
return task.status === 'in_progress'
|
||
})
|
||
allTasks = tasksWithInProgress.map((task: any) => ({
|
||
...task,
|
||
isSubTask: false
|
||
}))
|
||
// 由于是前端过滤,total 用过滤后的数量
|
||
totalCount = allTasks.length
|
||
}
|
||
} else {
|
||
// 接单部门用户(设计师):看子任务卡片 + 直接分配给自己的普通任务
|
||
const [itemResponse, taskResponse] = await Promise.all([
|
||
taskApi.getInProgressTaskItems(params),
|
||
// 获取直接分配给当前用户的普通任务(非流水线任务)
|
||
taskApi.getTasks({
|
||
...params,
|
||
role: 'receiver' // 以接单人身份查询
|
||
})
|
||
])
|
||
|
||
// 处理子任务
|
||
if (itemResponse.data && itemResponse.data.records) {
|
||
const items = itemResponse.data.records.map((item: any) => mapTaskItemToTask(item))
|
||
allTasks = [...allTasks, ...items]
|
||
totalCount += itemResponse.data.total || 0
|
||
}
|
||
|
||
// 处理直接分配的普通任务(排除流水线任务,因为流水线任务由子任务显示)
|
||
if (taskResponse.data && taskResponse.data.records) {
|
||
const normalTasks = taskResponse.data.records
|
||
.filter((task: any) => task.isPipeline !== 1) // 排除流水线任务
|
||
.map((task: any) => ({
|
||
...task,
|
||
isSubTask: false
|
||
}))
|
||
allTasks = [...allTasks, ...normalTasks]
|
||
totalCount += normalTasks.length
|
||
}
|
||
}
|
||
|
||
// 按更新时间排序
|
||
allTasks.sort((a, b) => {
|
||
const timeA = new Date(a.updateTime || a.startTime || 0).getTime()
|
||
const timeB = new Date(b.updateTime || b.startTime || 0).getTime()
|
||
return timeB - timeA // 降序
|
||
})
|
||
|
||
inProgressTaskList.value = allTasks
|
||
inProgressTotal.value = totalCount
|
||
|
||
} catch (error) {
|
||
console.error('加载进行中任务失败:', error)
|
||
message.error('加载进行中任务失败')
|
||
} finally {
|
||
inProgressLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 将子任务映射为任务格式(供显示用)
|
||
const mapTaskItemToTask = (item: any) => ({
|
||
...item,
|
||
id: item.id,
|
||
itemId: item.id,
|
||
itemNo: item.itemNo,
|
||
title: item.mainTaskTitle || item.taskTitle || item.title,
|
||
subTaskNo: item.itemNo,
|
||
isSubTask: true,
|
||
taskId: item.taskId,
|
||
groupNo: item.groupNo,
|
||
taskTypeId: item.taskTypeId,
|
||
taskTypeName: item.taskTypeName,
|
||
score: item.totalScore || item.score,
|
||
estimatedHours: item.estimatedHours,
|
||
quantity: item.quantity || 1,
|
||
previewImage: item.previewImage,
|
||
prevDownloadUrl: item.prevDownloadUrl,
|
||
description: item.description || item.taskDescription,
|
||
progress: item.progress,
|
||
status: 'in_progress',
|
||
receiverName: item.receiverName // 接单人姓名(部门负责人查看时显示)
|
||
})
|
||
|
||
// 加载待审核任务(所有处于审批流程中的任务 + 部门负责人的子任务组)
|
||
const loadReviewTasks = async () => {
|
||
reviewLoading.value = true
|
||
try {
|
||
const params: any = {
|
||
page: reviewPagination.page,
|
||
pageSize: reviewPagination.pageSize
|
||
}
|
||
|
||
// 添加筛选条件
|
||
if (reviewFilters.taskTypeId) params.taskTypeId = reviewFilters.taskTypeId
|
||
|
||
// 并行加载:主任务 + 子任务组(如果是部门负责人)
|
||
const promises: Promise<any>[] = [
|
||
taskApi.getAllPendingApprovalTasks(params)
|
||
]
|
||
|
||
// 如果是部门负责人,还要加载待审核的子任务组
|
||
if (isDepartmentLeader.value) {
|
||
promises.push(taskApi.getPendingApprovalTaskGroups())
|
||
}
|
||
|
||
const [tasksResponse, groupsResponse] = await Promise.all(promises)
|
||
|
||
let allItems: any[] = []
|
||
|
||
// 处理主任务
|
||
const tasks = tasksResponse.data?.records || []
|
||
for (const task of tasks) {
|
||
allItems.push({
|
||
...task,
|
||
_itemType: 'task' // 标记类型
|
||
})
|
||
}
|
||
|
||
// 处理子任务组(如果有)- 使用与主任务相同的数据结构
|
||
if (groupsResponse && groupsResponse.code === 200 && groupsResponse.data) {
|
||
for (const group of groupsResponse.data) {
|
||
allItems.push({
|
||
id: `group-${group.taskId}-${group.groupNo}`,
|
||
title: group.taskTitle || '子任务组',
|
||
taskNo: group.taskNo || '',
|
||
score: group.totalScore || 0,
|
||
totalScore: group.totalScore || 0,
|
||
createTime: group.createTime,
|
||
// 继承主任务的完整信息
|
||
brandId: group.brandId,
|
||
brandName: group.brandName,
|
||
categoryId: group.categoryId,
|
||
categoryName: group.categoryName,
|
||
platformId: group.platformId,
|
||
platformName: group.platformName,
|
||
spuInfo: group.spuInfo,
|
||
quantity: group.itemCount || 1, // 子任务数量作为数量显示
|
||
previewImage: group.previewImage,
|
||
attachmentUrl: group.attachmentUrl,
|
||
remark: group.remark,
|
||
typeDetails: group.typeDetails,
|
||
taskTypeId: group.taskTypeId,
|
||
taskTypeName: group.taskTypeName,
|
||
publisherId: group.publisherId,
|
||
publisherName: group.publisherName,
|
||
publisherDeptName: group.publisherDeptName,
|
||
// 审核人信息(流程阶段显示)
|
||
currentApproverName: group.currentApproverName,
|
||
currentApproverRole: group.currentApproverRole,
|
||
approvalStatus: group.approvalStatus,
|
||
// 前置任务的素材下载链接
|
||
prevDownloadUrls: group.prevDownloadUrls || [],
|
||
// 标记类型
|
||
_itemType: 'taskGroup',
|
||
_groupData: group // 保存原始子任务组数据
|
||
})
|
||
}
|
||
}
|
||
|
||
// 前端筛选(因为新接口只支持taskTypeId筛选)
|
||
if (reviewFilters.keyword) {
|
||
const keyword = reviewFilters.keyword.trim().toLowerCase()
|
||
allItems = allItems.filter((item: any) =>
|
||
item.title?.toLowerCase().includes(keyword) ||
|
||
item.taskNo?.toLowerCase().includes(keyword) ||
|
||
item.spuInfo?.toLowerCase().includes(keyword) ||
|
||
item.publisherName?.toLowerCase().includes(keyword)
|
||
)
|
||
}
|
||
|
||
if (reviewFilters.brandId) {
|
||
allItems = allItems.filter((item: any) => item._itemType === 'taskGroup' || item.brandId === reviewFilters.brandId)
|
||
}
|
||
|
||
if (reviewFilters.categoryId) {
|
||
allItems = allItems.filter((item: any) => item._itemType === 'taskGroup' || item.categoryId === reviewFilters.categoryId)
|
||
}
|
||
|
||
if (reviewFilters.platformId) {
|
||
allItems = allItems.filter((item: any) => item._itemType === 'taskGroup' || item.platformId === reviewFilters.platformId)
|
||
}
|
||
|
||
if (reviewFilters.minScore !== undefined) {
|
||
allItems = allItems.filter((item: any) => (item.score || 0) >= reviewFilters.minScore!)
|
||
}
|
||
|
||
if (reviewFilters.maxScore !== undefined) {
|
||
allItems = allItems.filter((item: any) => (item.score || 0) <= reviewFilters.maxScore!)
|
||
}
|
||
|
||
if (reviewFilters.timeRange) {
|
||
const now = dayjs()
|
||
let startTime: dayjs.Dayjs
|
||
if (reviewFilters.timeRange === 'today') {
|
||
startTime = now.startOf('day')
|
||
} else if (reviewFilters.timeRange === 'week') {
|
||
startTime = now.subtract(7, 'day')
|
||
} else if (reviewFilters.timeRange === 'month') {
|
||
startTime = now.subtract(30, 'day')
|
||
} else {
|
||
startTime = now.subtract(1, 'year')
|
||
}
|
||
allItems = allItems.filter((item: any) =>
|
||
item.createTime && dayjs(item.createTime).isAfter(startTime)
|
||
)
|
||
}
|
||
|
||
// 排序
|
||
if (reviewFilters.sortBy === 'score') {
|
||
allItems.sort((a: any, b: any) => (b.score || 0) - (a.score || 0))
|
||
} else {
|
||
allItems.sort((a: any, b: any) =>
|
||
dayjs(b.createTime || 0).valueOf() - dayjs(a.createTime || 0).valueOf()
|
||
)
|
||
}
|
||
|
||
reviewTaskList.value = allItems
|
||
reviewTotal.value = allItems.length
|
||
} catch (error) {
|
||
console.error('加载待审核任务失败:', error)
|
||
message.error('加载待审核任务失败')
|
||
} finally {
|
||
reviewLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 进行中任务分页变化
|
||
const handleInProgressPageChange = (page: number) => {
|
||
inProgressPagination.page = page
|
||
loadInProgressTasks()
|
||
}
|
||
|
||
// 待审核任务分页变化
|
||
const handleReviewPageChange = (page: number) => {
|
||
reviewPagination.page = page
|
||
loadReviewTasks()
|
||
}
|
||
|
||
// 重置进行中任务筛选条件
|
||
const handleInProgressFiltersReset = () => {
|
||
inProgressFilters.keyword = undefined
|
||
inProgressFilters.sortBy = 'latest'
|
||
inProgressFilters.taskTypeId = undefined
|
||
inProgressFilters.brandId = undefined
|
||
inProgressFilters.categoryId = undefined
|
||
inProgressFilters.platformId = undefined
|
||
inProgressFilters.timeRange = undefined
|
||
inProgressFilters.minProgress = undefined
|
||
inProgressFilters.maxProgress = undefined
|
||
inProgressPagination.page = 1
|
||
loadInProgressTasks()
|
||
}
|
||
|
||
// 重置待审核任务筛选条件
|
||
const handleReviewFiltersReset = () => {
|
||
reviewFilters.keyword = undefined
|
||
reviewFilters.sortBy = 'latest'
|
||
reviewFilters.taskTypeId = undefined
|
||
reviewFilters.brandId = undefined
|
||
reviewFilters.categoryId = undefined
|
||
reviewFilters.platformId = undefined
|
||
reviewFilters.timeRange = undefined
|
||
reviewFilters.minScore = undefined
|
||
reviewFilters.maxScore = undefined
|
||
reviewPagination.page = 1
|
||
loadReviewTasks()
|
||
}
|
||
|
||
// 判断是否可以更新进度(接单人)
|
||
const canUpdateProgress = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
return task.receiverId === userInfo.id
|
||
}
|
||
|
||
// 判断是否可以完结任务(接单人)
|
||
const canCompleteTask = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
return task.receiverId === userInfo.id && (task.progress || 0) >= 100
|
||
}
|
||
|
||
// 判断是否可以审核任务
|
||
// 对于审批流程中的任务:只有当前审核人可以审核
|
||
// 对于其他情况:只有部门负责人可以审核
|
||
const 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 handleDownloadAssets = (task: Task) => {
|
||
const downloadUrl = task.downloadUrl || task.deliveryPath
|
||
if (downloadUrl) {
|
||
// 如果是http链接,直接打开
|
||
if (downloadUrl.startsWith('http')) {
|
||
window.open(downloadUrl, '_blank')
|
||
} else {
|
||
// 复制到剪贴板
|
||
navigator.clipboard.writeText(downloadUrl).then(() => {
|
||
message.success('素材路径已复制到剪贴板')
|
||
}).catch(() => {
|
||
message.info(`素材路径: ${downloadUrl}`)
|
||
})
|
||
}
|
||
} else {
|
||
message.warning('暂无可下载的素材')
|
||
}
|
||
}
|
||
|
||
// 下载上一环节素材(流水线任务)
|
||
const handleDownloadPrevAssets = (task: TaskOrSubTask) => {
|
||
const prevDownloadUrl = task.prevDownloadUrl
|
||
if (prevDownloadUrl) {
|
||
// 如果包含多个链接(逗号分隔),显示弹窗让用户选择
|
||
const urls = prevDownloadUrl.split(',').filter(url => url.trim())
|
||
if (urls.length === 1) {
|
||
// 只有一个链接,直接打开
|
||
if (urls[0].startsWith('http')) {
|
||
window.open(urls[0], '_blank')
|
||
} else {
|
||
navigator.clipboard.writeText(urls[0]).then(() => {
|
||
message.success('素材路径已复制到剪贴板')
|
||
}).catch(() => {
|
||
message.info(`素材路径: ${urls[0]}`)
|
||
})
|
||
}
|
||
} else {
|
||
// 多个链接,显示列表让用户选择
|
||
Modal.info({
|
||
title: '上一环节素材下载',
|
||
content: h('div', [
|
||
h('p', { style: 'margin-bottom: 12px;' }, `共 ${urls.length} 个素材文件:`),
|
||
...urls.map((url, index) =>
|
||
h('div', { style: 'margin-bottom: 8px;' }, [
|
||
h('a', {
|
||
href: url,
|
||
target: '_blank',
|
||
style: 'color: #1890ff; word-break: break-all;'
|
||
}, `素材 ${index + 1}: ${url.length > 50 ? url.substring(0, 50) + '...' : url}`)
|
||
])
|
||
)
|
||
]),
|
||
width: 600
|
||
})
|
||
}
|
||
} else {
|
||
message.warning('暂无上一环节的素材')
|
||
}
|
||
}
|
||
|
||
const handleEditRejectedTask = (task: Task) => {
|
||
// 跳转到创建任务页面,并传入任务ID作为编辑标识
|
||
router.push({
|
||
path: '/tasks/create',
|
||
query: {
|
||
editId: task.id,
|
||
mode: 'edit'
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleCompleteTask = (task: TaskOrSubTask) => {
|
||
const isSubTask = task.isSubTask
|
||
const taskType = isSubTask ? '子任务' : '任务'
|
||
const taskNo = isSubTask ? (task.itemNo || task.taskNo) : task.taskNo
|
||
|
||
Modal.confirm({
|
||
title: `完结${taskType}`,
|
||
content: h('div', [
|
||
h('div', { style: 'margin-bottom: 12px;' }, `确认完结${taskType}:${task.title}`),
|
||
h('div', { style: 'color: #666; font-size: 13px;' }, [
|
||
h('p', { style: 'margin-bottom: 8px;' }, '系统将自动从素材平台获取您提交的素材。'),
|
||
h('p', { style: 'margin: 0;' }, `任务编号:${taskNo || '--'}`)
|
||
])
|
||
]),
|
||
okText: '确认完结',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
if (isSubTask) {
|
||
// 完结子任务(后端会自动调用第三方API验证)
|
||
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath: '' })
|
||
message.success('子任务已完成')
|
||
} else {
|
||
// 完结普通任务
|
||
await taskApi.completeTask(task.id!, { deliveryPath: '' })
|
||
message.success('任务已提交审核')
|
||
}
|
||
loadInProgressTasks()
|
||
loadAllTabCounts()
|
||
loadDashboardStats()
|
||
} catch (error: any) {
|
||
// 如果是素材未提交的错误,显示特定提示
|
||
const errorMsg = error.message || error.msg || '任务完结失败'
|
||
if (errorMsg.includes('素材未提交') || errorMsg.includes('3D.YYDD.AI')) {
|
||
Modal.warning({
|
||
title: '素材未提交',
|
||
content: h('div', [
|
||
h('p', '请先前往素材平台提交任务素材后再完结任务。'),
|
||
h('p', { style: 'margin-top: 8px;' }, [
|
||
'任务编号:',
|
||
h('strong', taskNo || '--')
|
||
]),
|
||
h('a', {
|
||
href: 'https://3d.yydd.ai',
|
||
target: '_blank',
|
||
style: 'color: #1890ff; margin-top: 12px; display: inline-block;'
|
||
}, '前往素材平台 →')
|
||
])
|
||
})
|
||
} else {
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 审核通过(使用两级审核流程)
|
||
const handleApproveTask = (task: Task) => {
|
||
// 如果是接单部门负责人审核,需要提供选择指派设计师的功能
|
||
if (task.currentApproverRole === 'receiving_leader') {
|
||
handleApproveTwoLevelTask(task)
|
||
return
|
||
}
|
||
|
||
// 需求部门负责人审核,直接通过即可
|
||
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 = async (task: Task) => {
|
||
selectedTask.value = task
|
||
detailDrawerVisible.value = true
|
||
// 已完成任务加载商品ID列表
|
||
if (task.status === 'completed' && task.id) {
|
||
await loadTaskProducts(task.id)
|
||
} else {
|
||
taskProducts.value = []
|
||
}
|
||
}
|
||
|
||
// 任务点击
|
||
const handleTaskClick = async (task: TaskOrSubTask) => {
|
||
selectedTask.value = task
|
||
detailDrawerVisible.value = true
|
||
// 已完成任务加载商品ID列表
|
||
if (task.status === 'completed' && task.id) {
|
||
await loadTaskProducts(task.id)
|
||
} else {
|
||
taskProducts.value = []
|
||
}
|
||
}
|
||
|
||
// 判断用户是否可以抢单
|
||
const canGrabTask = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
const userDeptId = userInfo.deptId
|
||
const userDeptType = userInfo.deptType // 'demand' 或 'receiving'
|
||
|
||
// 管理员拥有所有权限,可以直接抢单
|
||
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) {
|
||
return { canGrab: true, reason: '' }
|
||
}
|
||
|
||
// 检查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:验证部门是否允许接该需求类型(如果任务有需求类型限制)
|
||
const taskTypeId = task.isSubTask ? task.taskTypeId : (task as any).taskTypeId
|
||
if (taskTypeId) {
|
||
// 从配置store中获取任务类型详情
|
||
const demandType = configStore.demandTypes.find(t => t.id === taskTypeId)
|
||
if (demandType && demandType.allowedDeptIds) {
|
||
const allowedIds = demandType.allowedDeptIds.split(',')
|
||
// 检查当前用户部门ID是否在允许列表中
|
||
if (userInfo.deptId && !allowedIds.includes(String(userInfo.deptId))) {
|
||
return { canGrab: false, reason: '您的部门无权接收此类型的任务' }
|
||
}
|
||
}
|
||
}
|
||
|
||
return { canGrab: true, reason: '' }
|
||
}
|
||
|
||
// 抢单(支持普通任务和子任务)
|
||
const handleGrab = async (task: TaskOrSubTask) => {
|
||
const isSubTask = task.isSubTask
|
||
const taskType = isSubTask ? '子任务' : '任务'
|
||
const taskNo = isSubTask ? task.subTaskNo : task.taskNo
|
||
|
||
// 先检查权限
|
||
const { canGrab, reason } = canGrabTask(task)
|
||
if (!canGrab) {
|
||
message.warning(reason)
|
||
return
|
||
}
|
||
|
||
Modal.confirm({
|
||
title: `确认抢单${taskType}`,
|
||
content: `确定要抢单${taskType}"${task.title}"吗?${taskNo ? `(编号:${taskNo})` : ''}`,
|
||
okText: '确认',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
let response
|
||
if (isSubTask) {
|
||
// 抢子任务
|
||
response = await taskApi.grabTaskItem(task.itemId || task.id!)
|
||
} else {
|
||
// 抢普通任务
|
||
response = await taskApi.grabTask(task.id!)
|
||
}
|
||
|
||
if (response.code === 200) {
|
||
message.success(`${taskType}抢单成功!`)
|
||
loadTasks() // 刷新列表
|
||
loadAllTabCounts() // 刷新所有Tab数量
|
||
}
|
||
} catch (error: any) {
|
||
console.error('抢单失败:', error)
|
||
message.error(error.message || '抢单失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 从详情抽屉抢单
|
||
const handleGrabFromDetail = async () => {
|
||
if (!selectedTask.value) return
|
||
await handleGrab(selectedTask.value)
|
||
detailDrawerVisible.value = false
|
||
}
|
||
|
||
// ==================== 商品ID管理 ====================
|
||
|
||
// 加载任务商品ID列表
|
||
const loadTaskProducts = async (taskId: number) => {
|
||
try {
|
||
const response = await taskApi.getTaskProducts(taskId)
|
||
taskProducts.value = response.data || []
|
||
} catch (error) {
|
||
console.error('加载商品ID列表失败:', error)
|
||
taskProducts.value = []
|
||
}
|
||
}
|
||
|
||
// 判断是否可以管理商品ID(任务发起者或管理员)
|
||
const canManageProducts = (task: TaskOrSubTask) => {
|
||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||
// 管理员可以管理
|
||
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) return true
|
||
// 任务发起者可以管理
|
||
if (task.publisherId && task.publisherId === userInfo.id) return true
|
||
return false
|
||
}
|
||
|
||
// 添加商品ID
|
||
const handleAddProduct = async () => {
|
||
if (!selectedTask.value || !newProductId.value.trim()) {
|
||
message.warning('请输入商品ID')
|
||
return
|
||
}
|
||
|
||
addingProduct.value = true
|
||
try {
|
||
await taskApi.addTaskProduct(selectedTask.value.id!, {
|
||
productId: newProductId.value.trim()
|
||
})
|
||
message.success('添加成功')
|
||
newProductId.value = ''
|
||
await loadTaskProducts(selectedTask.value.id!)
|
||
} catch (error: any) {
|
||
message.error(error.message || '添加失败')
|
||
} finally {
|
||
addingProduct.value = false
|
||
}
|
||
}
|
||
|
||
// 批量添加商品ID
|
||
const handleBatchAddProducts = async () => {
|
||
if (!selectedTask.value || !batchProductIds.value.trim()) {
|
||
message.warning('请输入商品ID')
|
||
return
|
||
}
|
||
|
||
// 解析商品ID(支持换行和逗号分隔)
|
||
const ids = batchProductIds.value
|
||
.split(/[\n,]/)
|
||
.map(id => id.trim())
|
||
.filter(id => id.length > 0)
|
||
|
||
if (ids.length === 0) {
|
||
message.warning('请输入有效的商品ID')
|
||
return
|
||
}
|
||
|
||
addingProduct.value = true
|
||
try {
|
||
const count = await taskApi.addTaskProductsBatch(selectedTask.value.id!, ids)
|
||
message.success(`成功添加 ${count.data} 个商品ID`)
|
||
batchProductIds.value = ''
|
||
await loadTaskProducts(selectedTask.value.id!)
|
||
} catch (error: any) {
|
||
message.error(error.message || '批量添加失败')
|
||
} finally {
|
||
addingProduct.value = false
|
||
}
|
||
}
|
||
|
||
// 删除商品ID
|
||
const handleDeleteProduct = async (productId: number) => {
|
||
if (!selectedTask.value) return
|
||
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: '确定要删除这个商品ID吗?',
|
||
okText: '确定',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
await taskApi.deleteTaskProduct(selectedTask.value!.id!, productId)
|
||
message.success('删除成功')
|
||
await loadTaskProducts(selectedTask.value!.id!)
|
||
} catch (error: any) {
|
||
message.error(error.message || '删除失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 确认商品ID填写完成
|
||
const handleConfirmProductIdFilled = async () => {
|
||
if (!selectedTask.value || !selectedTask.value.id) return
|
||
|
||
confirmingProductId.value = true
|
||
try {
|
||
await taskApi.confirmTaskProductFilled(selectedTask.value.id)
|
||
message.success('任务已确认完结')
|
||
// 更新当前任务状态
|
||
selectedTask.value.productIdFilled = 1
|
||
// 刷新任务列表
|
||
await loadCompletedTasks()
|
||
await loadMyTasks()
|
||
} catch (error: any) {
|
||
message.error(error.message || '确认失败')
|
||
} finally {
|
||
confirmingProductId.value = false
|
||
}
|
||
}
|
||
|
||
// 判断是否是URL
|
||
const isUrl = (str: string) => {
|
||
// 从字符串中提取可能的URL部分
|
||
const urlMatch = str.match(/https?:\/\/[^\s]+/)
|
||
return urlMatch !== null
|
||
}
|
||
|
||
// 从字符串中提取URL
|
||
const extractUrl = (str: string) => {
|
||
const urlMatch = str.match(/https?:\/\/[^\s]+/)
|
||
return urlMatch ? urlMatch[0] : str
|
||
}
|
||
|
||
// 监听配置更新事件
|
||
const handleConfigUpdate = async (event: CustomEvent) => {
|
||
console.log('配置已更新,刷新配置数据:', event.detail)
|
||
await loadConfigs()
|
||
}
|
||
|
||
// 组件挂载
|
||
// 获取任务状态文本(增强版,支持区分驳回、审核中和草稿)
|
||
const getTaskStatusText = (status: string, task?: any) => {
|
||
// 如果有task对象,检查approvalStatus来区分不同状态
|
||
if (task) {
|
||
// 1. 检查是否被驳回(无论status是draft还是rejected)
|
||
if (task.approvalStatus === 'rejected_by_demand' || task.approvalStatus === 'rejected_by_receiving') {
|
||
return '已驳回'
|
||
}
|
||
// 检查status是否是rejected
|
||
if (status === 'rejected') {
|
||
return '已驳回'
|
||
}
|
||
|
||
// 2. 已完成任务(必须优先判断,避免被其他条件覆盖)
|
||
if (status === 'completed') {
|
||
// productIdFilled为0或undefined表示未填写
|
||
if (!task.productIdFilled || task.productIdFilled === 0) {
|
||
return '待填写ID'
|
||
}
|
||
return '已完结'
|
||
}
|
||
|
||
// 3. 检查是否正在审核中
|
||
if (task.approvalStatus === 'pending_demand_approval') {
|
||
return '待部门审核'
|
||
}
|
||
if (task.approvalStatus === 'pending_receiving_approval') {
|
||
return '待接单审核'
|
||
}
|
||
|
||
// 4. 流水线任务且已审核通过,根据是否有接收人显示不同状态
|
||
if (task.isPipeline === 1 && task.approvalStatus === 'approved') {
|
||
if (task.receiverId) {
|
||
return '进行中'
|
||
} else {
|
||
return '待接单'
|
||
}
|
||
}
|
||
|
||
// 5. 普通任务已审核通过但status还是draft,修正显示
|
||
if (task.approvalStatus === 'approved' && status === 'draft') {
|
||
if (task.receiverId) {
|
||
return '进行中'
|
||
} else {
|
||
return '待接单'
|
||
}
|
||
}
|
||
}
|
||
|
||
const statusMap: Record<string, string> = {
|
||
'pending': '待接单',
|
||
'in_progress': '进行中',
|
||
'review': '待审核',
|
||
'completed': '已完成',
|
||
'rejected': '已驳回',
|
||
'cancelled': '已取消',
|
||
'draft': '草稿'
|
||
}
|
||
return statusMap[status] || status
|
||
}
|
||
|
||
// 获取任务状态的CSS类名
|
||
const getTaskStatusClass = (task: any) => {
|
||
if (!task) return 'status-pending'
|
||
|
||
// 驳回状态
|
||
if (task.approvalStatus === 'rejected_by_demand' ||
|
||
task.approvalStatus === 'rejected_by_receiving' ||
|
||
task.status === 'rejected') {
|
||
return 'status-rejected'
|
||
}
|
||
|
||
// 已完成但未填写商品ID - 使用醒目的警告色
|
||
if (task.status === 'completed' && (!task.productIdFilled || task.productIdFilled === 0)) {
|
||
return 'status-pending-id'
|
||
}
|
||
|
||
// 已完结
|
||
if (task.status === 'completed') {
|
||
return 'status-completed'
|
||
}
|
||
|
||
// 其他状态
|
||
return `status-${task.status}`
|
||
}
|
||
|
||
// 加载所有Tab的数量统计
|
||
const loadAllTabCounts = async () => {
|
||
try {
|
||
// 并行加载所有Tab的数量统计
|
||
const promises: Promise<any>[] = []
|
||
|
||
// 1. 可接任务数量
|
||
promises.push(
|
||
taskApi.getTaskHall({ page: 1, pageSize: 1 }).then(res => {
|
||
if (res.data && res.data.total !== undefined) {
|
||
total.value = res.data.total
|
||
}
|
||
}).catch(() => {})
|
||
)
|
||
|
||
// 2. 进行中任务数量
|
||
promises.push(
|
||
taskApi.getTasks({
|
||
status: 'in_progress',
|
||
role: 'receiver',
|
||
page: 1,
|
||
pageSize: 1
|
||
}).then(res => {
|
||
if (res.data && res.data.total !== undefined) {
|
||
inProgressTotal.value = res.data.total
|
||
}
|
||
}).catch(() => {})
|
||
)
|
||
|
||
// 3. 待审核任务数量(主任务 + 子任务组)
|
||
if (isDepartmentLeader.value) {
|
||
// 部门负责人:同时计算主任务和子任务组
|
||
promises.push(
|
||
Promise.all([
|
||
taskApi.getAllPendingApprovalTasks({ page: 1, pageSize: 1 }),
|
||
taskApi.getPendingApprovalTaskGroups()
|
||
]).then(([tasksRes, groupsRes]) => {
|
||
let count = 0
|
||
if (tasksRes.data && tasksRes.data.total !== undefined) {
|
||
count += tasksRes.data.total
|
||
}
|
||
if (groupsRes.code === 200 && groupsRes.data) {
|
||
count += groupsRes.data.length
|
||
}
|
||
reviewTotal.value = count
|
||
}).catch(() => {})
|
||
)
|
||
} else {
|
||
// 普通用户:只计算主任务
|
||
promises.push(
|
||
taskApi.getAllPendingApprovalTasks({
|
||
page: 1,
|
||
pageSize: 1
|
||
}).then(res => {
|
||
if (res.data && res.data.total !== undefined) {
|
||
reviewTotal.value = res.data.total
|
||
}
|
||
}).catch(() => {})
|
||
)
|
||
}
|
||
|
||
// 4. 已完成任务数量
|
||
promises.push(
|
||
taskApi.getCompletedTasks({ page: 1, pageSize: 1 }).then(res => {
|
||
if (res.data && res.data.total !== undefined) {
|
||
completedTotal.value = res.data.total
|
||
}
|
||
}).catch(() => {})
|
||
)
|
||
|
||
// 5. 我发起的任务数量
|
||
promises.push(
|
||
taskApi.getMyTasks({ role: 'publisher', page: 1, pageSize: 1 }).then(res => {
|
||
if (res.code === 200 && res.data && res.data.total !== undefined) {
|
||
publishedTotal.value = res.data.total
|
||
}
|
||
}).catch(() => {})
|
||
)
|
||
|
||
await Promise.all(promises)
|
||
} 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);
|
||
}
|
||
|
||
// 带预览图的卡片布局
|
||
.card-with-preview {
|
||
display: flex;
|
||
gap: 16px;
|
||
|
||
.task-preview {
|
||
flex-shrink: 0;
|
||
width: 65px;
|
||
height: 65px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
|
||
.card-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
&.has-preview {
|
||
.task-title {
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 审核任务卡片特殊样式
|
||
.task-info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
margin: 16px 0;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.info-label {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
|
||
&.score-value {
|
||
color: #31f08b;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.task-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
// 任务编号区域样式
|
||
.task-no-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 10px 0;
|
||
margin-top: 10px;
|
||
border-top: 1px solid rgba(49, 240, 139, 0.2);
|
||
font-size: 12px;
|
||
|
||
span {
|
||
color: #ffffff !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.task-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
// 流程阶段
|
||
.process-stage {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-xs;
|
||
padding: $space-sm $space-md;
|
||
margin-bottom: $space-md;
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
border-radius: $radius-md;
|
||
font-size: $font-size-sm;
|
||
color: #31f08b;
|
||
|
||
.stage-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.stage-text {
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
gap: $space-sm;
|
||
}
|
||
|
||
.task-badges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: $space-xs;
|
||
align-items: center;
|
||
}
|
||
|
||
.task-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin: 0 0 8px 0;
|
||
// 限制标题最多显示2行
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
line-height: 1.4;
|
||
max-height: 2.8em;
|
||
word-break: break-all;
|
||
}
|
||
|
||
// 状态标签行(单独一行)
|
||
.task-badges-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4px 8px;
|
||
border-radius: $radius-small;
|
||
font-size: $font-size-xs;
|
||
font-weight: $font-weight-medium;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.badge-subtask {
|
||
background: rgba(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-assigned {
|
||
background: rgba(24, 144, 255, 0.1);
|
||
border: 1px solid rgba(24, 144, 255, 0.2);
|
||
color: #1890ff;
|
||
}
|
||
|
||
// 待填写ID状态样式 - 醒目警告色
|
||
.status-pending-id {
|
||
background: rgba(250, 173, 20, 0.2);
|
||
border: 1px solid rgba(250, 173, 20, 0.5);
|
||
color: #faad14;
|
||
font-weight: 600;
|
||
animation: pulse-warning 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse-warning {
|
||
0%, 100% {
|
||
box-shadow: 0 0 0 0 rgba(250, 173, 20, 0.4);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 8px 2px rgba(250, 173, 20, 0.3);
|
||
}
|
||
}
|
||
|
||
// 已完结状态样式
|
||
.status-finished {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border: 1px solid rgba(49, 240, 139, 0.3);
|
||
color: #31f08b;
|
||
}
|
||
|
||
// 确认完结区域样式
|
||
.confirm-complete-section {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid $border-primary;
|
||
}
|
||
|
||
// 所有子任务交付路径样式
|
||
.all-delivery-paths {
|
||
.delivery-paths-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.delivery-path-item {
|
||
padding: 8px 12px;
|
||
background: $bg-tertiary;
|
||
border-radius: $radius-sm;
|
||
font-size: 13px;
|
||
|
||
.delivery-link {
|
||
color: #31f08b;
|
||
text-decoration: none;
|
||
word-break: break-all;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.delivery-text {
|
||
color: $text-secondary;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 驳回信息显示
|
||
.reject-info {
|
||
margin: 12px 0;
|
||
padding: 10px 12px;
|
||
background: rgba(245, 34, 45, 0.08);
|
||
border: 1px solid rgba(245, 34, 45, 0.2);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
|
||
.reject-label {
|
||
color: #f5222d;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.reject-reason {
|
||
color: #ff7875;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
|
||
// 交付路径
|
||
.delivery-path {
|
||
padding: 12px;
|
||
background: rgba(49, 240, 139, 0.05);
|
||
border-radius: 6px;
|
||
|
||
.delivery-link {
|
||
color: #31f08b;
|
||
text-decoration: none;
|
||
word-break: break-all;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.delivery-text {
|
||
color: #e0e0e0;
|
||
margin: 0;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.badge-completed {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
// 任务信息容器(包含预览图)
|
||
.task-info-wrapper {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
margin-bottom: $space-md;
|
||
padding-bottom: $space-md;
|
||
border-bottom: 1px solid $border-primary;
|
||
}
|
||
|
||
// 任务信息
|
||
.task-info {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: $space-md;
|
||
}
|
||
|
||
// 预览图缩略图
|
||
.preview-thumbnail {
|
||
position: relative;
|
||
width: 45px;
|
||
height: 45px;
|
||
flex-shrink: 0;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
border: 1px solid $border-primary;
|
||
cursor: pointer;
|
||
|
||
.thumbnail-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.preview-popup {
|
||
display: none;
|
||
position: absolute;
|
||
top: 50%;
|
||
right: calc(100% + 10px);
|
||
transform: translateY(-50%);
|
||
width: 300px;
|
||
height: 300px;
|
||
background: $bg-card;
|
||
border: 2px solid $brand-primary;
|
||
border-radius: 8px;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
padding: 4px;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
&:hover .preview-popup {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.label {
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
.value {
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-medium;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
// 任务类型明细
|
||
.type-details-section {
|
||
background: rgba($brand-primary, 0.05);
|
||
border: 1px solid rgba($brand-primary, 0.2);
|
||
border-radius: 8px;
|
||
padding: $space-sm $space-md;
|
||
margin-bottom: $space-md;
|
||
|
||
.type-details-title {
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
margin-bottom: $space-xs;
|
||
}
|
||
|
||
.type-details-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.type-detail-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: $font-size-sm;
|
||
padding: 2px 0;
|
||
|
||
.type-name {
|
||
color: $text-secondary;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.type-score {
|
||
color: $brand-primary;
|
||
font-weight: $font-weight-medium;
|
||
margin-left: $space-md;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 任务详情
|
||
.task-details {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: $space-sm;
|
||
margin-bottom: $space-md;
|
||
|
||
// 响应式:小屏幕时改为单列
|
||
@media (max-width: 768px) {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.task-footer {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: $space-md;
|
||
padding-top: $space-sm;
|
||
border-top: 1px solid $border-primary;
|
||
margin-top: $space-sm;
|
||
|
||
.footer-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
|
||
&.task-no-item {
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
min-width: 0; // 允许flex子项收缩
|
||
|
||
.icon {
|
||
font-size: $font-size-base;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
// 确保文本不会换行,保持紧凑
|
||
span:not(.icon) {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
// 附件项需要跨两列(因为可能包含多个链接)
|
||
&.detail-item-attachment {
|
||
grid-column: 1 / -1;
|
||
|
||
span:not(.icon) {
|
||
white-space: normal;
|
||
overflow: visible;
|
||
text-overflow: clip;
|
||
}
|
||
}
|
||
|
||
// 子任务进度样式(保留旧样式备用)
|
||
&.subtask-progress {
|
||
grid-column: 1 / -1;
|
||
|
||
.subtask-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: nowrap;
|
||
white-space: nowrap;
|
||
|
||
.stat {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 22px;
|
||
padding: 0 6px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
|
||
&.completed {
|
||
background: rgba(82, 196, 26, 0.1);
|
||
color: #52c41a;
|
||
}
|
||
|
||
&.in-progress {
|
||
background: rgba(24, 144, 255, 0.1);
|
||
color: #1890ff;
|
||
}
|
||
|
||
&.pending {
|
||
background: rgba(250, 173, 20, 0.1);
|
||
color: #faad14;
|
||
}
|
||
|
||
&.total {
|
||
color: #999;
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
.stat-label {
|
||
color: #888;
|
||
font-size: 12px;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 子任务时间线样式
|
||
.subtask-timeline-wrapper {
|
||
width: 100%;
|
||
margin: 16px 0;
|
||
padding: 16px 20px;
|
||
background: rgba(0, 0, 0, 0.25);
|
||
border-radius: 8px;
|
||
|
||
&.full-width {
|
||
// 确保横向拉满
|
||
box-sizing: border-box;
|
||
}
|
||
}
|
||
|
||
.subtask-timeline {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
position: relative;
|
||
|
||
// 连接线
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 10px;
|
||
left: 20px;
|
||
right: 20px;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, #52c41a 0%, #303030 50%, #303030 100%);
|
||
z-index: 0;
|
||
}
|
||
|
||
.timeline-node {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
position: relative;
|
||
z-index: 1;
|
||
min-width: 60px;
|
||
max-width: 100px;
|
||
|
||
.node-dot {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #303030;
|
||
border: 3px solid #444;
|
||
margin-bottom: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.node-label {
|
||
font-size: 12px;
|
||
color: #888;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
margin-bottom: 4px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.node-status {
|
||
font-size: 11px;
|
||
color: #666;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
// 待接单状态
|
||
&.pending {
|
||
.node-dot {
|
||
background: #303030;
|
||
border-color: #faad14;
|
||
}
|
||
.node-label {
|
||
color: #faad14;
|
||
}
|
||
.node-status {
|
||
color: #faad14;
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
// 进行中状态
|
||
&.in-progress {
|
||
.node-dot {
|
||
background: #1890ff;
|
||
border-color: #1890ff;
|
||
box-shadow: 0 0 10px rgba(24, 144, 255, 0.6);
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
.node-label {
|
||
color: #1890ff;
|
||
font-weight: 600;
|
||
}
|
||
.node-status {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
// 已完成状态
|
||
&.completed {
|
||
.node-dot {
|
||
background: #52c41a;
|
||
border-color: #52c41a;
|
||
}
|
||
.node-label {
|
||
color: #52c41a;
|
||
}
|
||
.node-status {
|
||
color: #52c41a;
|
||
}
|
||
}
|
||
|
||
// 更多节点
|
||
&.more {
|
||
flex: 0.6;
|
||
.node-dot {
|
||
width: 18px;
|
||
height: 18px;
|
||
background: #444;
|
||
border-color: #666;
|
||
}
|
||
.node-label {
|
||
color: #888;
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
// 完结节点
|
||
&.final {
|
||
.node-dot {
|
||
background: #303030;
|
||
border-color: #666;
|
||
}
|
||
.node-label {
|
||
color: #888;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.completed {
|
||
.node-dot {
|
||
background: #52c41a;
|
||
border-color: #52c41a;
|
||
}
|
||
.node-label {
|
||
color: #52c41a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 脉冲动画
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
box-shadow: 0 0 4px rgba(24, 144, 255, 0.4);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 12px rgba(24, 144, 255, 0.8);
|
||
}
|
||
}
|
||
|
||
// 任务操作
|
||
.task-actions {
|
||
display: flex;
|
||
gap: $space-sm;
|
||
}
|
||
|
||
// 任务详情抽屉
|
||
.task-detail {
|
||
.detail-section {
|
||
margin-bottom: $space-xl;
|
||
|
||
h3 {
|
||
font-size: $font-size-2xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
h4 {
|
||
font-size: $font-size-lg;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
p {
|
||
color: $text-secondary;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.detail-badges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: $space-xs;
|
||
margin-top: $space-sm;
|
||
}
|
||
|
||
.detail-actions {
|
||
margin-top: $space-2xl;
|
||
padding-top: $space-xl;
|
||
border-top: 1px solid $border-primary;
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: $space-md;
|
||
|
||
button {
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.attachment-links {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-sm;
|
||
|
||
.attachment-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: $space-sm $space-md;
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-md;
|
||
color: $text-primary;
|
||
text-decoration: none;
|
||
transition: all $transition-base;
|
||
|
||
&:hover {
|
||
background: $bg-card-hover;
|
||
border-color: #31f08b;
|
||
color: #31f08b;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分页
|
||
.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;
|
||
flex-wrap: wrap;
|
||
|
||
.prev-assets-btn {
|
||
background: rgba(24, 144, 255, 0.1);
|
||
border-color: #1890ff;
|
||
color: #1890ff;
|
||
|
||
&:hover {
|
||
background: rgba(24, 144, 255, 0.2);
|
||
}
|
||
}
|
||
margin-top: $space-md;
|
||
|
||
button {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
// 任务卡片特定样式 - 统一简洁风格,不使用彩色边框
|
||
.task-card.in-progress,
|
||
.task-card.review,
|
||
.task-card.approval-card {
|
||
// 移除彩色边框,保持统一灰色底色
|
||
}
|
||
|
||
// badge样式补充 - 统一使用绿色主题
|
||
.status-draft {
|
||
background: rgba(250, 173, 20, 0.1);
|
||
color: #faad14;
|
||
border: 1px solid rgba(250, 173, 20, 0.2);
|
||
}
|
||
|
||
.status-rejected {
|
||
background: rgba(245, 34, 45, 0.1);
|
||
color: #f5222d;
|
||
border: 1px solid rgba(245, 34, 45, 0.2);
|
||
}
|
||
|
||
.badge-in-progress {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
.badge-review {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
.badge-approval {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
color: #31f08b;
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
// 按钮样式
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: $radius-md;
|
||
font-size: $font-size-sm;
|
||
font-weight: $font-weight-medium;
|
||
cursor: pointer;
|
||
transition: all $transition-base;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #31f08b;
|
||
color: #000;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: #28d477;
|
||
transform: translateY(-1px);
|
||
}
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
color: $text-primary;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: $bg-card-hover;
|
||
border-color: $border-color;
|
||
}
|
||
}
|
||
|
||
.btn-danger {
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
color: $text-secondary;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: $bg-card-hover;
|
||
border-color: $border-color;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
// 商品ID管理样式
|
||
.product-section {
|
||
.product-count {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.product-list {
|
||
margin: $space-md 0;
|
||
|
||
.product-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: $space-sm $space-md;
|
||
background: $bg-secondary;
|
||
border-radius: $radius-md;
|
||
margin-bottom: $space-xs;
|
||
|
||
.product-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
|
||
.product-id {
|
||
font-family: 'SF Mono', Monaco, monospace;
|
||
font-size: $font-size-base;
|
||
color: #31f08b;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.product-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
|
||
.creator {
|
||
&::after {
|
||
content: '·';
|
||
margin-left: $space-sm;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-products {
|
||
text-align: center;
|
||
padding: $space-xl;
|
||
color: $text-tertiary;
|
||
|
||
p {
|
||
margin-top: $space-sm;
|
||
font-size: $font-size-sm;
|
||
}
|
||
}
|
||
|
||
.add-product-form {
|
||
margin-top: $space-md;
|
||
padding-top: $space-md;
|
||
border-top: 1px solid $border-primary;
|
||
|
||
.batch-add {
|
||
margin-top: $space-sm;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 内联附件链接样式
|
||
.task-details {
|
||
.attachment-link {
|
||
color: #31f08b;
|
||
text-decoration: none;
|
||
margin-left: 4px;
|
||
margin-right: 8px;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 审批弹窗暗黑主题适配 */
|
||
:deep(.approve-modal) .ant-modal-content,
|
||
:deep(.approve-modal) .ant-modal-header {
|
||
background-color: #1f1f1f !important;
|
||
color: #fff !important;
|
||
border-bottom: 1px solid #303030;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-modal-title {
|
||
color: #fff !important;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-modal-close {
|
||
color: rgba(255, 255, 255, 0.45) !important;
|
||
|
||
&:hover {
|
||
color: #fff !important;
|
||
}
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-radio-wrapper {
|
||
color: #fff !important;
|
||
transition: all 0.3s;
|
||
|
||
&:hover .ant-radio-inner {
|
||
border-color: #31f08b !important;
|
||
}
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-radio-checked .ant-radio-inner {
|
||
border-color: #31f08b !important;
|
||
background-color: #31f08b !important;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-radio-checked .ant-radio-inner::after {
|
||
background-color: #fff !important;
|
||
}
|
||
|
||
/* 选中状态的 Radio 卡片边框颜色 */
|
||
:deep(.approve-modal) .assign-option.ant-radio-wrapper-checked {
|
||
border-color: #31f08b !important;
|
||
background-color: rgba(49, 240, 139, 0.1) !important;
|
||
}
|
||
|
||
/* 下拉框暗黑适配 */
|
||
:deep(.approve-modal) .ant-select-selector {
|
||
background-color: #141414 !important;
|
||
border-color: #303030 !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-select:not(.ant-select-disabled):hover .ant-select-selector,
|
||
:deep(.approve-modal) .ant-select-focused .ant-select-selector {
|
||
border-color: #31f08b !important;
|
||
box-shadow: 0 0 0 2px rgba(49, 240, 139, 0.2) !important;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-select-arrow {
|
||
color: rgba(255, 255, 255, 0.45) !important;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-select-item-option-selected {
|
||
background-color: rgba(49, 240, 139, 0.1) !important;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-select-item-option-active {
|
||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||
}
|
||
|
||
/* 输入框适配 */
|
||
:deep(.approve-modal) .ant-input-number {
|
||
background-color: #141414;
|
||
border-color: #303030;
|
||
color: #fff;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-input-number:hover,
|
||
:deep(.approve-modal) .ant-input-number-focused {
|
||
border-color: #31f08b;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-input-number-handler-wrap {
|
||
background-color: #262626;
|
||
border-left-color: #303030;
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-input-number-handler {
|
||
border-top-color: #303030;
|
||
|
||
&:hover .ant-input-number-handler-up-inner,
|
||
&:hover .ant-input-number-handler-down-inner {
|
||
color: #31f08b;
|
||
}
|
||
}
|
||
|
||
/* 按钮适配 */
|
||
:deep(.approve-modal) .ant-btn-primary {
|
||
background-color: #31f08b;
|
||
border-color: #31f08b;
|
||
color: #000;
|
||
font-weight: 500;
|
||
|
||
&:hover {
|
||
background-color: #5cf2a3;
|
||
border-color: #5cf2a3;
|
||
}
|
||
}
|
||
|
||
:deep(.approve-modal) .ant-btn-text.ant-btn-danger:hover {
|
||
background-color: rgba(255, 77, 79, 0.1);
|
||
}
|
||
|
||
/* 级联选择器深色主题 - 组件内部 */
|
||
:deep(.approve-modal) .ant-cascader {
|
||
.ant-select-selector {
|
||
background-color: #141414 !important;
|
||
border-color: #303030 !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
&:hover .ant-select-selector {
|
||
border-color: #31f08b !important;
|
||
}
|
||
|
||
.ant-select-selection-placeholder {
|
||
color: #666 !important;
|
||
}
|
||
|
||
.ant-select-selection-item {
|
||
color: #fff !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- 全局样式:级联选择器弹窗深色主题(弹窗渲染在body上) -->
|
||
<style>
|
||
.dark-cascader-popup {
|
||
background-color: #1f1f1f !important;
|
||
border-radius: 8px;
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5) !important;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-menu {
|
||
background-color: #1f1f1f !important;
|
||
border-color: #303030 !important;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-menu-item {
|
||
color: #fff !important;
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-menu-item:hover {
|
||
background-color: rgba(49, 240, 139, 0.1) !important;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled),
|
||
.dark-cascader-popup .ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled):hover {
|
||
background-color: rgba(49, 240, 139, 0.2) !important;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-menu-item-expand-icon {
|
||
color: #888 !important;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-menu-item-content {
|
||
color: #fff !important;
|
||
}
|
||
|
||
/* 搜索框深色主题 */
|
||
.dark-cascader-popup .ant-cascader-input {
|
||
background-color: #262626 !important;
|
||
border-color: #303030 !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
.dark-cascader-popup .ant-cascader-input::placeholder {
|
||
color: #666 !important;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.dark-cascader-popup .ant-cascader-empty {
|
||
color: #888 !important;
|
||
}
|
||
</style>
|
||
|