2244 lines
64 KiB
Vue
2244 lines
64 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>
|
||
|
||
<!-- 任务表单 -->
|
||
<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="index"
|
||
class="task-type-item"
|
||
>
|
||
<div class="task-type-row">
|
||
<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="100"
|
||
placeholder="数量"
|
||
size="large"
|
||
style="width: 100%"
|
||
@change="calculateScore"
|
||
/>
|
||
</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="filterOption"
|
||
@focus="loadDesignersForItem(item)"
|
||
>
|
||
<a-select-option
|
||
v-for="designer in getDesignersForItem(item)"
|
||
:key="designer.id"
|
||
:value="designer.id"
|
||
>
|
||
{{ designer.realName }} (进行中: {{ designer.inProgressCount || 0 }})
|
||
</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">{{ group.totalScore }}分</span>
|
||
<span class="summary-divider">|</span>
|
||
<span class="summary-item">{{ 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">= {{ 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">{{ 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="商品条码"
|
||
name="productBarcode"
|
||
required
|
||
>
|
||
<a-input-group compact>
|
||
<a-input
|
||
v-model:value="formData.productBarcode"
|
||
placeholder="请输入商品条码用于库存检查"
|
||
size="large"
|
||
style="width: calc(100% - 100px)"
|
||
@pressEnter="handleCheckInventory"
|
||
/>
|
||
<a-button
|
||
type="primary"
|
||
size="large"
|
||
@click="handleCheckInventory"
|
||
:loading="checkingInventory"
|
||
>
|
||
检查库存
|
||
</a-button>
|
||
</a-input-group>
|
||
|
||
<!-- 库存检查结果 -->
|
||
<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" 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" :class="{ 'low-stock': inventoryCheckResult.availableQty <= 1 }">
|
||
{{ 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
|
||
:filter-option="filterOption"
|
||
>
|
||
<a-select-option
|
||
v-for="brand in brands"
|
||
:key="brand.id"
|
||
:value="brand.id"
|
||
>
|
||
{{ 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"
|
||
>
|
||
<a-select-option
|
||
v-for="platform in platforms"
|
||
:key="platform.id"
|
||
:value="platform.id"
|
||
>
|
||
{{ 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
|
||
:filter-option="filterOption"
|
||
>
|
||
<a-select-option
|
||
v-for="category in categories"
|
||
:key="category.id"
|
||
:value="category.id"
|
||
>
|
||
{{ category.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
|
||
<!-- SPU信息 -->
|
||
<a-form-item label="SPU信息" name="spuInfo">
|
||
<a-textarea
|
||
v-model:value="formData.spuInfo"
|
||
placeholder="请输入SPU信息"
|
||
:rows="2"
|
||
size="large"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<!-- 预览图上传 -->
|
||
<a-form-item label="预览图" name="previewImage">
|
||
<div class="preview-upload-area">
|
||
<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>
|
||
</a-upload>
|
||
<div class="preview-tip">
|
||
<span class="tip-icon">
|
||
<Icon name="image" size="sm" color="#31f08b" />
|
||
</span>
|
||
<span>建议上传正方形图片,系统将自动压缩处理,显示在任务卡片上</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,单文件最大50MB,最多上传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 { getSystemConfigsMapByGroup } 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
|
||
}
|
||
|
||
// 级联选择器选项接口
|
||
interface CascaderOption {
|
||
value: number
|
||
label: string
|
||
children?: CascaderOption[]
|
||
unitScore?: number
|
||
estimatedHours?: number
|
||
}
|
||
|
||
// 设计师列表缓存 { deptId: designers[] }
|
||
const designersCache = reactive<Record<number, any[]>>({})
|
||
|
||
// 表单数据
|
||
const formData = reactive({
|
||
title: '',
|
||
isPipeline: true, // 固定为流水线任务
|
||
pipelineMode: 'sequential' as 'sequential' | 'parallel', // 固定为顺序流水线
|
||
taskTypes: [{ taskTypeId: undefined, taskTypePath: undefined, quantity: 1, receiverId: undefined }] as TaskTypeItem[], // 任务类型数组
|
||
taskSubType: '', // 动态加载,初始为空
|
||
brandId: undefined,
|
||
platformId: undefined,
|
||
categoryId: undefined,
|
||
spuInfo: '',
|
||
attachmentUrl: '',
|
||
previewImage: '', // 预览图URL
|
||
remark: '', // 备注信息
|
||
releaseType: 'default',
|
||
productBarcode: '', // 商品条码(库存检查用)
|
||
// receiverId: undefined, // 已移除,改用子任务指定
|
||
isUrgent: false
|
||
})
|
||
|
||
// 库存检查相关
|
||
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 handleCheckInventory = async () => {
|
||
if (!formData.productBarcode) {
|
||
message.warning('请输入商品条码')
|
||
return
|
||
}
|
||
|
||
checkingInventory.value = true
|
||
inventoryCheckResult.value = null
|
||
|
||
try {
|
||
const response = await checkInventoryForTask(formData.productBarcode)
|
||
inventoryCheckResult.value = response.data
|
||
|
||
if (response.data.canPublish) {
|
||
message.success('库存检查通过,可以发布任务')
|
||
} else {
|
||
message.error(response.data.message || '库存不足,请先申请样品')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('检查库存失败:', error)
|
||
message.error(error.response?.data?.message || '检查库存失败')
|
||
} finally {
|
||
checkingInventory.value = false
|
||
}
|
||
}
|
||
|
||
// 表单验证规则
|
||
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 hierarchicalTaskTypes = ref<any[]>([])
|
||
|
||
// 加载层级任务类型数据
|
||
const loadHierarchicalTaskTypes = async () => {
|
||
try {
|
||
const response = await configApi.getDemandTypesTree()
|
||
if (response && response.data) {
|
||
hierarchicalTaskTypes.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 === 3) {
|
||
// 选择了三级分类,设置taskTypeId为三级分类的ID
|
||
item.taskTypeId = value[2] as number
|
||
item.taskTypePath = value
|
||
} else {
|
||
// 未选择完整路径
|
||
item.taskTypeId = undefined
|
||
item.taskTypePath = value
|
||
}
|
||
calculateScore()
|
||
}
|
||
|
||
// 获取任务类型信息(根据ID从平铺的taskTypes中查找)
|
||
const getTaskTypeInfo = (taskTypeId: number) => {
|
||
// 从平铺列表中查找
|
||
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
|
||
}
|
||
|
||
// 加载设计师列表
|
||
const loadDesignersForItem = async (item: TaskTypeItem) => {
|
||
if (!item.taskTypeId) return
|
||
|
||
const typeInfo = taskTypes.value.find(t => t.id === item.taskTypeId)
|
||
if (!typeInfo || !typeInfo.allowedDeptIds) return
|
||
|
||
// 简化处理:取第一个允许的部门ID
|
||
const deptIds = typeInfo.allowedDeptIds.split(',')
|
||
if (deptIds.length === 0) return
|
||
|
||
const deptId = Number(deptIds[0])
|
||
|
||
if (!designersCache[deptId]) {
|
||
try {
|
||
const res = await userApi.getDesignersWithWorkload(deptId)
|
||
if (res.code === 200) {
|
||
designersCache[deptId] = res.data
|
||
}
|
||
} catch (error) {
|
||
console.error('加载设计师失败:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取当前任务类型对应的设计师列表
|
||
const getDesignersForItem = (item: TaskTypeItem) => {
|
||
if (!item.taskTypeId) return []
|
||
|
||
const typeInfo = taskTypes.value.find(t => t.id === item.taskTypeId)
|
||
if (!typeInfo || !typeInfo.allowedDeptIds) return []
|
||
|
||
const deptIds = typeInfo.allowedDeptIds.split(',')
|
||
if (deptIds.length === 0) return []
|
||
|
||
const deptId = Number(deptIds[0])
|
||
return designersCache[deptId] || []
|
||
}
|
||
|
||
// 添加任务类型
|
||
const addTaskType = () => {
|
||
formData.taskTypes.push({
|
||
taskTypeId: undefined,
|
||
taskTypePath: undefined,
|
||
quantity: 1,
|
||
receiverId: undefined
|
||
})
|
||
}
|
||
|
||
// 删除任务类型
|
||
const removeTaskType = (index: number) => {
|
||
if (formData.taskTypes.length > 1) {
|
||
formData.taskTypes.splice(index, 1)
|
||
calculateScore()
|
||
}
|
||
}
|
||
|
||
// 自动生成任务标题
|
||
const generateTitle = () => {
|
||
const parts: string[] = []
|
||
|
||
// 1. SPU信息
|
||
if (formData.spuInfo && formData.spuInfo.trim()) {
|
||
parts.push(formData.spuInfo.trim())
|
||
}
|
||
|
||
// 2. 平台名称
|
||
if (formData.platformId) {
|
||
const platform = platforms.value.find((p: any) => p.id === formData.platformId)
|
||
if (platform) {
|
||
parts.push(platform.name)
|
||
}
|
||
}
|
||
|
||
// 3. 品牌名称
|
||
if (formData.brandId) {
|
||
const brand = brands.value.find((b: any) => b.id === formData.brandId)
|
||
if (brand) {
|
||
parts.push(brand.name)
|
||
}
|
||
}
|
||
|
||
// 4. 任务子类型
|
||
if (formData.taskSubType) {
|
||
parts.push(formData.taskSubType)
|
||
}
|
||
|
||
// 5. 需求类型(第一个任务类型名称)
|
||
if (formData.taskTypes.length > 0 && formData.taskTypes[0].taskTypeId) {
|
||
const taskType = taskTypes.value.find((t: any) => t.id === formData.taskTypes[0].taskTypeId)
|
||
if (taskType) {
|
||
parts.push(taskType.name)
|
||
}
|
||
}
|
||
|
||
// 生成标题
|
||
if (parts.length > 0) {
|
||
formData.title = parts.join('-')
|
||
}
|
||
}
|
||
|
||
// 监听相关字段变化,自动生成标题
|
||
watch(
|
||
() => [
|
||
formData.spuInfo,
|
||
formData.platformId,
|
||
formData.brandId,
|
||
formData.taskSubType,
|
||
formData.taskTypes[0]?.taskTypeId
|
||
],
|
||
() => {
|
||
// 只有当标题为空或是之前自动生成的标题时才自动更新
|
||
generateTitle()
|
||
},
|
||
{ deep: true }
|
||
)
|
||
|
||
// 计算预估积分(累加所有任务类型)
|
||
const estimatedScore = computed(() => {
|
||
let totalScore = 0
|
||
|
||
for (const item of formData.taskTypes) {
|
||
if (item.taskTypeId && item.quantity > 0) {
|
||
const selectedType = taskTypes.value.find(t => t.id === item.taskTypeId)
|
||
if (selectedType) {
|
||
totalScore += (selectedType.unitScore || 0) * item.quantity
|
||
}
|
||
}
|
||
}
|
||
|
||
return totalScore
|
||
})
|
||
|
||
// 计算流水线分组
|
||
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 = taskTypes.value.find(t => t.id === taskType.taskTypeId)
|
||
if (!typeInfo) continue
|
||
|
||
// 获取接单部门配置
|
||
const deptIds = typeInfo.allowedDeptIds || ''
|
||
const 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
|
||
})
|
||
|
||
// 根据部门ID获取部门名称
|
||
const getDeptNameFromIds = (deptIds: string): string => {
|
||
if (!deptIds) return '所有部门'
|
||
|
||
// 这里可以从部门列表中查找,暂时简化处理
|
||
// TODO: 从系统中获取部门名称
|
||
const ids = deptIds.split(',')
|
||
if (ids.length === 1) {
|
||
return `部门 ${ids[0]}`
|
||
}
|
||
return `${ids.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 calculateScore = () => {
|
||
// 计算已在computed中完成
|
||
const score = estimatedScore.value
|
||
// 管理员跳过积分检查
|
||
if (!isAdmin.value && score > teamScore.value) {
|
||
message.warning('团队积分余额不足,无法发起此任务')
|
||
}
|
||
}
|
||
|
||
// 筛选选项
|
||
const filterOption = (input: string, option: any) => {
|
||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||
}
|
||
|
||
// 取消
|
||
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.productBarcode) {
|
||
message.error('请输入商品条码')
|
||
return
|
||
}
|
||
if (!inventoryCheckResult.value) {
|
||
message.error('请先检查库存')
|
||
return
|
||
}
|
||
if (!inventoryCheckResult.value.canPublish) {
|
||
message.error('库存不足,请先申请样品后再发布任务')
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查积分余额(管理员跳过)
|
||
if (!isAdmin.value && estimatedScore.value > teamScore.value) {
|
||
message.error('团队积分余额不足')
|
||
return
|
||
}
|
||
|
||
// 检查指定接收人
|
||
if (formData.releaseType === 'assigned') {
|
||
const missingReceiver = validTaskTypes.some(item => !item.receiverId)
|
||
if (missingReceiver) {
|
||
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
|
||
})),
|
||
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 // 主任务不设接收人,由子任务指定
|
||
}
|
||
|
||
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 isLt50M = file.size / 1024 / 1024 < 50
|
||
if (!isLt50M) {
|
||
console.error('[文件验证] 文件大小超限:', file.size)
|
||
message.error('文件大小不能超过 50MB')
|
||
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 loadTaskForEdit = async (taskId: number) => {
|
||
try {
|
||
loading.value = true
|
||
const response = await taskApi.getTaskById(taskId)
|
||
const task = response.data
|
||
|
||
if (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.taskTypeIds) {
|
||
const typeIds = task.taskTypeIds.split(',').map((id: string) => parseInt(id.trim()))
|
||
// 尝试获取每个类型对应的数量(如果有taskItems,从中获取)
|
||
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
|
||
}]
|
||
}
|
||
|
||
// 处理附件 - 完整加载原有附件
|
||
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('任务数据加载成功:', task)
|
||
}
|
||
} 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'
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载发布类型配置失败:', error)
|
||
}
|
||
}
|
||
|
||
// 组件挂载
|
||
onMounted(async () => {
|
||
// 先刷新用户信息,确保部门信息最新
|
||
try {
|
||
await authStore.fetchUserInfo()
|
||
console.log('用户信息已刷新:', authStore.userInfo)
|
||
} catch (error) {
|
||
console.error('刷新用户信息失败:', error)
|
||
}
|
||
|
||
// 然后加载配置和积分
|
||
await Promise.all([
|
||
loadConfigData(),
|
||
loadTeamScore(),
|
||
loadTaskSubTypes(),
|
||
loadReleaseTypeConfig(),
|
||
loadHierarchicalTaskTypes()
|
||
])
|
||
|
||
// 检查是否是编辑模式
|
||
const editId = route.query.editId
|
||
if (editId) {
|
||
await loadTaskForEdit(Number(editId))
|
||
}
|
||
|
||
// 监听配置更新事件
|
||
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
|
||
})
|
||
|
||
// 组件卸载
|
||
onUnmounted(() => {
|
||
window.removeEventListener('configUpdated', handleConfigUpdate 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;
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发布类型选项
|
||
// 任务发布类型按钮组
|
||
.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;
|
||
|
||
&:last-child {
|
||
margin-bottom: $space-lg;
|
||
}
|
||
}
|
||
|
||
.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-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;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 库存检查结果样式
|
||
.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; // 增加高度以便容纳更多选项
|
||
|
||
&: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;
|
||
}
|
||
}
|
||
</style>
|