3037 lines
89 KiB
Vue
3037 lines
89 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="create-task-page">
|
||
<div class="container">
|
||
<!-- 积分余额提示 -->
|
||
<div class="score-balance-card chart-card">
|
||
<div class="balance-info">
|
||
<div class="balance-label">团队积分余额</div>
|
||
<div class="balance-value">{{ teamScore }} 分</div>
|
||
</div>
|
||
<div class="balance-tip">
|
||
<span class="tip-icon">
|
||
<Icon name="info" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span v-if="isAdmin" style="color: #31f08b;">管理员身份,无需积分即可发起任务</span>
|
||
<span v-else>本次任务预估消耗: <strong>{{ estimatedScore }}</strong> 分</span>
|
||
</div>
|
||
|
||
<!-- 时效预估 -->
|
||
<div v-if="estimatedTotalHours > 0" class="time-estimate">
|
||
<div class="estimate-item">
|
||
<span class="estimate-label">累计时效:</span>
|
||
<span class="estimate-value">{{ formattedEstimatedTime }}</span>
|
||
</div>
|
||
<div class="estimate-item">
|
||
<span class="estimate-label">预计完成:</span>
|
||
<span class="estimate-value">{{ estimatedDeadline }}</span>
|
||
</div>
|
||
<div class="estimate-tip">
|
||
<Icon name="info" size="xs" style="margin-right: 4px; opacity: 0.6;" />
|
||
<span>按{{ workHoursPerDay }}小时/工作日计算</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务表单 -->
|
||
<div class="task-form chart-card">
|
||
<a-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
layout="vertical"
|
||
>
|
||
<!-- 任务标题(自动生成,不可编辑) -->
|
||
<a-form-item label="任务标题" name="title" required>
|
||
<a-input
|
||
v-model:value="formData.title"
|
||
placeholder="填写下方信息后自动生成标题"
|
||
size="large"
|
||
disabled
|
||
class="auto-generated-title"
|
||
/>
|
||
<div class="title-auto-tip">
|
||
<Icon name="info" size="xs" style="margin-right: 4px;" />
|
||
<span>标题将根据SPU、品牌、平台自动生成</span>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 流水线任务说明 -->
|
||
<a-form-item label="任务模式" style="display: block !important;">
|
||
<div style="padding: 12px; background: rgba(49, 240, 139, 0.05); border-radius: 6px; font-size: 13px; color: #8a8a8a; line-height: 1.6;">
|
||
<strong style="color: #31f08b;">顺序流水线模式:</strong>子任务按顺序完成,前一个完成后才能开始下一个。适合有先后依赖的任务(如:出图 → 修图 → 排版)。流水线任务可由多个设计师协作完成,每个设计师负责不同的子任务。
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 任务类型列表 -->
|
||
<a-form-item label="任务类型" required>
|
||
<div class="task-types-list">
|
||
<div
|
||
v-for="(item, index) in formData.taskTypes"
|
||
:key="item.id || index"
|
||
class="task-type-item"
|
||
:class="{ 'dragging': dragIndex === index, 'drag-over': dragOverIndex === index }"
|
||
@dragover.prevent="handleDragOver($event, index)"
|
||
@dragleave="handleDragLeave"
|
||
@drop="handleDrop($event, index)"
|
||
>
|
||
<div class="task-type-row">
|
||
<!-- 拖拽手柄 -->
|
||
<div
|
||
class="drag-handle"
|
||
title="拖拽调整顺序"
|
||
draggable="true"
|
||
@dragstart="handleDragStart($event, index)"
|
||
@dragend="handleDragEnd"
|
||
>
|
||
<Icon name="menu" size="sm" />
|
||
</div>
|
||
<div class="task-type-select task-type-cascader">
|
||
<a-cascader
|
||
v-model:value="item.taskTypePath"
|
||
:options="taskTypeCascaderOptions"
|
||
placeholder="请选择任务类型(一级→二级→三级)"
|
||
size="large"
|
||
:loading="loading"
|
||
allow-clear
|
||
expand-trigger="hover"
|
||
:show-search="{ filter: cascaderFilter }"
|
||
@change="(value: any) => onTaskTypeChange(value, item, index)"
|
||
popup-class-name="task-type-cascader-popup"
|
||
>
|
||
<template #displayRender="{ labels }">
|
||
<span>{{ labels.join(' / ') }}</span>
|
||
</template>
|
||
</a-cascader>
|
||
<div v-if="item.taskTypeId && getTaskTypeInfo(item.taskTypeId)" class="task-type-score-tip">
|
||
<span class="score-badge">{{ getTaskTypeInfo(item.taskTypeId)?.unitScore }}分/个</span>
|
||
<span class="hour-badge">{{ getTaskTypeInfo(item.taskTypeId)?.estimatedHours }}小时</span>
|
||
</div>
|
||
</div>
|
||
<div class="task-type-quantity">
|
||
<a-input-number
|
||
v-model:value="item.quantity"
|
||
:min="1"
|
||
:max="10000"
|
||
placeholder="数量"
|
||
size="large"
|
||
style="width: 100%"
|
||
@change="calculateScore"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 部门选择器:当任务类型有多个接单部门时显示 -->
|
||
<div v-if="getTaskTypeDeptOptions(item).length > 1" class="task-type-dept">
|
||
<a-select
|
||
v-model:value="item.selectedDeptId"
|
||
placeholder="选择接单部门"
|
||
style="width: 160px"
|
||
size="large"
|
||
@change="calculateScore"
|
||
>
|
||
<a-select-option
|
||
v-for="dept in getTaskTypeDeptOptions(item)"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
>
|
||
{{ dept.deptName }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div v-if="formData.releaseType === 'assigned'" class="task-type-receiver">
|
||
<a-select
|
||
v-model:value="item.receiverId"
|
||
placeholder="选择设计师(可留空)"
|
||
style="width: 200px"
|
||
show-search
|
||
allow-clear
|
||
size="large"
|
||
:filter-option="filterDesignerOption"
|
||
option-filter-prop="label"
|
||
@focus="loadDesignersForItem(item)"
|
||
>
|
||
<a-select-option
|
||
v-for="designer in getDesignersForItem(item)"
|
||
:key="designer.id"
|
||
:value="designer.id"
|
||
:label="designer.realName"
|
||
>
|
||
<div class="designer-option">
|
||
<span class="designer-name">{{ designer.realName }}</span>
|
||
<span class="designer-workload">进行中: {{ designer.inProgressCount || 0 }}</span>
|
||
</div>
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="task-type-actions">
|
||
<a-button
|
||
v-if="formData.taskTypes.length > 1"
|
||
danger
|
||
size="large"
|
||
@click="removeTaskType(index)"
|
||
>
|
||
<Icon name="trash" size="sm" />
|
||
删除
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<a-button
|
||
type="dashed"
|
||
size="large"
|
||
block
|
||
@click="addTaskType"
|
||
class="add-task-type-btn"
|
||
>
|
||
<Icon name="plus" size="sm" style="margin-right: 8px;" />
|
||
添加任务类型
|
||
</a-button>
|
||
|
||
<!-- 流水线任务分组预览 -->
|
||
<div v-if="pipelineGroups.length > 0" class="pipeline-preview">
|
||
<div class="preview-header">
|
||
<Icon name="layers" size="sm" style="margin-right: 6px;" />
|
||
<span>子任务分组预览</span>
|
||
<span class="preview-mode">(顺序执行)</span>
|
||
</div>
|
||
|
||
<div class="groups-container">
|
||
<div
|
||
v-for="(group, groupIndex) in pipelineGroups"
|
||
:key="groupIndex"
|
||
class="group-card"
|
||
>
|
||
<div class="group-header">
|
||
<div class="group-title">
|
||
<span class="group-badge">组 {{ groupIndex + 1 }}</span>
|
||
<span class="group-dept">{{ group.deptName || '所有部门' }}</span>
|
||
<span class="group-count">({{ group.items.length }}个任务)</span>
|
||
</div>
|
||
<div class="group-summary">
|
||
<span class="summary-item">{{ formatScore(group.totalScore) }}分</span>
|
||
<span class="summary-divider">|</span>
|
||
<span class="summary-item">{{ formatWorkHours(group.totalHours) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="group-items">
|
||
<div
|
||
v-for="(item, itemIndex) in group.items"
|
||
:key="itemIndex"
|
||
class="group-item"
|
||
>
|
||
<span class="item-icon">└─</span>
|
||
<span class="item-name">{{ item.name }}</span>
|
||
<span class="item-quantity">× {{ item.quantity }}</span>
|
||
<span class="item-score">= {{ formatScore(item.totalScore) }}分</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="preview-footer">
|
||
<span>总计:</span>
|
||
<span class="footer-score">{{ totalPipelineScore }}分</span>
|
||
<span class="footer-divider">|</span>
|
||
<span class="footer-hours">{{ formatWorkHours(Number(totalPipelineHours)) }}</span>
|
||
<span class="footer-divider">|</span>
|
||
<span class="footer-groups">{{ pipelineGroups.length }}个子任务组</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</a-form-item>
|
||
|
||
<!-- 任务子类型 -->
|
||
<a-form-item label="任务子类型" name="taskSubType" required>
|
||
<a-radio-group
|
||
v-model:value="formData.taskSubType"
|
||
button-style="solid"
|
||
size="large"
|
||
>
|
||
<a-radio-button
|
||
v-for="subType in taskSubTypes"
|
||
:key="subType.id"
|
||
:value="subType.name"
|
||
>
|
||
{{ subType.name }}
|
||
</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
|
||
<!-- 商品条码(当任务子类型需要检查库存时显示) -->
|
||
<a-form-item
|
||
v-if="selectedSubTypeNeedsInventoryCheck"
|
||
label="商品条码"
|
||
required
|
||
>
|
||
<!-- 已添加的条码列表 -->
|
||
<div v-if="formData.productBarcodes && formData.productBarcodes.length > 0" class="barcode-list">
|
||
<div class="barcode-list-title">
|
||
<span>已添加条码 ({{ formData.productBarcodes.length }}个)</span>
|
||
</div>
|
||
<div class="barcode-tags">
|
||
<a-tag
|
||
v-for="(barcode, index) in formData.productBarcodes"
|
||
:key="index"
|
||
color="green"
|
||
closable
|
||
@close="removeBarcode(index)"
|
||
>
|
||
{{ barcode }}
|
||
</a-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加新条码 -->
|
||
<div style="margin-top: 8px; display: flex; gap: 10px;">
|
||
<a-input
|
||
v-model:value="newBarcodeInput"
|
||
placeholder="请输入商品条码,验证库存后添加"
|
||
size="large"
|
||
style="flex: 1;"
|
||
@pressEnter="handleAddBarcode"
|
||
/>
|
||
<a-button
|
||
type="primary"
|
||
size="large"
|
||
@click="handleAddBarcode"
|
||
:loading="checkingInventory"
|
||
>
|
||
验证并添加
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 库存检查结果(临时显示) -->
|
||
<div v-if="inventoryCheckResult" class="inventory-result" :class="{ 'success': inventoryCheckResult.canPublish, 'error': !inventoryCheckResult.canPublish }">
|
||
<a-alert
|
||
:type="inventoryCheckResult.canPublish ? 'success' : 'error'"
|
||
:message="inventoryCheckResult.canPublish ? '库存充足,已添加' : '库存不足'"
|
||
:description="inventoryCheckResult.message"
|
||
show-icon
|
||
/>
|
||
<div v-if="inventoryCheckResult.found && !inventoryCheckResult.canPublish" class="inventory-details">
|
||
<div class="detail-item">
|
||
<span class="label">商品名称:</span>
|
||
<span class="value">{{ inventoryCheckResult.name || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="label">可用库存:</span>
|
||
<span class="value low-stock">{{ inventoryCheckResult.availableQty }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 品牌和平台 -->
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="品牌" name="brandId" required>
|
||
<a-select
|
||
v-model:value="formData.brandId"
|
||
placeholder="请选择品牌"
|
||
size="large"
|
||
show-search
|
||
option-filter-prop="label"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
:label="brand.name"
|
||
>
|
||
{{ brand.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="平台" name="platformId" required>
|
||
<a-select
|
||
v-model:value="formData.platformId"
|
||
placeholder="请选择平台"
|
||
size="large"
|
||
show-search
|
||
option-filter-prop="label"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
:label="platform.name"
|
||
>
|
||
{{ platform.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 类目 -->
|
||
<a-form-item label="叶子类目" name="categoryId" required>
|
||
<a-select
|
||
v-model:value="formData.categoryId"
|
||
placeholder="请选择叶子类目"
|
||
size="large"
|
||
show-search
|
||
option-filter-prop="label"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
:label="category.name"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
|
||
<!-- SPU信息 -->
|
||
<a-form-item label="SPU信息" name="spuInfo">
|
||
<a-input
|
||
v-model:value="formData.spuInfo"
|
||
placeholder="请输入SPU编号(支持大写字母、数字和横杠)"
|
||
size="large"
|
||
@input="handleSpuInput"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<!-- 预览图上传 -->
|
||
<a-form-item label="预览图" name="previewImage">
|
||
<div
|
||
class="preview-upload-area"
|
||
@paste="handlePasteImage"
|
||
tabindex="0"
|
||
style="outline: none;"
|
||
>
|
||
<a-upload
|
||
v-model:file-list="previewFileList"
|
||
:before-upload="beforePreviewUpload"
|
||
:custom-request="handlePreviewUpload"
|
||
list-type="picture-card"
|
||
:max-count="1"
|
||
accept="image/*"
|
||
@remove="handlePreviewRemove"
|
||
>
|
||
<div v-if="!formData.previewImage" class="upload-button">
|
||
<Icon name="plus" size="lg" />
|
||
<div class="upload-text">上传预览图</div>
|
||
<div class="paste-hint">或按 Ctrl+V 粘贴图片</div>
|
||
</div>
|
||
</a-upload>
|
||
<div class="preview-tip">
|
||
<span class="tip-icon">
|
||
<Icon name="image" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>建议上传正方形图片,系统将自动压缩处理,显示在任务卡片上。支持 Ctrl+V 粘贴图片</span>
|
||
</div>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 上传附件 -->
|
||
<a-form-item label="需求表格附件" name="attachmentUrl">
|
||
<div class="upload-area">
|
||
<a-upload
|
||
v-model:file-list="fileList"
|
||
:before-upload="beforeUpload"
|
||
:custom-request="handleUpload"
|
||
:remove="handleRemove"
|
||
@change="handleFileChange"
|
||
accept=".xls,.xlsx,.doc,.docx,.pdf,.zip"
|
||
:max-count="5"
|
||
list-type="text"
|
||
>
|
||
<a-button size="large">
|
||
<Icon name="upload" size="sm" style="margin-right: 8px;" />
|
||
点击上传或拖拽文件到此处
|
||
</a-button>
|
||
</a-upload>
|
||
<div class="upload-tip">
|
||
<span class="tip-icon">
|
||
<Icon name="document" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>支持格式:Excel (.xlsx/.xls)、Word (.docx/.doc)、PDF、ZIP,单文件最大500MB,最多上传5个文件</span>
|
||
</div>
|
||
<div v-if="uploadProgress > 0 && uploadProgress < 100" class="upload-progress">
|
||
<a-progress :percent="uploadProgress" :show-info="true" />
|
||
</div>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 任务发布类型 -->
|
||
<a-form-item label="任务发布类型" name="releaseType" required>
|
||
<div class="release-type-buttons">
|
||
<button
|
||
v-if="releaseTypeConfig.default"
|
||
type="button"
|
||
:class="['release-type-button', { active: formData.releaseType === 'default' }]"
|
||
@click="formData.releaseType = 'default'"
|
||
>
|
||
<div class="button-icon">
|
||
<Icon name="arrowPath" size="md" />
|
||
</div>
|
||
<div class="button-content">
|
||
<div class="button-title">默认模式(推荐)</div>
|
||
<div class="button-desc">系统自动分配给主管,由主管负责分配</div>
|
||
</div>
|
||
<div v-if="formData.releaseType === 'default'" class="selected-indicator">
|
||
<Icon name="checkCircle" size="sm" />
|
||
</div>
|
||
</button>
|
||
|
||
<!-- 招募任务模式 -->
|
||
<button
|
||
v-if="releaseTypeConfig.recruitment"
|
||
type="button"
|
||
:class="['release-type-button', { active: formData.releaseType === 'recruitment' }]"
|
||
@click="formData.releaseType = 'recruitment'"
|
||
>
|
||
<div class="button-icon">
|
||
<Icon name="bell" size="md" />
|
||
</div>
|
||
<div class="button-content">
|
||
<div class="button-title">招募任务</div>
|
||
<div class="button-desc">发布到任务大厅,设计师可自行抢单</div>
|
||
</div>
|
||
<div v-if="formData.releaseType === 'recruitment'" class="selected-indicator">
|
||
<Icon name="checkCircle" size="sm" />
|
||
</div>
|
||
</button>
|
||
|
||
<!-- 指定任务模式 -->
|
||
<button
|
||
v-if="releaseTypeConfig.assigned"
|
||
type="button"
|
||
:class="['release-type-button', { active: formData.releaseType === 'assigned' }]"
|
||
@click="formData.releaseType = 'assigned'"
|
||
>
|
||
<div class="button-icon">
|
||
<Icon name="star" size="md" />
|
||
</div>
|
||
<div class="button-content">
|
||
<div class="button-title">指定任务</div>
|
||
<div class="button-desc">指定特定设计师接收任务</div>
|
||
</div>
|
||
<div v-if="formData.releaseType === 'assigned'" class="selected-indicator">
|
||
<Icon name="checkCircle" size="sm" />
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
|
||
<!-- 是否紧急 -->
|
||
<a-form-item label="优先级">
|
||
<a-switch
|
||
v-model:checked="formData.isUrgent"
|
||
checked-children="紧急"
|
||
un-checked-children="普通"
|
||
/>
|
||
<span class="text-tertiary urgent-tip">
|
||
紧急任务会优先显示和处理
|
||
</span>
|
||
</a-form-item>
|
||
|
||
<!-- 提交按钮 -->
|
||
<div class="form-actions">
|
||
<a-button size="large" @click="handleCancel">
|
||
取消
|
||
</a-button>
|
||
<a-button
|
||
type="primary"
|
||
size="large"
|
||
:loading="submitting"
|
||
@click="handleSubmit"
|
||
>
|
||
{{ editMode ? '重新提交' : '发起任务' }}
|
||
</a-button>
|
||
</div>
|
||
</a-form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { message } from 'ant-design-vue'
|
||
import type { UploadFile, UploadProps } from 'ant-design-vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import * as userApi from '@/api/user'
|
||
import * as taskApi from '@/api/task'
|
||
import * as configApi from '@/api/config'
|
||
import * as uploadApi from '@/api/upload'
|
||
import * as departmentApi from '@/api/department'
|
||
import { getSystemConfigsMapByGroup, getWorkHoursConfig } from '@/api/system'
|
||
import { uploadFilesToS3 } from '@/api/file'
|
||
import type { FormInstance } from 'ant-design-vue'
|
||
import type { DemandType, Brand, Platform, Category, TaskSubType } from '@/api/config'
|
||
import { checkInventoryForTask, type InventoryResult } from '@/api/thirdPartyDb'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { useConfigStore } from '@/stores/config'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const authStore = useAuthStore()
|
||
|
||
// 编辑模式
|
||
const editMode = ref(false)
|
||
const editTaskId = ref<number | null>(null)
|
||
|
||
// 全局配置store
|
||
const configStore = useConfigStore()
|
||
|
||
// 表单ref
|
||
const formRef = ref<FormInstance>()
|
||
|
||
// 提交状态
|
||
const submitting = ref(false)
|
||
const loading = ref(false)
|
||
|
||
// 团队积分余额
|
||
const teamScore = ref(0)
|
||
|
||
// 判断是否是管理员(管理员跳过积分检查)
|
||
const isAdmin = computed(() => {
|
||
const userInfo = authStore.userInfo
|
||
if (!userInfo) return false
|
||
// 检查 isAdmin 字段
|
||
if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) return true
|
||
// 检查角色
|
||
if (userInfo.roleIds) {
|
||
const roleIds = userInfo.roleIds.split(',').map((id: string) => id.trim())
|
||
if (roleIds.includes('1') || roleIds.includes('2')) return true // 1=SUPER_ADMIN, 2=ADMIN
|
||
}
|
||
return false
|
||
})
|
||
|
||
// 任务子类型列表
|
||
const taskSubTypes = ref<TaskSubType[]>([])
|
||
|
||
// 发布类型配置(控制哪些按钮显示)
|
||
const releaseTypeConfig = ref({
|
||
default: true,
|
||
recruitment: true,
|
||
assigned: true
|
||
})
|
||
|
||
// 文件上传相关
|
||
const fileList = ref<UploadFile[]>([])
|
||
const uploadProgress = ref(0)
|
||
|
||
// 预览图上传相关
|
||
const previewFileList = ref<UploadFile[]>([])
|
||
const previewUploading = ref(false)
|
||
|
||
// 任务类型项接口
|
||
interface TaskTypeItem {
|
||
taskTypeId: number | undefined
|
||
taskTypePath?: (string | number)[] // 级联选择路径 [level1Id, level2Id, level3Id]
|
||
quantity: number
|
||
receiverId?: number // 指定接收人ID
|
||
selectedDeptId?: number // 当任务类型有多个接单部门时,用户选择的部门ID
|
||
}
|
||
|
||
// 级联选择器选项接口
|
||
interface CascaderOption {
|
||
value: number
|
||
label: string
|
||
children?: CascaderOption[]
|
||
unitScore?: number
|
||
estimatedHours?: number
|
||
}
|
||
|
||
// 设计师列表缓存 { taskType_{id}: designers[] }
|
||
const designersCache = reactive<Record<string, any[]>>({})
|
||
|
||
// 拖拽排序相关
|
||
const dragIndex = ref<number | null>(null)
|
||
const dragOverIndex = ref<number | null>(null)
|
||
|
||
// 表单数据
|
||
const formData = reactive({
|
||
title: '',
|
||
isPipeline: true, // 固定为流水线任务
|
||
pipelineMode: 'sequential' as 'sequential' | 'parallel', // 固定为顺序流水线
|
||
taskTypes: [{ taskTypeId: undefined, taskTypePath: undefined, quantity: 1, receiverId: undefined, selectedDeptId: undefined }] as TaskTypeItem[], // 任务类型数组
|
||
taskSubType: '', // 动态加载,初始为空
|
||
brandId: undefined,
|
||
platformId: undefined,
|
||
categoryId: undefined,
|
||
spuInfo: '',
|
||
attachmentUrl: '',
|
||
previewImage: '', // 预览图URL
|
||
remark: '', // 备注信息
|
||
releaseType: 'default',
|
||
productBarcodes: [] as string[], // 商品条码列表(已验证库存)
|
||
// receiverId: undefined, // 已移除,改用子任务指定
|
||
isUrgent: false
|
||
})
|
||
|
||
// 新条码输入框
|
||
const newBarcodeInput = ref('')
|
||
|
||
// 库存检查相关
|
||
const checkingInventory = ref(false)
|
||
const inventoryCheckResult = ref<InventoryResult | null>(null)
|
||
|
||
// 判断当前选择的任务子类型是否需要检查库存
|
||
const selectedSubTypeNeedsInventoryCheck = computed(() => {
|
||
if (!formData.taskSubType) return false
|
||
const selectedSubType = taskSubTypes.value.find(st => st.name === formData.taskSubType)
|
||
return selectedSubType?.checkInventory === 1
|
||
})
|
||
|
||
// 验证并添加条码
|
||
const handleAddBarcode = async () => {
|
||
const barcode = newBarcodeInput.value.trim()
|
||
|
||
if (!barcode) {
|
||
message.warning('请输入商品条码')
|
||
return
|
||
}
|
||
|
||
// 检查是否已添加
|
||
if (formData.productBarcodes.includes(barcode)) {
|
||
message.warning('该条码已添加')
|
||
return
|
||
}
|
||
|
||
checkingInventory.value = true
|
||
inventoryCheckResult.value = null
|
||
|
||
try {
|
||
const response = await checkInventoryForTask(barcode)
|
||
inventoryCheckResult.value = response.data
|
||
|
||
if (response.data.canPublish) {
|
||
// 库存充足,添加到列表
|
||
formData.productBarcodes.push(barcode)
|
||
newBarcodeInput.value = '' // 清空输入框
|
||
message.success(`条码 ${barcode} 库存充足,已添加`)
|
||
// 2秒后清除结果提示
|
||
setTimeout(() => {
|
||
inventoryCheckResult.value = null
|
||
}, 2000)
|
||
} else {
|
||
message.error(response.data.message || '库存不足,无法添加该条码')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('检查库存失败:', error)
|
||
message.error(error.response?.data?.message || '检查库存失败')
|
||
} finally {
|
||
checkingInventory.value = false
|
||
}
|
||
}
|
||
|
||
// 移除条码
|
||
const removeBarcode = (index: number) => {
|
||
formData.productBarcodes.splice(index, 1)
|
||
}
|
||
|
||
// 兼容旧函数名
|
||
const handleCheckInventory = handleAddBarcode
|
||
|
||
// 表单验证规则
|
||
const formRules = {
|
||
title: [
|
||
{ required: true, message: '请输入任务标题', trigger: 'blur' },
|
||
{ min: 2, max: 100, message: '标题长度在2-100个字符', trigger: 'blur' }
|
||
],
|
||
taskTypeIds: [
|
||
{
|
||
required: true,
|
||
type: 'array',
|
||
min: 1,
|
||
message: '请选择至少一个任务类型',
|
||
trigger: 'change'
|
||
}
|
||
],
|
||
quantity: [
|
||
{ required: true, message: '请输入数量', trigger: 'change' }
|
||
],
|
||
taskSubType: [
|
||
{ required: true, message: '请选择任务子类型', trigger: 'change' }
|
||
],
|
||
brandId: [
|
||
{ required: true, message: '请选择品牌', trigger: 'change' }
|
||
],
|
||
platformId: [
|
||
{ required: true, message: '请选择平台', trigger: 'change' }
|
||
],
|
||
categoryId: [
|
||
{ required: true, message: '请选择类目', trigger: 'change' }
|
||
],
|
||
releaseType: [
|
||
{ required: true, message: '请选择发布类型', trigger: 'change' }
|
||
]
|
||
}
|
||
|
||
// 使用全局配置store的数据
|
||
const taskTypes = computed(() => configStore.demandTypes)
|
||
const brands = computed(() => configStore.brands)
|
||
const platforms = computed(() => configStore.platforms)
|
||
const categories = computed(() => configStore.categories)
|
||
|
||
// 工作时间配置(用于时效格式化)
|
||
const workHoursPerDay = ref(9) // 默认9小时
|
||
|
||
// 层级任务类型数据
|
||
const hierarchicalTaskTypes = ref<any[]>([])
|
||
|
||
// 部门列表数据
|
||
const departmentList = ref<departmentApi.DepartmentOverview[]>([])
|
||
|
||
// 加载层级任务类型数据
|
||
const loadHierarchicalTaskTypes = async () => {
|
||
try {
|
||
const response = await configApi.getDemandTypesTree()
|
||
if (response && response.data) {
|
||
hierarchicalTaskTypes.value = response.data
|
||
}
|
||
} catch (error) {
|
||
console.error('加载层级任务类型失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载部门列表数据
|
||
const loadDepartmentList = async () => {
|
||
try {
|
||
const response = await departmentApi.getDepartmentList()
|
||
if (response && response.data) {
|
||
departmentList.value = response.data
|
||
}
|
||
} catch (error) {
|
||
console.error('加载部门列表失败:', error)
|
||
}
|
||
}
|
||
|
||
// 构建级联选择器选项(三级结构)
|
||
const taskTypeCascaderOptions = computed<CascaderOption[]>(() => {
|
||
const buildOptions = (items: any[]): CascaderOption[] => {
|
||
return items.map(item => {
|
||
const option: CascaderOption = {
|
||
value: item.id,
|
||
label: item.name,
|
||
unitScore: item.unitScore,
|
||
estimatedHours: item.estimatedHours
|
||
}
|
||
|
||
// 如果有子级,递归构建
|
||
if (item.children && item.children.length > 0) {
|
||
option.children = buildOptions(item.children)
|
||
}
|
||
|
||
return option
|
||
})
|
||
}
|
||
|
||
return buildOptions(hierarchicalTaskTypes.value)
|
||
})
|
||
|
||
// 级联选择器搜索过滤
|
||
const cascaderFilter = (inputValue: string, path: CascaderOption[]) => {
|
||
return path.some(option =>
|
||
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
|
||
)
|
||
}
|
||
|
||
// 任务类型级联选择变更
|
||
const onTaskTypeChange = (value: (string | number)[] | undefined, item: TaskTypeItem, index: number) => {
|
||
if (value && value.length > 0) {
|
||
// 取选择路径中的最后一个ID(叶子节点)
|
||
const lastId = value[value.length - 1] as number
|
||
// 检查该节点是否是叶子节点(没有子分类)
|
||
const selectedNode = findTaskTypeById(lastId)
|
||
if (selectedNode && (!selectedNode.children || selectedNode.children.length === 0)) {
|
||
// 是叶子节点,可以设置taskTypeId
|
||
item.taskTypeId = lastId
|
||
} else {
|
||
// 不是叶子节点,还需要继续选择
|
||
item.taskTypeId = undefined
|
||
}
|
||
item.taskTypePath = value
|
||
} else {
|
||
// 清空选择
|
||
item.taskTypeId = undefined
|
||
item.taskTypePath = undefined
|
||
}
|
||
calculateScore()
|
||
}
|
||
|
||
// 获取任务类型信息(根据ID递归查找层级数据)
|
||
const getTaskTypeInfo = (taskTypeId: number) => {
|
||
// 先从层级数据中查找(包含完整的分值信息)
|
||
const found = findTaskTypeById(taskTypeId, hierarchicalTaskTypes.value)
|
||
if (found) return found
|
||
// 兼容:如果层级数据中找不到,从平铺列表中查找
|
||
return taskTypes.value.find((t: any) => t.id === taskTypeId)
|
||
}
|
||
|
||
// 从平铺列表中根据ID查找任务类型(递归查找层级数据)
|
||
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
|
||
}
|
||
|
||
// 根据任务类型ID查找完整的级联路径
|
||
const findTaskTypePath = (targetId: number, items: any[] = hierarchicalTaskTypes.value, currentPath: number[] = []): number[] | null => {
|
||
for (const item of items) {
|
||
const newPath = [...currentPath, item.id]
|
||
if (item.id === targetId) {
|
||
return newPath
|
||
}
|
||
if (item.children && item.children.length > 0) {
|
||
const found = findTaskTypePath(targetId, item.children, newPath)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 加载设计师列表(根据任务类型ID)
|
||
const loadDesignersForItem = async (item: TaskTypeItem) => {
|
||
if (!item.taskTypeId) return
|
||
|
||
const cacheKey = `taskType_${item.taskTypeId}`
|
||
|
||
if (!designersCache[cacheKey]) {
|
||
try {
|
||
// 使用 taskTypeId 调用后端 API,后端会处理接单部门配置
|
||
const designers = await userApi.getDesignersWithWorkload(undefined, item.taskTypeId)
|
||
designersCache[cacheKey] = designers
|
||
} catch (error) {
|
||
console.error('加载设计师失败:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取当前任务类型对应的设计师列表
|
||
const getDesignersForItem = (item: TaskTypeItem) => {
|
||
if (!item.taskTypeId) return []
|
||
|
||
const cacheKey = `taskType_${item.taskTypeId}`
|
||
return designersCache[cacheKey] || []
|
||
}
|
||
|
||
// 添加任务类型
|
||
const addTaskType = () => {
|
||
formData.taskTypes.push({
|
||
taskTypeId: undefined,
|
||
taskTypePath: undefined,
|
||
quantity: 1,
|
||
receiverId: undefined,
|
||
selectedDeptId: undefined
|
||
})
|
||
}
|
||
|
||
// 删除任务类型
|
||
const removeTaskType = (index: number) => {
|
||
if (formData.taskTypes.length > 1) {
|
||
formData.taskTypes.splice(index, 1)
|
||
calculateScore()
|
||
}
|
||
}
|
||
|
||
// 拖拽开始
|
||
const handleDragStart = (e: DragEvent, index: number) => {
|
||
dragIndex.value = index
|
||
if (e.dataTransfer) {
|
||
e.dataTransfer.effectAllowed = 'move'
|
||
e.dataTransfer.setData('text/plain', String(index))
|
||
e.dataTransfer.setData('application/json', JSON.stringify({ index }))
|
||
// 设置拖拽图像为整个 item
|
||
const target = e.target as HTMLElement
|
||
const itemElement = target.closest('.task-type-item') as HTMLElement
|
||
if (itemElement) {
|
||
// 创建一个临时元素作为拖拽图像
|
||
const dragImage = itemElement.cloneNode(true) as HTMLElement
|
||
dragImage.style.opacity = '0.8'
|
||
dragImage.style.position = 'absolute'
|
||
dragImage.style.top = '-1000px'
|
||
document.body.appendChild(dragImage)
|
||
e.dataTransfer.setDragImage(dragImage, e.offsetX, e.offsetY)
|
||
setTimeout(() => document.body.removeChild(dragImage), 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 拖拽结束
|
||
const handleDragEnd = () => {
|
||
dragIndex.value = null
|
||
dragOverIndex.value = null
|
||
}
|
||
|
||
// 拖拽经过
|
||
const handleDragOver = (e: DragEvent, index: number) => {
|
||
e.preventDefault()
|
||
if (dragIndex.value !== null && dragIndex.value !== index) {
|
||
dragOverIndex.value = index
|
||
}
|
||
}
|
||
|
||
// 拖拽离开
|
||
const handleDragLeave = () => {
|
||
dragOverIndex.value = null
|
||
}
|
||
|
||
// 放置
|
||
const handleDrop = (e: DragEvent, targetIndex: number) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
|
||
// 从拖拽数据中获取源索引
|
||
let sourceIndex = dragIndex.value
|
||
if (sourceIndex === null) {
|
||
// 尝试从 dataTransfer 中读取
|
||
const data = e.dataTransfer?.getData('text/plain')
|
||
if (data) {
|
||
sourceIndex = parseInt(data, 10)
|
||
} else {
|
||
handleDragEnd()
|
||
return
|
||
}
|
||
}
|
||
|
||
if (sourceIndex === null || sourceIndex === targetIndex || sourceIndex < 0 || targetIndex < 0) {
|
||
handleDragEnd()
|
||
return
|
||
}
|
||
|
||
console.log('拖拽排序:', sourceIndex, '->', targetIndex)
|
||
|
||
// 交换位置 - 使用 splice 原地修改数组以确保响应式更新
|
||
const [removed] = formData.taskTypes.splice(sourceIndex, 1)
|
||
formData.taskTypes.splice(targetIndex, 0, removed)
|
||
|
||
// 重新计算积分(pipelineGroups 是 computed,会自动更新)
|
||
calculateScore()
|
||
|
||
message.success('任务顺序已调整')
|
||
|
||
handleDragEnd()
|
||
}
|
||
|
||
// 处理SPU输入(只允许大写字母、数字和横杠)
|
||
const handleSpuInput = (e: Event) => {
|
||
const input = e.target as HTMLInputElement
|
||
// 移除非大写字母、数字和横杠的字符,同时将小写字母转为大写
|
||
const filtered = input.value.toUpperCase().replace(/[^A-Z0-9\-]/g, '')
|
||
formData.spuInfo = filtered
|
||
}
|
||
|
||
// 自动生成任务标题(SPU + 品牌名 + 平台名)
|
||
const generateTitle = () => {
|
||
const parts: string[] = []
|
||
|
||
// 1. SPU信息
|
||
if (formData.spuInfo && formData.spuInfo.trim()) {
|
||
parts.push(formData.spuInfo.trim())
|
||
}
|
||
|
||
// 2. 品牌名称
|
||
if (formData.brandId) {
|
||
const brand = brands.value.find((b: any) => b.id === formData.brandId)
|
||
if (brand) {
|
||
parts.push(brand.name)
|
||
}
|
||
}
|
||
|
||
// 3. 平台名称
|
||
if (formData.platformId) {
|
||
const platform = platforms.value.find((p: any) => p.id === formData.platformId)
|
||
if (platform) {
|
||
parts.push(platform.name)
|
||
}
|
||
}
|
||
|
||
// 生成标题
|
||
if (parts.length > 0) {
|
||
formData.title = parts.join('-')
|
||
}
|
||
}
|
||
|
||
// 监听相关字段变化,自动生成标题(SPU + 品牌 + 平台)
|
||
watch(
|
||
() => [
|
||
formData.spuInfo,
|
||
formData.brandId,
|
||
formData.platformId
|
||
],
|
||
() => {
|
||
generateTitle()
|
||
}
|
||
)
|
||
|
||
// 格式化积分(保留两位小数,修复浮点数精度问题)
|
||
const formatScore = (score: number | null | undefined): string => {
|
||
if (score === null || score === undefined) return '0.00'
|
||
// 先乘以100,四舍五入取整,再除以100,避免浮点数精度问题
|
||
return (Math.round(Number(score) * 100) / 100).toFixed(2)
|
||
}
|
||
|
||
// 计算预估积分(累加所有任务类型)
|
||
const estimatedScore = computed(() => {
|
||
let totalScore = 0
|
||
|
||
for (const item of formData.taskTypes) {
|
||
if (item.taskTypeId && item.quantity > 0) {
|
||
const selectedType = getTaskTypeInfo(item.taskTypeId)
|
||
if (selectedType) {
|
||
totalScore += (selectedType.unitScore || 0) * item.quantity
|
||
}
|
||
}
|
||
}
|
||
|
||
return formatScore(totalScore)
|
||
})
|
||
|
||
// 计算累计时效(小时)
|
||
const estimatedTotalHours = computed(() => {
|
||
let totalHours = 0
|
||
|
||
for (const item of formData.taskTypes) {
|
||
if (item.taskTypeId && item.quantity > 0) {
|
||
const selectedType = getTaskTypeInfo(item.taskTypeId)
|
||
if (selectedType && selectedType.estimatedHours) {
|
||
totalHours += selectedType.estimatedHours * item.quantity
|
||
}
|
||
}
|
||
}
|
||
|
||
return totalHours
|
||
})
|
||
|
||
// 格式化时效显示(基于工作时间配置)
|
||
const formattedEstimatedTime = computed(() => {
|
||
const hours = estimatedTotalHours.value
|
||
if (hours === 0) return '--'
|
||
|
||
if (hours < 1) {
|
||
return `${Math.round(hours * 60)}分钟`
|
||
} else {
|
||
// 使用formatWorkHours函数来格式化(基于工作时间配置)
|
||
return formatWorkHours(hours)
|
||
}
|
||
})
|
||
|
||
// 计算预计完成时间(基于工作时间配置)
|
||
const estimatedDeadline = computed(() => {
|
||
const totalHours = estimatedTotalHours.value
|
||
if (totalHours === 0) return '--'
|
||
|
||
const now = new Date()
|
||
const workHours = workHoursPerDay.value || 9 // 默认9小时
|
||
|
||
// 计算需要多少个工作日
|
||
const totalDays = Math.ceil(totalHours / workHours)
|
||
// 按自然日计算(每工作日对应1个自然日)
|
||
const deadline = new Date(now.getTime() + totalDays * 24 * 60 * 60 * 1000)
|
||
return formatDeadline(deadline)
|
||
})
|
||
|
||
// 格式化截止时间
|
||
const formatDeadline = (date: Date) => {
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hour = String(date.getHours()).padStart(2, '0')
|
||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||
}
|
||
|
||
// 获取任务类型的部门配置(三级分类从二级父类继承)
|
||
const getTypeAllowedDeptIds = (typeInfo: any): string => {
|
||
// 如果当前类型有配置,直接返回
|
||
if (typeInfo.allowedDeptIds) {
|
||
return typeInfo.allowedDeptIds
|
||
}
|
||
|
||
// 如果有父级ID,查找父级的配置(三级分类继承二级分类)
|
||
if (typeInfo.parentId) {
|
||
const parentInfo = findTaskTypeById(typeInfo.parentId, hierarchicalTaskTypes.value)
|
||
if (parentInfo && parentInfo.allowedDeptIds) {
|
||
return parentInfo.allowedDeptIds
|
||
}
|
||
}
|
||
|
||
return ''
|
||
}
|
||
|
||
// 计算流水线分组
|
||
const pipelineGroups = computed(() => {
|
||
if (formData.taskTypes.length === 0) {
|
||
return []
|
||
}
|
||
|
||
const groups: Array<{
|
||
deptName: string
|
||
deptIds: string
|
||
items: Array<{
|
||
name: string
|
||
quantity: number
|
||
unitScore: number
|
||
totalScore: number
|
||
estimatedHours: number
|
||
}>
|
||
totalScore: number
|
||
totalHours: number
|
||
}> = []
|
||
|
||
let currentGroup: any = null
|
||
|
||
for (const taskType of formData.taskTypes) {
|
||
if (!taskType.taskTypeId || !taskType.quantity) continue
|
||
|
||
const typeInfo = getTaskTypeInfo(taskType.taskTypeId)
|
||
if (!typeInfo) continue
|
||
|
||
// 获取接单部门配置(三级分类从二级父类继承)
|
||
let deptIds = getTypeAllowedDeptIds(typeInfo)
|
||
let deptName = ''
|
||
|
||
// 如果用户选择了具体部门,使用用户选择的部门
|
||
if (taskType.selectedDeptId) {
|
||
deptIds = String(taskType.selectedDeptId)
|
||
const selectedDept = departmentList.value.find(d => d.id === taskType.selectedDeptId)
|
||
deptName = selectedDept?.deptName || '未知部门'
|
||
} else {
|
||
deptName = getDeptNameFromIds(deptIds)
|
||
}
|
||
|
||
// 判断是否需要创建新分组
|
||
if (!currentGroup || currentGroup.deptIds !== deptIds) {
|
||
// 保存之前的分组
|
||
if (currentGroup) {
|
||
groups.push(currentGroup)
|
||
}
|
||
|
||
// 创建新分组
|
||
currentGroup = {
|
||
deptName,
|
||
deptIds,
|
||
items: [],
|
||
totalScore: 0,
|
||
totalHours: 0
|
||
}
|
||
}
|
||
|
||
// 添加到当前分组
|
||
const itemScore = typeInfo.unitScore * taskType.quantity
|
||
const itemHours = typeInfo.estimatedHours * taskType.quantity
|
||
|
||
currentGroup.items.push({
|
||
name: typeInfo.name,
|
||
quantity: taskType.quantity,
|
||
unitScore: typeInfo.unitScore,
|
||
totalScore: itemScore,
|
||
estimatedHours: typeInfo.estimatedHours
|
||
})
|
||
|
||
currentGroup.totalScore += itemScore
|
||
currentGroup.totalHours += itemHours
|
||
}
|
||
|
||
// 添加最后一个分组
|
||
if (currentGroup && currentGroup.items.length > 0) {
|
||
groups.push(currentGroup)
|
||
}
|
||
|
||
return groups
|
||
})
|
||
|
||
// 获取任务类型可选的部门列表
|
||
const getTaskTypeDeptOptions = (item: TaskTypeItem): Array<{ id: number, deptName: string }> => {
|
||
if (!item.taskTypeId) return []
|
||
|
||
const typeInfo = getTaskTypeInfo(item.taskTypeId)
|
||
if (!typeInfo) return []
|
||
|
||
const deptIds = getTypeAllowedDeptIds(typeInfo)
|
||
if (!deptIds) return []
|
||
|
||
const ids = deptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
|
||
if (ids.length <= 1) return [] // 只有一个部门时不需要选择
|
||
|
||
// 返回部门列表
|
||
return ids.map(id => {
|
||
const dept = departmentList.value.find(d => d.id === id)
|
||
return dept ? { id: dept.id!, deptName: dept.deptName } : null
|
||
}).filter(d => d !== null) as Array<{ id: number, deptName: string }>
|
||
}
|
||
|
||
// 根据部门ID获取部门名称
|
||
const getDeptNameFromIds = (deptIds: string): string => {
|
||
if (!deptIds) return '所有部门'
|
||
|
||
const ids = deptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
|
||
if (ids.length === 0) return '所有部门'
|
||
|
||
// 从部门列表中查找部门信息
|
||
const depts = ids.map(id => departmentList.value.find(d => d.id === id)).filter(d => d)
|
||
if (depts.length === 0) return '所有部门'
|
||
|
||
// 如果只有一个部门,显示部门名称
|
||
if (depts.length === 1) {
|
||
return depts[0]!.deptName
|
||
}
|
||
|
||
// 检查是否所有部门属于同一个父部门
|
||
const parentIds = depts.map(d => d!.parentId).filter(id => id !== undefined && id !== null)
|
||
if (parentIds.length === depts.length) {
|
||
// 所有部门都有父部门
|
||
const uniqueParentIds = [...new Set(parentIds)]
|
||
if (uniqueParentIds.length === 1) {
|
||
// 所有部门属于同一个父部门,显示父部门名称
|
||
const parentDept = departmentList.value.find(d => d.id === uniqueParentIds[0])
|
||
if (parentDept) {
|
||
return parentDept.deptName
|
||
}
|
||
}
|
||
}
|
||
|
||
// 部门属于不同父部门,显示具体名称
|
||
const deptNames = depts.map(d => d!.deptName)
|
||
if (deptNames.length <= 3) {
|
||
return deptNames.join('、')
|
||
}
|
||
return `${deptNames.slice(0, 2).join('、')}等${deptNames.length}个部门`
|
||
}
|
||
|
||
// 流水线总积分
|
||
const totalPipelineScore = computed(() => {
|
||
return pipelineGroups.value.reduce((sum, group) => sum + group.totalScore, 0)
|
||
})
|
||
|
||
// 流水线总工时
|
||
const totalPipelineHours = computed(() => {
|
||
return pipelineGroups.value.reduce((sum, group) => sum + group.totalHours, 0).toFixed(1)
|
||
})
|
||
|
||
// 格式化工时显示(基于工作时间配置)
|
||
const formatWorkHours = (hours: number): string => {
|
||
if (hours === 0) return '0小时'
|
||
|
||
const workHours = workHoursPerDay.value || 9 // 默认9小时
|
||
const days = Math.floor(hours / workHours)
|
||
// 处理浮点数精度问题,保留两位小数
|
||
let remainingHours = Math.round((hours % workHours) * 100) / 100
|
||
|
||
// 格式化小时显示
|
||
const formatHours = (h: number): string => {
|
||
if (Number.isInteger(h)) return `${h}小时`
|
||
return `${h.toFixed(2).replace(/\.?0+$/, '')}小时`
|
||
}
|
||
|
||
if (days === 0) {
|
||
return formatHours(remainingHours)
|
||
} else if (remainingHours === 0) {
|
||
return `${days}天`
|
||
} else {
|
||
return `${days}天${formatHours(remainingHours)}`
|
||
}
|
||
}
|
||
|
||
// 计算积分
|
||
const calculateScore = () => {
|
||
// 计算已在computed中完成
|
||
const score = estimatedScore.value
|
||
// 管理员跳过积分检查
|
||
if (!isAdmin.value && score > teamScore.value) {
|
||
message.warning('团队积分余额不足,无法发起此任务')
|
||
}
|
||
}
|
||
|
||
// 筛选设计师选项
|
||
const filterDesignerOption = (input: string, option: any) => {
|
||
// 搜索设计师姓名
|
||
const designerName = option.label || ''
|
||
return designerName.toLowerCase().includes(input.toLowerCase())
|
||
}
|
||
|
||
// 取消
|
||
const handleCancel = () => {
|
||
router.back()
|
||
}
|
||
|
||
// 提交
|
||
const handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
|
||
try {
|
||
// 验证表单
|
||
await formRef.value.validate()
|
||
|
||
// 验证任务类型
|
||
const validTaskTypes = formData.taskTypes.filter(item => item.taskTypeId && item.quantity > 0)
|
||
if (validTaskTypes.length === 0) {
|
||
message.error('请至少添加一个任务类型')
|
||
return
|
||
}
|
||
|
||
// 检查库存(如果需要)
|
||
if (selectedSubTypeNeedsInventoryCheck.value) {
|
||
if (!formData.productBarcodes || formData.productBarcodes.length === 0) {
|
||
message.error('请至少添加一个已验证库存的商品条码')
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查积分余额(管理员跳过)
|
||
if (!isAdmin.value && estimatedScore.value > teamScore.value) {
|
||
message.error('团队积分余额不足')
|
||
return
|
||
}
|
||
|
||
// 检查指定接收人(指定模式下,允许部分任务类型不指定设计师,由接单部门负责人分配)
|
||
// 不再强制所有任务类型都必须选择接收人
|
||
|
||
submitting.value = true
|
||
|
||
// 检查是否有正在上传的文件
|
||
const uploadingFiles = fileList.value.filter(file =>
|
||
file.status === 'uploading' || (file.status !== 'done' && file.status !== 'error')
|
||
)
|
||
|
||
if (uploadingFiles.length > 0) {
|
||
console.warn('[提交任务] 检测到正在上传的文件,等待上传完成...')
|
||
message.warning('文件正在上传中,请等待上传完成后再提交任务')
|
||
submitting.value = false
|
||
return
|
||
}
|
||
|
||
// 构建附件URL(如果有上传的文件)- 使用S3返回的fileUrl
|
||
console.log('[提交任务] 文件列表:', fileList.value)
|
||
console.log('[提交任务] 文件列表详情:', fileList.value.map(f => ({
|
||
name: f.name,
|
||
status: f.status,
|
||
url: f.url,
|
||
response: f.response
|
||
})))
|
||
|
||
const attachmentUrls = fileList.value
|
||
.filter(file => {
|
||
const hasUrl = file.status === 'done' && (
|
||
file.response?.fileUrl ||
|
||
file.response?.data?.[0] ||
|
||
file.url
|
||
)
|
||
console.log(`[提交任务] 文件 ${file.name}: status=${file.status}, hasUrl=${!!hasUrl}`)
|
||
return hasUrl
|
||
})
|
||
.map(file => {
|
||
// 尝试从多个可能的字段获取URL
|
||
const url = file.response?.fileUrl ||
|
||
file.response?.data?.[0] ||
|
||
file.url ||
|
||
''
|
||
console.log(`[提交任务] 文件 ${file.name} 的URL:`, url)
|
||
return url
|
||
})
|
||
.filter(url => url)
|
||
|
||
console.log('[提交任务] 提取的附件URLs:', attachmentUrls)
|
||
const attachmentUrl = attachmentUrls.length > 0 ? attachmentUrls.join(',') : formData.attachmentUrl
|
||
console.log('[提交任务] 最终attachmentUrl:', attachmentUrl)
|
||
|
||
// 流水线任务(固定使用顺序流水线模式)
|
||
const pipelineData = {
|
||
title: formData.title,
|
||
isPipeline: true,
|
||
pipelineMode: 'sequential', // 固定为顺序流水线
|
||
items: validTaskTypes.map((item, index) => ({
|
||
taskTypeId: item.taskTypeId!,
|
||
quantity: item.quantity,
|
||
sortOrder: index + 1,
|
||
receiverId: formData.releaseType === 'assigned' ? item.receiverId : undefined,
|
||
selectedDeptId: item.selectedDeptId // 用户选择的接单部门
|
||
})),
|
||
brandId: formData.brandId,
|
||
platformId: formData.platformId,
|
||
categoryId: formData.categoryId,
|
||
spuInfo: formData.spuInfo,
|
||
taskSubType: formData.taskSubType,
|
||
attachmentUrl: attachmentUrl,
|
||
remark: formData.remark || '',
|
||
priority: formData.isUrgent ? 3 : 1, // 0-低 1-普通 2-高 3-紧急
|
||
releaseType: formData.releaseType, // 发布类型
|
||
receiverId: undefined, // 主任务不设接收人,由子任务指定
|
||
productBarcodes: formData.productBarcodes, // 商品条码列表
|
||
previewImage: formData.previewImage // 预览图URL
|
||
}
|
||
|
||
let response: any
|
||
|
||
if (editMode.value && editTaskId.value) {
|
||
// 编辑模式:更新任务并重新提交审核
|
||
const updateData = {
|
||
...pipelineData,
|
||
id: editTaskId.value
|
||
}
|
||
response = await taskApi.updateTask(editTaskId.value, updateData)
|
||
|
||
if (response.code === 200) {
|
||
message.success('任务已重新提交审核')
|
||
setTimeout(() => {
|
||
router.push('/tasks')
|
||
}, 1000)
|
||
}
|
||
} else {
|
||
// 新建模式
|
||
response = await taskApi.createPipelineTask(pipelineData)
|
||
|
||
if (response.code === 200) {
|
||
message.success('流水线任务创建成功')
|
||
setTimeout(() => {
|
||
router.push('/tasks')
|
||
}, 1000)
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('发起任务失败:', error)
|
||
if (error.errorFields) {
|
||
// 表单验证错误
|
||
message.error('请检查表单填写')
|
||
} else {
|
||
message.error(error.message || '发起任务失败')
|
||
}
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// 加载配置数据(从全局store加载)
|
||
const loadConfigData = async () => {
|
||
loading.value = true
|
||
try {
|
||
await configStore.loadAllConfigs()
|
||
} catch (error) {
|
||
console.error('加载配置数据失败:', error)
|
||
message.error('加载配置数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载任务子类型
|
||
const loadTaskSubTypes = async () => {
|
||
try {
|
||
const response = await configApi.getEnabledTaskSubTypes()
|
||
taskSubTypes.value = response.data || []
|
||
// 设置默认选中第一个子类型
|
||
if (taskSubTypes.value.length > 0 && !formData.taskSubType) {
|
||
formData.taskSubType = taskSubTypes.value[0].name
|
||
}
|
||
} catch (error) {
|
||
console.error('加载任务子类型失败:', error)
|
||
message.error('加载任务子类型失败')
|
||
}
|
||
}
|
||
|
||
// 监听配置更新事件
|
||
const handleConfigUpdate = async (event: CustomEvent) => {
|
||
console.log('配置已更新,刷新配置数据:', event.detail)
|
||
await loadConfigData()
|
||
}
|
||
|
||
// 加载团队积分余额
|
||
const loadTeamScore = async () => {
|
||
console.log('[loadTeamScore] 开始加载积分')
|
||
console.log('[loadTeamScore] 当前用户信息:', authStore.userInfo)
|
||
|
||
// 确保用户信息是最新的
|
||
if (!authStore.userInfo || !authStore.userInfo.deptId) {
|
||
console.log('[loadTeamScore] 用户信息缺失或无部门ID,重新获取用户信息')
|
||
await authStore.fetchUserInfo()
|
||
console.log('[loadTeamScore] 重新获取后的用户信息:', authStore.userInfo)
|
||
}
|
||
|
||
const deptId = authStore.userInfo?.deptId
|
||
console.log('[loadTeamScore] 部门ID:', deptId)
|
||
|
||
if (!deptId) {
|
||
// 如果用户没有部门,不显示积分或者显示为0
|
||
console.warn('[loadTeamScore] 用户没有部门ID,设置积分为0')
|
||
teamScore.value = 0
|
||
return
|
||
}
|
||
|
||
try {
|
||
console.log('[loadTeamScore] 调用API获取积分池,部门ID:', deptId)
|
||
const response = await configApi.getScorePoolByDeptId(deptId)
|
||
console.log('[loadTeamScore] API响应:', response)
|
||
|
||
// request 拦截器返回的是 { code: 200, data: {...} } 格式
|
||
// 如果 code === 200,拦截器返回整个 res 对象
|
||
if (response && response.data) {
|
||
console.log('[loadTeamScore] 积分池数据:', response.data)
|
||
console.log('[loadTeamScore] 余额:', response.data.balance)
|
||
teamScore.value = response.data.balance || 0
|
||
} else {
|
||
console.warn('[loadTeamScore] 响应格式异常,设置积分为0')
|
||
teamScore.value = 0
|
||
}
|
||
console.log('[loadTeamScore] 最终积分值:', teamScore.value)
|
||
} catch (error) {
|
||
console.error('[loadTeamScore] 加载团队积分失败:', error)
|
||
teamScore.value = 0
|
||
}
|
||
}
|
||
|
||
// 文件上传前验证
|
||
const beforeUpload = (file: File) => {
|
||
console.log('[文件验证] 验证文件:', file.name, file.size, file.type)
|
||
|
||
const isValidType = ['.xls', '.xlsx', '.doc', '.docx', '.pdf', '.zip'].some(ext =>
|
||
file.name.toLowerCase().endsWith(ext)
|
||
)
|
||
if (!isValidType) {
|
||
console.error('[文件验证] 文件类型不支持:', file.name)
|
||
message.error('只支持 Excel、Word、PDF、ZIP 格式的文件')
|
||
return false
|
||
}
|
||
|
||
const isLt500M = file.size / 1024 / 1024 < 500
|
||
if (!isLt500M) {
|
||
console.error('[文件验证] 文件大小超限:', file.size)
|
||
message.error('文件大小不能超过 500MB')
|
||
return false
|
||
}
|
||
|
||
console.log('[文件验证] 文件验证通过,将使用custom-request上传')
|
||
// 返回 true 让组件继续处理,但使用 customRequest 自定义上传逻辑
|
||
return true
|
||
}
|
||
|
||
// 文件列表变化处理(监听文件添加,确保上传被触发)
|
||
const handleFileChange = (info: any) => {
|
||
console.log('[文件变化] onChange事件触发:', info)
|
||
const { file } = info
|
||
|
||
// 如果文件刚被添加且还没有开始上传(status 为 undefined 或 'ready')
|
||
if ((!file.status || file.status === 'ready') && file.originFileObj) {
|
||
console.log('[文件变化] 检测到新文件,文件状态:', file.status)
|
||
// customRequest 应该会自动调用,这里只是记录
|
||
}
|
||
}
|
||
|
||
// 自定义上传处理(使用S3上传)
|
||
const handleUpload = async (options: any) => {
|
||
const { file, onSuccess, onError, onProgress } = options
|
||
|
||
console.log('[文件上传] 开始上传文件:', file.name, file.size)
|
||
|
||
try {
|
||
uploadProgress.value = 0
|
||
|
||
// 使用S3上传接口
|
||
console.log('[文件上传] 调用 uploadFilesToS3 API')
|
||
const fileUrls = await uploadFilesToS3(
|
||
[file as File],
|
||
'task', // relatedType
|
||
undefined, // relatedId,创建任务时还没有ID
|
||
(progress) => {
|
||
uploadProgress.value = progress
|
||
if (onProgress) {
|
||
onProgress({ percent: progress })
|
||
}
|
||
}
|
||
)
|
||
|
||
console.log('[文件上传] 上传成功,返回URLs:', fileUrls)
|
||
|
||
if (fileUrls && fileUrls.length > 0) {
|
||
const fileUrl = fileUrls[0]
|
||
console.log('[文件上传] 准备更新文件列表,URL:', fileUrl)
|
||
|
||
// 更新文件列表
|
||
const fileIndex = fileList.value.findIndex(f => f.uid === file.uid)
|
||
if (fileIndex !== -1) {
|
||
const updatedFile = {
|
||
...fileList.value[fileIndex],
|
||
status: 'done',
|
||
response: { fileUrl: fileUrl, data: fileUrls },
|
||
url: fileUrl,
|
||
percent: 100
|
||
} as UploadFile
|
||
|
||
fileList.value[fileIndex] = updatedFile
|
||
console.log('[文件上传] 文件列表已更新:', fileList.value[fileIndex])
|
||
}
|
||
|
||
uploadProgress.value = 0
|
||
if (onSuccess) {
|
||
// onSuccess 的第一个参数是响应数据,第二个参数是文件对象
|
||
onSuccess({ fileUrl: fileUrl, data: fileUrls }, file)
|
||
}
|
||
message.success('文件上传成功')
|
||
} else {
|
||
throw new Error('上传失败:未返回文件URL')
|
||
}
|
||
} catch (error: any) {
|
||
uploadProgress.value = 0
|
||
const fileIndex = fileList.value.findIndex(f => f.uid === file.uid)
|
||
if (fileIndex !== -1) {
|
||
fileList.value[fileIndex] = {
|
||
...fileList.value[fileIndex],
|
||
status: 'error'
|
||
} as UploadFile
|
||
}
|
||
if (onError) {
|
||
onError(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除文件
|
||
const handleRemove = (file: UploadFile) => {
|
||
const index = fileList.value.findIndex(f => f.uid === file.uid)
|
||
if (index !== -1) {
|
||
fileList.value.splice(index, 1)
|
||
}
|
||
return true
|
||
}
|
||
|
||
// 预览图上传前验证
|
||
const beforePreviewUpload = (file: File) => {
|
||
const isImage = file.type.startsWith('image/')
|
||
if (!isImage) {
|
||
message.error('只能上传图片文件')
|
||
return false
|
||
}
|
||
const isLt10M = file.size / 1024 / 1024 < 10
|
||
if (!isLt10M) {
|
||
message.error('图片大小不能超过 10MB')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// 预览图上传处理
|
||
const handlePreviewUpload = async (options: any) => {
|
||
const { file, onSuccess, onError } = options
|
||
|
||
try {
|
||
previewUploading.value = true
|
||
|
||
const formDataObj = new FormData()
|
||
formDataObj.append('file', file)
|
||
|
||
const response = await fetch('/api/file/upload-preview', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||
},
|
||
body: formDataObj
|
||
})
|
||
|
||
const result = await response.json()
|
||
|
||
if (result.code === 200 && result.data) {
|
||
formData.previewImage = result.data
|
||
previewFileList.value = [{
|
||
uid: file.uid,
|
||
name: file.name,
|
||
status: 'done',
|
||
url: result.data
|
||
} as UploadFile]
|
||
if (onSuccess) {
|
||
onSuccess(result.data)
|
||
}
|
||
message.success('预览图上传成功')
|
||
} else {
|
||
throw new Error(result.msg || '上传失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('预览图上传失败:', error)
|
||
if (onError) {
|
||
onError(error)
|
||
}
|
||
message.error('预览图上传失败: ' + error.message)
|
||
} finally {
|
||
previewUploading.value = false
|
||
}
|
||
}
|
||
|
||
// 删除预览图
|
||
const handlePreviewRemove = () => {
|
||
formData.previewImage = ''
|
||
previewFileList.value = []
|
||
return true
|
||
}
|
||
|
||
// 处理粘贴图片
|
||
const handlePasteImage = async (e: ClipboardEvent) => {
|
||
e.preventDefault()
|
||
|
||
// 如果已经有预览图,不处理粘贴
|
||
if (formData.previewImage) {
|
||
return
|
||
}
|
||
|
||
const items = e.clipboardData?.items
|
||
if (!items) return
|
||
|
||
// 查找图片数据
|
||
for (let i = 0; i < items.length; i++) {
|
||
const item = items[i]
|
||
if (item.type.indexOf('image') !== -1) {
|
||
const file = item.getAsFile()
|
||
if (!file) continue
|
||
|
||
// 验证文件大小
|
||
const isLt10M = file.size / 1024 / 1024 < 10
|
||
if (!isLt10M) {
|
||
message.error('图片大小不能超过 10MB')
|
||
return
|
||
}
|
||
|
||
// 创建 File 对象用于上传
|
||
const uploadFile: UploadFile = {
|
||
uid: `paste-${Date.now()}`,
|
||
name: `粘贴图片-${Date.now()}.png`,
|
||
status: 'uploading',
|
||
originFileObj: file
|
||
}
|
||
|
||
previewFileList.value = [uploadFile]
|
||
|
||
// 调用上传处理函数
|
||
try {
|
||
previewUploading.value = true
|
||
const formDataObj = new FormData()
|
||
formDataObj.append('file', file)
|
||
|
||
const response = await fetch('/api/file/upload-preview', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||
},
|
||
body: formDataObj
|
||
})
|
||
|
||
const result = await response.json()
|
||
|
||
if (result.code === 200 && result.data) {
|
||
formData.previewImage = result.data
|
||
previewFileList.value = [{
|
||
uid: uploadFile.uid,
|
||
name: uploadFile.name,
|
||
status: 'done',
|
||
url: result.data
|
||
} as UploadFile]
|
||
message.success('图片粘贴成功')
|
||
} else {
|
||
throw new Error(result.msg || '上传失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('粘贴图片上传失败:', error)
|
||
previewFileList.value = []
|
||
message.error('粘贴图片上传失败: ' + error.message)
|
||
} finally {
|
||
previewUploading.value = false
|
||
}
|
||
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载任务数据用于编辑
|
||
const loadTaskForEdit = async (taskId: number) => {
|
||
try {
|
||
loading.value = true
|
||
const response = await taskApi.getTaskById(taskId)
|
||
const task = response.data
|
||
|
||
if (task) {
|
||
console.log('加载任务数据:', task)
|
||
|
||
// 填充基本表单数据
|
||
formData.title = task.title || ''
|
||
formData.taskSubType = task.taskSubType || ''
|
||
formData.brandId = task.brandId
|
||
formData.platformId = task.platformId
|
||
formData.categoryId = task.categoryId
|
||
formData.spuInfo = task.spuInfo || ''
|
||
formData.remark = task.remark || ''
|
||
formData.releaseType = task.releaseType || 'recruitment'
|
||
formData.isUrgent = task.isUrgent === 1 || task.isUrgent === true
|
||
|
||
// 处理预览图
|
||
if (task.previewImage) {
|
||
formData.previewImage = task.previewImage
|
||
previewFileList.value = [{
|
||
uid: 'existing-preview',
|
||
name: '预览图',
|
||
status: 'done' as const,
|
||
url: task.previewImage
|
||
}]
|
||
}
|
||
|
||
// 处理任务类型和数量 - 优先从子任务的typeDetails中获取
|
||
if (task.isPipeline === 1 && task.subTasks && task.subTasks.length > 0) {
|
||
// 流水线任务:从子任务的typeDetails中提取任务类型信息
|
||
console.log('流水线任务子任务:', task.subTasks)
|
||
const taskTypeItems: TaskTypeItem[] = []
|
||
|
||
for (const subTask of task.subTasks) {
|
||
// 优先从typeDetails解析(包含完整的任务类型明细)
|
||
if (subTask.typeDetails) {
|
||
try {
|
||
const details = typeof subTask.typeDetails === 'string'
|
||
? JSON.parse(subTask.typeDetails)
|
||
: subTask.typeDetails
|
||
|
||
for (const detail of details) {
|
||
taskTypeItems.push({
|
||
taskTypeId: detail.typeId,
|
||
quantity: detail.quantity || 1,
|
||
receiverId: detail.receiverId || subTask.receiverId,
|
||
selectedDeptId: undefined // 不预设部门,让用户重新选择或自动匹配
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析typeDetails失败:', e)
|
||
// 解析失败时使用子任务的基本信息
|
||
taskTypeItems.push({
|
||
taskTypeId: subTask.taskTypeId,
|
||
quantity: subTask.quantity || 1,
|
||
receiverId: subTask.receiverId,
|
||
selectedDeptId: undefined
|
||
})
|
||
}
|
||
} else {
|
||
// 没有typeDetails时使用子任务的基本信息
|
||
taskTypeItems.push({
|
||
taskTypeId: subTask.taskTypeId,
|
||
quantity: subTask.quantity || 1,
|
||
receiverId: subTask.receiverId,
|
||
selectedDeptId: undefined
|
||
})
|
||
}
|
||
}
|
||
|
||
formData.taskTypes = taskTypeItems
|
||
console.log('解析后的任务类型:', formData.taskTypes)
|
||
} else if (task.taskTypeIds) {
|
||
// 非流水线任务:从taskTypeIds字符串解析
|
||
const typeIds = task.taskTypeIds.split(',').map((id: string) => parseInt(id.trim()))
|
||
formData.taskTypes = typeIds.map((typeId: number, index: number) => ({
|
||
taskTypeId: typeId,
|
||
quantity: index === 0 ? (task.quantity || 1) : 1,
|
||
receiverId: task.receiverId
|
||
}))
|
||
} else if (task.taskTypeId) {
|
||
// 兼容单任务类型
|
||
formData.taskTypes = [{
|
||
taskTypeId: task.taskTypeId,
|
||
quantity: task.quantity || 1,
|
||
receiverId: task.receiverId
|
||
}]
|
||
}
|
||
|
||
// 为每个任务类型设置级联路径(需要在层级数据加载后执行)
|
||
// 使用 nextTick 确保层级数据已加载
|
||
nextTick(() => {
|
||
for (const item of formData.taskTypes) {
|
||
if (item.taskTypeId) {
|
||
const path = findTaskTypePath(item.taskTypeId)
|
||
if (path) {
|
||
item.taskTypePath = path
|
||
console.log(`任务类型 ${item.taskTypeId} 的路径:`, path)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
// 处理附件 - 完整加载原有附件
|
||
if (task.attachmentUrl) {
|
||
formData.attachmentUrl = task.attachmentUrl
|
||
// 将附件URL转换为fileList格式
|
||
const urls = task.attachmentUrl.split(',').filter((u: string) => u.trim())
|
||
fileList.value = urls.map((url: string, index: number) => {
|
||
const fileName = url.split('/').pop() || `附件${index + 1}`
|
||
return {
|
||
uid: `existing-${index}`,
|
||
name: fileName,
|
||
status: 'done' as const,
|
||
url: url.trim()
|
||
}
|
||
})
|
||
}
|
||
|
||
editMode.value = true
|
||
editTaskId.value = taskId
|
||
|
||
// 显示驳回原因提示(如果是被驳回的任务)
|
||
if (task.rejectReason) {
|
||
message.warning(`驳回原因:${task.rejectReason},请修改后重新提交`)
|
||
}
|
||
|
||
console.log('任务数据加载成功,formData.taskTypes:', formData.taskTypes)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载任务数据失败:', error)
|
||
message.error('加载任务数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载发布类型配置
|
||
const loadReleaseTypeConfig = async () => {
|
||
try {
|
||
const res = await getSystemConfigsMapByGroup('task')
|
||
if (res.data) {
|
||
releaseTypeConfig.value = {
|
||
default: res.data['task.release_type.default'] !== 'false',
|
||
recruitment: res.data['task.release_type.recruitment'] !== 'false',
|
||
assigned: res.data['task.release_type.assigned'] !== 'false'
|
||
}
|
||
// 如果当前选择的类型被禁用,自动切换到第一个可用的类型
|
||
if (!releaseTypeConfig.value[formData.releaseType as keyof typeof releaseTypeConfig.value]) {
|
||
if (releaseTypeConfig.value.default) {
|
||
formData.releaseType = 'default'
|
||
} else if (releaseTypeConfig.value.recruitment) {
|
||
formData.releaseType = 'recruitment'
|
||
} else if (releaseTypeConfig.value.assigned) {
|
||
formData.releaseType = 'assigned'
|
||
}
|
||
}
|
||
}
|
||
// 加载工作时间配置
|
||
try {
|
||
const workHoursResponse = await getWorkHoursConfig()
|
||
if (workHoursResponse.data && workHoursResponse.data.workHoursPerDay) {
|
||
workHoursPerDay.value = workHoursResponse.data.workHoursPerDay
|
||
}
|
||
} catch (error) {
|
||
console.error('加载工作时间配置失败:', error)
|
||
// 使用默认值
|
||
}
|
||
} catch (error) {
|
||
console.error('加载发布类型配置失败:', error)
|
||
}
|
||
}
|
||
|
||
// 全局粘贴事件处理(当预览图区域可见时)
|
||
const handleGlobalPaste = (e: ClipboardEvent) => {
|
||
// 如果预览图区域可见且没有图片,处理粘贴
|
||
if (!formData.previewImage) {
|
||
handlePasteImage(e)
|
||
}
|
||
}
|
||
|
||
// 组件挂载
|
||
onMounted(async () => {
|
||
// 添加全局粘贴事件监听
|
||
document.addEventListener('paste', handleGlobalPaste as EventListener)
|
||
|
||
// 先刷新用户信息,确保部门信息最新
|
||
try {
|
||
await authStore.fetchUserInfo()
|
||
console.log('用户信息已刷新:', authStore.userInfo)
|
||
} catch (error) {
|
||
console.error('刷新用户信息失败:', error)
|
||
}
|
||
|
||
// 然后加载配置和积分
|
||
await Promise.all([
|
||
loadConfigData(),
|
||
loadTeamScore(),
|
||
loadTaskSubTypes(),
|
||
loadReleaseTypeConfig(),
|
||
loadHierarchicalTaskTypes(),
|
||
loadDepartmentList()
|
||
])
|
||
|
||
// 检查是否是编辑模式
|
||
const editId = route.query.editId
|
||
if (editId) {
|
||
await loadTaskForEdit(Number(editId))
|
||
}
|
||
|
||
// 监听配置更新事件
|
||
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
|
||
})
|
||
|
||
// 组件卸载
|
||
onUnmounted(() => {
|
||
window.removeEventListener('configUpdated', handleConfigUpdate as EventListener)
|
||
document.removeEventListener('paste', handleGlobalPaste as EventListener)
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import "@/styles/variables.scss";
|
||
|
||
.create-task-page {
|
||
min-height: 100vh;
|
||
padding: $space-xl 0;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
// 积分余额卡片 - Cursor 风格
|
||
.score-balance-card {
|
||
margin-bottom: $space-xl;
|
||
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;
|
||
}
|
||
}
|
||
|
||
.balance-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
.balance-label {
|
||
font-size: $font-size-base;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.balance-value {
|
||
font-size: $font-size-3xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.balance-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
padding: $space-sm $space-md;
|
||
background: rgba($brand-primary, 0.1);
|
||
border-radius: $radius-small;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
|
||
.tip-icon {
|
||
font-size: $font-size-lg;
|
||
}
|
||
|
||
strong {
|
||
color: $brand-primary;
|
||
}
|
||
}
|
||
|
||
.time-estimate {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: $space-md;
|
||
margin-top: $space-md;
|
||
padding: $space-md;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: $radius-small;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
|
||
.estimate-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-xs;
|
||
|
||
.estimate-label {
|
||
color: $text-tertiary;
|
||
font-size: $font-size-sm;
|
||
}
|
||
|
||
.estimate-value {
|
||
color: $brand-primary;
|
||
font-weight: 600;
|
||
font-size: $font-size-base;
|
||
}
|
||
}
|
||
|
||
.estimate-tip {
|
||
flex-basis: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: $font-size-xs;
|
||
color: $text-tertiary;
|
||
margin-top: $space-xs;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
}
|
||
|
||
// 任务表单 - Cursor 风格
|
||
.task-form {
|
||
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;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
padding: $space-lg;
|
||
}
|
||
}
|
||
|
||
// 上传区域
|
||
.upload-area {
|
||
.upload-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
margin-top: $space-sm;
|
||
padding: $space-sm $space-md;
|
||
background: rgba($color-info, 0.1);
|
||
border-radius: $radius-small;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
|
||
.tip-icon {
|
||
font-size: $font-size-base;
|
||
}
|
||
}
|
||
|
||
// 上传文件列表样式
|
||
:deep(.ant-upload-list) {
|
||
color: $text-primary;
|
||
margin-top: 12px;
|
||
|
||
.ant-upload-list-item {
|
||
color: $text-primary;
|
||
border-color: rgba(255, 255, 255, 0.15);
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
margin-top: 8px;
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border-color: rgba(255, 255, 255, 0.25);
|
||
}
|
||
|
||
.ant-upload-list-item-name {
|
||
color: $text-primary;
|
||
flex: 1;
|
||
|
||
&:hover {
|
||
color: $brand-primary;
|
||
}
|
||
}
|
||
|
||
// 删除按钮样式增强
|
||
.ant-upload-list-item-card-actions,
|
||
.ant-upload-list-item-actions {
|
||
display: flex !important;
|
||
align-items: center;
|
||
|
||
.anticon,
|
||
.ant-btn {
|
||
color: #ff6b6b !important;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
color: #ff4d4f !important;
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
|
||
// 删除图标按钮
|
||
.anticon-delete,
|
||
.ant-upload-list-item-card-actions-btn {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 77, 79, 0.1);
|
||
|
||
&:hover {
|
||
background: rgba(255, 77, 79, 0.2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-upload-list-item-error {
|
||
border-color: rgba(255, 77, 79, 0.3);
|
||
|
||
.ant-upload-list-item-name {
|
||
color: $color-error;
|
||
}
|
||
}
|
||
|
||
// 上传完成状态的文件项
|
||
.ant-upload-list-item-done {
|
||
border-color: rgba(49, 240, 139, 0.2);
|
||
|
||
.ant-upload-list-item-name {
|
||
color: $brand-primary;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发布类型选项
|
||
// 任务发布类型按钮组
|
||
.release-type-buttons {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.release-type-button {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
width: 100%;
|
||
padding: 14px 16px;
|
||
background: $bg-secondary;
|
||
border: 2px solid $border-primary;
|
||
border-radius: $radius-md;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
text-align: left;
|
||
|
||
&:hover {
|
||
background: $bg-tertiary;
|
||
border-color: rgba($brand-primary, 0.4);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
&.active {
|
||
background: rgba($brand-primary, 0.1);
|
||
border-color: $brand-primary;
|
||
|
||
.button-icon {
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.15);
|
||
}
|
||
|
||
.button-title {
|
||
color: $brand-primary;
|
||
}
|
||
}
|
||
|
||
.button-icon {
|
||
flex-shrink: 0;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: $bg-tertiary;
|
||
border-radius: $radius-sm;
|
||
color: $text-secondary;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.button-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.button-title {
|
||
font-size: 14px;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin-bottom: 4px;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.button-desc {
|
||
font-size: 13px;
|
||
color: $text-secondary;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.selected-indicator {
|
||
flex-shrink: 0;
|
||
color: $brand-primary;
|
||
animation: scaleIn 0.3s ease;
|
||
}
|
||
}
|
||
|
||
@keyframes scaleIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.5);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.urgent-tip {
|
||
margin-left: $space-md;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
// 任务类型列表样式
|
||
.task-types-list {
|
||
.task-type-item {
|
||
margin-bottom: $space-md;
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid transparent;
|
||
border-radius: 8px;
|
||
transition: all 0.2s ease;
|
||
cursor: grab;
|
||
|
||
&:last-child {
|
||
margin-bottom: $space-lg;
|
||
}
|
||
|
||
&:hover {
|
||
background: rgba(49, 240, 139, 0.05);
|
||
border-color: rgba(49, 240, 139, 0.2);
|
||
}
|
||
|
||
&.dragging {
|
||
opacity: 0.5;
|
||
cursor: grabbing;
|
||
border-color: $brand-primary;
|
||
background: rgba(49, 240, 139, 0.1);
|
||
}
|
||
|
||
&.drag-over {
|
||
border-color: $brand-primary;
|
||
border-style: dashed;
|
||
background: rgba(49, 240, 139, 0.15);
|
||
transform: scale(1.02);
|
||
}
|
||
}
|
||
|
||
.drag-handle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 40px;
|
||
color: #666;
|
||
cursor: grab;
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
flex-shrink: 0;
|
||
|
||
&:hover {
|
||
color: $brand-primary;
|
||
background: rgba(49, 240, 139, 0.1);
|
||
}
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
|
||
.task-type-row {
|
||
display: flex;
|
||
gap: $space-md;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.task-type-select {
|
||
flex: 2;
|
||
}
|
||
|
||
.task-type-cascader {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
:deep(.ant-cascader) {
|
||
width: 100%;
|
||
}
|
||
|
||
.task-type-score-tip {
|
||
display: flex;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
|
||
.score-badge {
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.1);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.hour-badge {
|
||
color: $text-secondary;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.task-type-quantity {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.task-type-dept {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.task-type-actions {
|
||
min-width: 90px;
|
||
}
|
||
|
||
.add-task-type-btn {
|
||
border-style: dashed;
|
||
border-color: $border-primary;
|
||
color: $text-secondary;
|
||
background: transparent;
|
||
margin-top: $space-md;
|
||
|
||
&:hover {
|
||
border-color: #31f08b;
|
||
color: #31f08b;
|
||
background: rgba(49, 240, 139, 0.05);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 表单标签和文字颜色统一
|
||
:deep(.ant-form-item-label > label) {
|
||
color: $text-primary;
|
||
}
|
||
|
||
:deep(.ant-form-item-required::before) {
|
||
color: $brand-primary;
|
||
}
|
||
|
||
:deep(.ant-input),
|
||
:deep(.ant-select-selector),
|
||
:deep(.ant-input-number),
|
||
:deep(.ant-input-password),
|
||
:deep(.ant-picker),
|
||
:deep(.ant-radio-button-wrapper),
|
||
:deep(.ant-switch) {
|
||
color: $text-primary;
|
||
}
|
||
|
||
// 确保开关选中状态的文字是黑色
|
||
:deep(.ant-switch.ant-switch-checked) {
|
||
.ant-switch-inner {
|
||
color: #000000 !important;
|
||
}
|
||
|
||
.ant-switch-handle::before {
|
||
background: #000000 !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-input::placeholder),
|
||
:deep(.ant-select-selection-placeholder),
|
||
:deep(.ant-picker-input input::placeholder) {
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
// 表单操作
|
||
.form-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: $space-md;
|
||
margin-top: $space-xxl;
|
||
padding-top: $space-xl;
|
||
border-top: 1px solid $border-primary;
|
||
}
|
||
|
||
// 流水线任务样式
|
||
.pipeline-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.pipeline-mode-select {
|
||
:deep(.ant-radio-button-wrapper) {
|
||
height: auto;
|
||
padding: 16px 20px;
|
||
line-height: 1.4;
|
||
border-color: rgba($brand-primary, 0.2);
|
||
|
||
&:hover {
|
||
color: $brand-primary;
|
||
border-color: $brand-primary;
|
||
}
|
||
|
||
&.ant-radio-button-wrapper-checked {
|
||
background: rgba($brand-primary, 0.1);
|
||
border-color: $brand-primary;
|
||
color: $brand-primary;
|
||
|
||
&:hover {
|
||
background: rgba($brand-primary, 0.15);
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-radio-group) {
|
||
display: flex;
|
||
gap: 12px;
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.ant-radio-button-wrapper) {
|
||
flex: 1;
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
// 流水线分组预览
|
||
.pipeline-preview {
|
||
margin-top: 20px;
|
||
padding: 16px;
|
||
background: rgba($brand-primary, 0.03);
|
||
border: 1px solid rgba($brand-primary, 0.15);
|
||
border-radius: $radius-md;
|
||
|
||
.preview-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
font-size: 14px;
|
||
font-weight: $font-weight-semibold;
|
||
color: $brand-primary;
|
||
|
||
.preview-mode {
|
||
margin-left: 8px;
|
||
font-size: 13px;
|
||
color: $text-secondary;
|
||
font-weight: normal;
|
||
}
|
||
}
|
||
|
||
.groups-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.group-card {
|
||
background: $bg-secondary;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-sm;
|
||
padding: 12px;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
border-color: rgba($brand-primary, 0.3);
|
||
box-shadow: 0 2px 8px rgba($brand-primary, 0.1);
|
||
}
|
||
|
||
.group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid $border-primary;
|
||
|
||
.group-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.group-badge {
|
||
padding: 2px 8px;
|
||
background: rgba($brand-primary, 0.15);
|
||
color: $brand-primary;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
|
||
.group-dept {
|
||
font-size: 14px;
|
||
font-weight: $font-weight-medium;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.group-count {
|
||
font-size: 12px;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.group-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
|
||
.summary-item {
|
||
color: $brand-primary;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.summary-divider {
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.group-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
.group-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 8px;
|
||
background: rgba($brand-primary, 0.03);
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
|
||
.item-icon {
|
||
color: $text-tertiary;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.item-name {
|
||
flex: 1;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.item-quantity {
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.item-score {
|
||
color: $brand-primary;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.preview-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
background: rgba($brand-primary, 0.08);
|
||
border-radius: $radius-sm;
|
||
font-size: 14px;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
|
||
.footer-score {
|
||
color: $brand-primary;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.footer-hours,
|
||
.footer-groups {
|
||
color: $text-primary;
|
||
}
|
||
|
||
.footer-divider {
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 任务标题自动生成提示
|
||
.title-auto-tip {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
// 自动生成的标题输入框样式
|
||
.auto-generated-title {
|
||
:deep(.ant-input-disabled) {
|
||
background: rgba(49, 240, 139, 0.05) !important;
|
||
border-color: rgba(49, 240, 139, 0.3) !important;
|
||
color: $text-primary !important;
|
||
cursor: default;
|
||
|
||
&::placeholder {
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 预览图上传区域
|
||
.preview-upload-area {
|
||
.upload-button {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: $text-tertiary;
|
||
|
||
.upload-text {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.paste-hint {
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: #666;
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
// 当区域获得焦点时显示提示
|
||
&:focus-within {
|
||
.paste-hint {
|
||
opacity: 1;
|
||
color: $brand-primary;
|
||
}
|
||
}
|
||
|
||
.preview-tip {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.tip-icon {
|
||
margin-right: 6px;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-upload-list-picture-card) {
|
||
.ant-upload-list-item {
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
background: rgba(255, 255, 255, 0.02);
|
||
|
||
&:hover {
|
||
border-color: $brand-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-upload-select-picture-card) {
|
||
width: 104px;
|
||
height: 104px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||
border-radius: 8px;
|
||
|
||
&:hover {
|
||
border-color: $brand-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 条码列表样式
|
||
.barcode-list {
|
||
padding: 12px;
|
||
background: rgba(49, 240, 139, 0.05);
|
||
border: 1px solid rgba(49, 240, 139, 0.2);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
|
||
.barcode-list-title {
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.barcode-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
|
||
:deep(.ant-tag) {
|
||
margin: 0;
|
||
padding: 4px 10px;
|
||
font-size: 13px;
|
||
border-radius: 4px;
|
||
background: rgba(49, 240, 139, 0.15);
|
||
border-color: rgba(49, 240, 139, 0.3);
|
||
color: #31f08b;
|
||
|
||
.anticon-close {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
|
||
&:hover {
|
||
color: #ff4d4f;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 库存检查结果样式
|
||
.inventory-result {
|
||
margin-top: 12px;
|
||
|
||
.inventory-details {
|
||
margin-top: 12px;
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
margin-bottom: 8px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.label {
|
||
color: $text-tertiary;
|
||
width: 100px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.value {
|
||
color: $text-primary;
|
||
|
||
&.low-stock {
|
||
color: #ff4d4f;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|
||
|
||
<style lang="scss">
|
||
@import '@/styles/variables.scss';
|
||
|
||
// 自定义级联选择器下拉弹窗样式(挂载在body下,需全局覆盖)
|
||
.task-type-cascader-popup {
|
||
background-color: #1f1f1f !important; // 强制深色背景
|
||
border: 1px solid #303030 !important;
|
||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6) !important;
|
||
border-radius: 8px !important;
|
||
padding: 4px !important;
|
||
|
||
.ant-cascader-menu {
|
||
background: transparent !important;
|
||
border-right: 1px solid #303030 !important;
|
||
height: 280px !important; // 增加高度以便容纳更多选项
|
||
|
||
// 自定义滚动条样式
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 3px;
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.25);
|
||
}
|
||
}
|
||
|
||
&:last-child {
|
||
border-right: none !important;
|
||
}
|
||
|
||
.ant-cascader-menu-item {
|
||
color: #a0a0a0; // 普通文字颜色
|
||
padding: 10px 16px;
|
||
margin: 2px 4px;
|
||
border-radius: 6px;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||
color: #e5e5e5;
|
||
}
|
||
|
||
// 激活/选中状态
|
||
&-active {
|
||
background-color: rgba(49, 240, 139, 0.15) !important; // 品牌色背景
|
||
color: #31f08b !important; // 品牌色文字
|
||
font-weight: 600;
|
||
|
||
// 覆盖箭头颜色
|
||
.ant-cascader-menu-item-expand-icon {
|
||
color: #31f08b !important;
|
||
}
|
||
}
|
||
|
||
// 展开图标
|
||
&-expand-icon {
|
||
color: #555;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 搜索结果高亮
|
||
.ant-cascader-menu-item-keyword {
|
||
color: #31f08b !important;
|
||
}
|
||
}
|
||
|
||
// 设计师选择器选项样式
|
||
.designer-option {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
.designer-name {
|
||
color: #e5e5e5;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.designer-workload {
|
||
color: #8a8a8a;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style>
|