6288 lines
205 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<DefaultLayout>
<div class="task-hall-page">
<div class="container">
<!-- 数据总览 -->
<div class="overview-section">
<div class="overview-header">
<h2 class="section-title">数据总览</h2>
<div class="overview-actions">
<a-month-picker
v-model:value="selectedMonth"
placeholder="选择月份"
format="YYYY-MM"
@change="handleMonthChange"
style="width: 150px; margin-right: 12px;"
/>
<a-button type="primary" size="large" @click="handleCreateTask">
<Icon name="plus" size="sm" style="margin-right: 6px;" />
发起任务
</a-button>
</div>
</div>
<div class="stats-grid">
<div
v-for="card in dataCards"
:key="card.id"
class="stat-card"
>
<div class="stat-icon">
<Icon :name="card.iconName" size="xl" color="#31f08b" />
</div>
<div class="stat-content">
<div class="stat-label">{{ card.label }}</div>
<div class="stat-value">{{ card.value }}</div>
</div>
</div>
</div>
</div>
<!-- 标签页切换 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange" class="task-tabs">
<a-tab-pane key="available" tab="可接任务">
<!-- 搜索和筛选栏(合并) -->
<div class="search-filter-bar chart-card">
<!-- 第一行:搜索、排序和统计 -->
<div class="search-row">
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索任务标题、SPU信息、发布人..."
class="search-input"
@search="handleFilterChange"
@pressEnter="handleFilterChange"
/>
<a-select
v-model:value="filters.sortBy"
placeholder="排序方式"
class="sort-select"
@change="handleFilterChange"
>
<a-select-option value="latest">最新发布</a-select-option>
<a-select-option value="score">积分最高</a-select-option>
<a-select-option value="deadline">时效最短</a-select-option>
<a-select-option value="hot">最热门</a-select-option>
</a-select>
<span class="stats-text">共 {{ total }} 个任务</span>
</div>
<!-- 第二行:筛选条件 -->
<div class="filter-row">
<a-select
v-model:value="filters.taskTypeId"
placeholder="任务类型"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.demandTypeId"
placeholder="需求类型"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="type in demandTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.brandId"
placeholder="品牌"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="brand in brands"
:key="brand.id"
:value="brand.id"
>
{{ brand.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.categoryId"
placeholder="类目"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.platformId"
placeholder="平台"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option
v-for="platform in platforms"
:key="platform.id"
:value="platform.id"
>
{{ platform.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.timeRange"
placeholder="发布时间"
class="filter-select"
allowClear
@change="handleFilterChange"
>
<a-select-option value="today">今天</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
<div class="score-range">
<a-input
v-model:value="filters.minScore"
placeholder="最小积分"
class="score-input"
@pressEnter="handleFilterChange"
/>
<span class="score-separator">-</span>
<a-input
v-model:value="filters.maxScore"
placeholder="最大积分"
class="score-input"
@pressEnter="handleFilterChange"
/>
</div>
<a-button @click="handleReset" class="reset-btn">重置</a-button>
</div>
</div>
<!-- 任务列表 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" />
</div>
<div v-else-if="taskList.length === 0" class="empty-state chart-card">
<div class="empty-icon">
<Icon name="inbox" size="xl" color="#6B6B6B" />
</div>
<h3>暂无可接任务</h3>
<p>当前没有待接单的任务,请稍后再来查看</p>
</div>
<div v-else class="task-grid">
<div
v-for="task in taskList"
:key="task.id"
class="task-card chart-card"
@click="handleTaskClick(task)"
>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
<div class="task-badges">
<span v-if="task.isSubTask" class="badge badge-subtask">
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
子任务
</span>
<span v-if="isNewTask(task)" class="badge badge-new">
<Icon name="sparkles" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
新发布
</span>
<span v-if="isHighScoreTask(task)" class="badge badge-high-score">
<Icon name="star" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
高分
</span>
<span v-if="task.isUrgent" class="badge badge-urgent">
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
紧急
</span>
</div>
</div>
<!-- 流程阶段 -->
<div v-if="getProcessStage(task)" class="process-stage">
<span class="stage-icon">
<Icon name="clock" size="sm" color="#31f08b" />
</span>
<span class="stage-text">{{ getProcessStage(task) }}</span>
</div>
<!-- 任务信息 -->
<div class="task-info-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>