qiangdanxitong/frontend/src/views/CreateTask.vue

2244 lines
64 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<DefaultLayout>
<div class="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>