feat: 新增三个功能
1. 任务标题自动生成 - 根据SPU信息+平台+品牌+任务子类型+需求类型自动组合生成标题 - 添加watch监听相关字段变化自动更新标题 2. 预览图上传功能 - 添加S3预览图上传API(自动压缩到200px宽度) - Task实体增加previewImage字段 - 前端添加预览图上传组件 - 任务卡片显示65x65像素预览图 3. 接单大厅按部门显示 - 根据用户所属部门筛选可接单的任务类型 - 只显示用户部门对应的任务类型的任务
This commit is contained in:
parent
2687906aa1
commit
2c713febc6
@ -167,6 +167,13 @@
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Thumbnailator - 图片处理库 -->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>0.4.20</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -181,4 +181,54 @@ public class FileUploadController {
|
||||
return Result.error("获取文件列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传预览图(自动压缩到最大宽度200px)
|
||||
*
|
||||
* @param file 图片文件
|
||||
* @param request HttpServletRequest
|
||||
* @return 预览图URL
|
||||
*/
|
||||
@PostMapping("/upload-preview")
|
||||
public Result<String> uploadPreviewImage(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
HttpServletRequest request) {
|
||||
|
||||
try {
|
||||
// 从token获取用户信息
|
||||
String token = request.getHeader("Authorization");
|
||||
if (token == null || !token.startsWith("Bearer ")) {
|
||||
return Result.error("未登录或token无效");
|
||||
}
|
||||
token = token.substring(7);
|
||||
|
||||
String username = jwtUtil.getUsernameFromToken(token);
|
||||
if (username == null) {
|
||||
return Result.error("token无效");
|
||||
}
|
||||
|
||||
User user = userService.getUserByUsername(username);
|
||||
if (user == null) {
|
||||
return Result.error("用户不存在");
|
||||
}
|
||||
|
||||
// 验证是否为图片
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("image/")) {
|
||||
return Result.error("只能上传图片文件(jpg, png, gif等)");
|
||||
}
|
||||
|
||||
// 上传预览图
|
||||
String previewUrl = s3Service.uploadPreviewImage(
|
||||
file,
|
||||
user.getId(),
|
||||
user.getRealName() != null ? user.getRealName() : user.getUsername()
|
||||
);
|
||||
|
||||
return Result.success(previewUrl);
|
||||
|
||||
} catch (Exception e) {
|
||||
return Result.error("预览图上传失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +171,7 @@ public class TaskController {
|
||||
return Result.success(taskPage);
|
||||
}
|
||||
|
||||
@Operation(summary = "任务大厅", description = "获取可接单的任务列表,支持多种筛选、搜索和排序")
|
||||
@Operation(summary = "任务大厅", description = "获取可接单的任务列表,支持多种筛选、搜索和排序,根据用户所属部门筛选")
|
||||
@GetMapping("/hall")
|
||||
public Result<IPage<Task>> getTaskHall(
|
||||
@Parameter(description = "任务类型ID") @RequestParam(required = false) Long taskTypeId,
|
||||
@ -187,17 +187,23 @@ public class TaskController {
|
||||
@Parameter(description = "发布时间范围:today/week/month") @RequestParam(required = false) String timeRange,
|
||||
@Parameter(description = "排序方式:latest/score/deadline/hot") @RequestParam(required = false) String sortBy,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@RequestHeader("Authorization") String authHeader) {
|
||||
|
||||
// 获取当前用户的部门ID
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
Long userDeptId = taskService.getUserDeptId(userId);
|
||||
|
||||
log.info("查询任务大厅: taskTypeId={}, demandTypeId={}, brandId={}, categoryId={}, platformId={}, " +
|
||||
"taskSubType={}, minScore={}, maxScore={}, keyword={}, publisherName={}, timeRange={}, " +
|
||||
"sortBy={}, page={}, pageSize={}",
|
||||
"sortBy={}, page={}, pageSize={}, userDeptId={}",
|
||||
taskTypeId, demandTypeId, brandId, categoryId, platformId, taskSubType,
|
||||
minScore, maxScore, keyword, publisherName, timeRange, sortBy, page, pageSize);
|
||||
minScore, maxScore, keyword, publisherName, timeRange, sortBy, page, pageSize, userDeptId);
|
||||
|
||||
IPage<Task> taskPage = taskService.getTaskHall(page, pageSize, taskTypeId, demandTypeId,
|
||||
brandId, categoryId, platformId, taskSubType, minScore, maxScore, keyword,
|
||||
publisherName, timeRange, sortBy);
|
||||
publisherName, timeRange, sortBy, userDeptId);
|
||||
return Result.success(taskPage);
|
||||
}
|
||||
|
||||
|
||||
@ -105,6 +105,11 @@ public class Task implements Serializable {
|
||||
*/
|
||||
private String attachmentUrl;
|
||||
|
||||
/**
|
||||
* 预览图URL
|
||||
*/
|
||||
private String previewImage;
|
||||
|
||||
/**
|
||||
* 交付路径
|
||||
*/
|
||||
@ -409,6 +414,9 @@ public class Task implements Serializable {
|
||||
public String getAttachmentUrl() { return attachmentUrl; }
|
||||
public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; }
|
||||
|
||||
public String getPreviewImage() { return previewImage; }
|
||||
public void setPreviewImage(String previewImage) { this.previewImage = previewImage; }
|
||||
|
||||
public String getDeliveryPath() { return deliveryPath; }
|
||||
public void setDeliveryPath(String deliveryPath) { this.deliveryPath = deliveryPath; }
|
||||
|
||||
|
||||
@ -76,6 +76,71 @@ public interface TaskMapper extends BaseMapper<Task> {
|
||||
@Param("timeRange") String timeRange,
|
||||
@Param("sortBy") String sortBy);
|
||||
|
||||
/**
|
||||
* 查询任务大厅(按部门筛选)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT t.*, " +
|
||||
" u.real_name AS publisher_name, " +
|
||||
" d.dept_name AS publisher_dept_name, " +
|
||||
" b.brand_name AS brand_name, " +
|
||||
" c.category_name AS category_name, " +
|
||||
" p.platform_name AS platform_name, " +
|
||||
" approver.real_name AS current_approver_name " +
|
||||
"FROM task t " +
|
||||
"LEFT JOIN sys_user u ON t.publisher_id = u.id " +
|
||||
"LEFT JOIN sys_dept d ON t.publisher_dept_id = d.id " +
|
||||
"LEFT JOIN brand b ON t.brand_id = b.id " +
|
||||
"LEFT JOIN category c ON t.category_id = c.id " +
|
||||
"LEFT JOIN platform p ON t.platform_id = p.id " +
|
||||
"LEFT JOIN sys_user approver ON t.current_approver_id = approver.id " +
|
||||
"WHERE t.status = 'pending' AND t.release_type = 'recruitment' AND t.approval_status = 'approved' AND t.deleted = 0 AND (t.is_pipeline = 0 OR t.is_pipeline IS NULL)" +
|
||||
"<if test='allowedTaskTypeIds != null and allowedTaskTypeIds.size() > 0'>" +
|
||||
" AND t.task_type_id IN " +
|
||||
" <foreach item='typeId' collection='allowedTaskTypeIds' open='(' separator=',' close=')'>" +
|
||||
" #{typeId}" +
|
||||
" </foreach>" +
|
||||
"</if>" +
|
||||
"<if test='taskTypeId != null'> AND t.task_type_id = #{taskTypeId}</if>" +
|
||||
"<if test='demandTypeId != null'> AND t.demand_type_id = #{demandTypeId}</if>" +
|
||||
"<if test='brandId != null'> AND t.brand_id = #{brandId}</if>" +
|
||||
"<if test='categoryId != null'> AND t.category_id = #{categoryId}</if>" +
|
||||
"<if test='platformId != null'> AND t.platform_id = #{platformId}</if>" +
|
||||
"<if test='taskSubType != null and taskSubType != \"\"'> AND t.task_sub_type = #{taskSubType}</if>" +
|
||||
"<if test='minScore != null'> AND t.score >= #{minScore}</if>" +
|
||||
"<if test='maxScore != null'> AND t.score <= #{maxScore}</if>" +
|
||||
"<if test='keyword != null and keyword != \"\"'> " +
|
||||
" AND (t.title LIKE CONCAT('%', #{keyword}, '%') " +
|
||||
" OR t.spu_info LIKE CONCAT('%', #{keyword}, '%') " +
|
||||
" OR t.remark LIKE CONCAT('%', #{keyword}, '%') " +
|
||||
" OR u.real_name LIKE CONCAT('%', #{keyword}, '%'))" +
|
||||
"</if>" +
|
||||
"<if test='publisherName != null and publisherName != \"\"'> AND u.real_name LIKE CONCAT('%', #{publisherName}, '%')</if>" +
|
||||
"<if test='timeRange != null and timeRange == \"today\"'> AND DATE(t.create_time) = CURDATE()</if>" +
|
||||
"<if test='timeRange != null and timeRange == \"week\"'> AND t.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)</if>" +
|
||||
"<if test='timeRange != null and timeRange == \"month\"'> AND t.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)</if>" +
|
||||
"<choose>" +
|
||||
" <when test='sortBy == \"score\"'> ORDER BY t.score DESC, t.create_time DESC</when>" +
|
||||
" <when test='sortBy == \"deadline\"'> ORDER BY t.deadline ASC, t.create_time DESC</when>" +
|
||||
" <when test='sortBy == \"hot\"'> ORDER BY t.score DESC, (SELECT COUNT(*) FROM task WHERE task_type_id = t.task_type_id AND status = 'pending') DESC, t.create_time DESC</when>" +
|
||||
" <otherwise> ORDER BY t.create_time DESC</otherwise>" +
|
||||
"</choose>" +
|
||||
"</script>")
|
||||
IPage<Task> selectTaskHallByDept(Page<Task> page,
|
||||
@Param("taskTypeId") Long taskTypeId,
|
||||
@Param("demandTypeId") Long demandTypeId,
|
||||
@Param("brandId") Long brandId,
|
||||
@Param("categoryId") Long categoryId,
|
||||
@Param("platformId") Long platformId,
|
||||
@Param("taskSubType") String taskSubType,
|
||||
@Param("minScore") Integer minScore,
|
||||
@Param("maxScore") Integer maxScore,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("publisherName") String publisherName,
|
||||
@Param("timeRange") String timeRange,
|
||||
@Param("sortBy") String sortBy,
|
||||
@Param("allowedTaskTypeIds") List<Long> allowedTaskTypeIds);
|
||||
|
||||
/**
|
||||
* 查询我发布的任务
|
||||
*/
|
||||
|
||||
@ -18,11 +18,14 @@ import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
|
||||
/**
|
||||
* S3文件存储服务
|
||||
@ -143,4 +146,84 @@ public class S3Service {
|
||||
.orderByDesc(FileUpload::getCreateTime);
|
||||
return fileUploadMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传预览图(压缩到最大宽度200px,保持比例)
|
||||
*
|
||||
* @param file 图片文件
|
||||
* @param uploaderId 上传者ID
|
||||
* @param uploaderName 上传者姓名
|
||||
* @return 压缩后图片的URL
|
||||
*/
|
||||
public String uploadPreviewImage(MultipartFile file, Long uploaderId, String uploaderName) throws IOException {
|
||||
// 获取激活的S3配置
|
||||
S3Config config = s3ConfigMapper.getActiveConfig();
|
||||
if (config == null) {
|
||||
throw new RuntimeException("S3配置未激活,请先配置S3存储");
|
||||
}
|
||||
|
||||
// 验证是否为图片
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("image/")) {
|
||||
throw new RuntimeException("只能上传图片文件");
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
String storedName = "preview_" + timestamp + "_" + uuid + fileExtension;
|
||||
|
||||
// 构建文件路径(预览图专用目录)
|
||||
String filePath = config.getFilePath() + "previews/" + storedName;
|
||||
|
||||
try {
|
||||
// 压缩图片到最大宽度200px,保持比例
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
Thumbnails.of(file.getInputStream())
|
||||
.width(200) // 最大宽度200像素
|
||||
.keepAspectRatio(true) // 保持比例
|
||||
.outputQuality(0.85) // 图片质量
|
||||
.toOutputStream(outputStream);
|
||||
|
||||
byte[] compressedBytes = outputStream.toByteArray();
|
||||
|
||||
// 创建S3客户端
|
||||
S3Client s3Client = createS3Client(config);
|
||||
|
||||
// 上传压缩后的图片
|
||||
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
|
||||
.bucket(config.getBucket())
|
||||
.key(filePath)
|
||||
.contentType(contentType)
|
||||
.build();
|
||||
|
||||
s3Client.putObject(putObjectRequest,
|
||||
RequestBody.fromInputStream(new ByteArrayInputStream(compressedBytes), compressedBytes.length));
|
||||
|
||||
// 生成文件URL
|
||||
String fileUrl = config.getPublicDomain() + "/" + filePath;
|
||||
|
||||
// 保存文件上传记录
|
||||
FileUpload fileUpload = new FileUpload();
|
||||
fileUpload.setOriginalName(originalFilename);
|
||||
fileUpload.setStoredName(storedName);
|
||||
fileUpload.setFilePath(filePath);
|
||||
fileUpload.setFileUrl(fileUrl);
|
||||
fileUpload.setFileSize((long) compressedBytes.length);
|
||||
fileUpload.setFileType(contentType);
|
||||
fileUpload.setUploaderId(uploaderId);
|
||||
fileUpload.setUploaderName(uploaderName);
|
||||
fileUpload.setRelatedType("preview_image");
|
||||
fileUploadMapper.insert(fileUpload);
|
||||
|
||||
log.info("预览图上传成功: {} -> {} (压缩后大小: {} bytes)", originalFilename, fileUrl, compressedBytes.length);
|
||||
return fileUrl;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("预览图上传失败: {}", originalFilename, e);
|
||||
throw new RuntimeException("预览图上传失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,16 +634,48 @@ public class TaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务大厅
|
||||
* 获取任务大厅(根据用户部门筛选可接单的任务)
|
||||
*/
|
||||
public IPage<Task> getTaskHall(Integer page, Integer pageSize, Long taskTypeId, Long demandTypeId,
|
||||
Long brandId, Long categoryId, Long platformId, String taskSubType,
|
||||
Integer minScore, Integer maxScore, String keyword, String publisherName,
|
||||
String timeRange, String sortBy) {
|
||||
String timeRange, String sortBy, Long userDeptId) {
|
||||
Page<Task> pageParam = new Page<>(page, pageSize);
|
||||
return taskMapper.selectTaskHall(pageParam, taskTypeId, demandTypeId, brandId, categoryId,
|
||||
|
||||
// 获取当前用户部门可以接单的任务类型ID列表
|
||||
List<Long> allowedTaskTypeIds = null;
|
||||
if (userDeptId != null) {
|
||||
allowedTaskTypeIds = demandTypeMapper.selectList(null).stream()
|
||||
.filter(dt -> {
|
||||
String allowedDepts = dt.getAllowedDeptIds();
|
||||
// 如果allowedDeptIds为空,表示所有部门都可接单
|
||||
if (allowedDepts == null || allowedDepts.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
// 检查用户部门是否在允许列表中
|
||||
String[] deptIds = allowedDepts.split(",");
|
||||
for (String deptId : deptIds) {
|
||||
if (deptId.trim().equals(String.valueOf(userDeptId))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(DemandType::getId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return taskMapper.selectTaskHallByDept(pageParam, taskTypeId, demandTypeId, brandId, categoryId,
|
||||
platformId, taskSubType, minScore, maxScore, keyword,
|
||||
publisherName, timeRange, sortBy);
|
||||
publisherName, timeRange, sortBy, allowedTaskTypeIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所属部门ID
|
||||
*/
|
||||
public Long getUserDeptId(Long userId) {
|
||||
User user = userService.getUserById(userId);
|
||||
return user != null ? user.getDepartmentId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -25,13 +25,17 @@
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<!-- 任务标题 -->
|
||||
<!-- 任务标题(自动生成) -->
|
||||
<a-form-item label="任务标题" name="title" required>
|
||||
<a-input
|
||||
v-model:value="formData.title"
|
||||
placeholder="请输入任务标题"
|
||||
placeholder="填写下方信息后自动生成标题"
|
||||
size="large"
|
||||
/>
|
||||
<div class="title-auto-tip">
|
||||
<Icon name="info" size="xs" style="margin-right: 4px;" />
|
||||
<span>标题将根据SPU信息、平台、品牌、任务子类型、需求类型自动生成</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 流水线任务说明 -->
|
||||
@ -265,6 +269,32 @@
|
||||
/>
|
||||
</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">
|
||||
@ -445,6 +475,10 @@ const taskSubTypes = ref<TaskSubType[]>([])
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
const uploadProgress = ref(0)
|
||||
|
||||
// 预览图上传相关
|
||||
const previewFileList = ref<UploadFile[]>([])
|
||||
const previewUploading = ref(false)
|
||||
|
||||
// 任务类型项接口
|
||||
interface TaskTypeItem {
|
||||
taskTypeId: number | undefined
|
||||
@ -467,6 +501,7 @@ const formData = reactive({
|
||||
categoryId: undefined,
|
||||
spuInfo: '',
|
||||
attachmentUrl: '',
|
||||
previewImage: '', // 预览图URL
|
||||
remark: '', // 备注信息
|
||||
releaseType: 'default',
|
||||
// receiverId: undefined, // 已移除,改用子任务指定
|
||||
@ -570,6 +605,66 @@ const removeTaskType = (index: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成任务标题
|
||||
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
|
||||
@ -1035,6 +1130,74 @@ const handleRemove = (file: UploadFile) => {
|
||||
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 {
|
||||
@ -1637,5 +1800,65 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 任务标题自动生成提示
|
||||
.title-auto-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 预览图上传区域
|
||||
.preview-upload-area {
|
||||
.upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $text-muted;
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -200,28 +200,37 @@
|
||||
class="task-card chart-card"
|
||||
@click="handleTaskClick(task)"
|
||||
>
|
||||
<!-- 任务标题 -->
|
||||
<div class="task-header">
|
||||
<h3 class="task-title">{{ task.title }}</h3>
|
||||
<div class="task-badges">
|
||||
<span v-if="task.isSubTask" class="badge badge-subtask">
|
||||
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
|
||||
子任务
|
||||
</span>
|
||||
<span v-if="isNewTask(task)" class="badge badge-new">
|
||||
<Icon name="sparkles" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
新发布
|
||||
</span>
|
||||
<span v-if="isHighScoreTask(task)" class="badge badge-high-score">
|
||||
<Icon name="star" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
高分
|
||||
</span>
|
||||
<span v-if="task.isUrgent" class="badge badge-urgent">
|
||||
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
紧急
|
||||
</span>
|
||||
<!-- 带预览图的卡片布局 -->
|
||||
<div class="card-with-preview">
|
||||
<!-- 预览图 -->
|
||||
<div v-if="task.previewImage" class="task-preview">
|
||||
<img :src="task.previewImage" alt="预览图" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="card-content" :class="{ 'has-preview': task.previewImage }">
|
||||
<!-- 任务标题 -->
|
||||
<div class="task-header">
|
||||
<h3 class="task-title">{{ task.title }}</h3>
|
||||
<div class="task-badges">
|
||||
<span v-if="task.isSubTask" class="badge badge-subtask">
|
||||
<Icon name="layers" size="sm" color="#1890ff" style="margin-right: 4px; vertical-align: middle;" />
|
||||
子任务
|
||||
</span>
|
||||
<span v-if="isNewTask(task)" class="badge badge-new">
|
||||
<Icon name="sparkles" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
新发布
|
||||
</span>
|
||||
<span v-if="isHighScoreTask(task)" class="badge badge-high-score">
|
||||
<Icon name="star" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
高分
|
||||
</span>
|
||||
<span v-if="task.isUrgent" class="badge badge-urgent">
|
||||
<Icon name="fire" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
|
||||
紧急
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流程阶段 -->
|
||||
<div v-if="getProcessStage(task)" class="process-stage">
|
||||
@ -337,6 +346,8 @@
|
||||
{{ canGrabTask(task).canGrab ? '抢单' : '不可接单' }}
|
||||
</button>
|
||||
</div>
|
||||
</div> <!-- 结束 card-content -->
|
||||
</div> <!-- 结束 card-with-preview -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3918,11 +3929,11 @@ const handleConfigUpdate = async (event: CustomEvent) => {
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
// 获取任务状态文本(增强版,支持区分驳回和草稿)
|
||||
// 获取任务状态文本(增强版,支持区分驳回、审核中和草稿)
|
||||
const getTaskStatusText = (status: string, task?: any) => {
|
||||
// 如果有task对象,检查approvalStatus来区分驳回和草稿
|
||||
// 如果有task对象,检查approvalStatus来区分不同状态
|
||||
if (task) {
|
||||
// 检查是否被驳回(无论status是draft还是rejected)
|
||||
// 1. 检查是否被驳回(无论status是draft还是rejected)
|
||||
if (task.approvalStatus === 'rejected_by_demand' || task.approvalStatus === 'rejected_by_receiving') {
|
||||
return '已驳回'
|
||||
}
|
||||
@ -3930,6 +3941,32 @@ const getTaskStatusText = (status: string, task?: any) => {
|
||||
if (status === 'rejected') {
|
||||
return '已驳回'
|
||||
}
|
||||
|
||||
// 2. 检查是否正在审核中
|
||||
if (task.approvalStatus === 'pending_demand_approval') {
|
||||
return '待部门审核'
|
||||
}
|
||||
if (task.approvalStatus === 'pending_receiving_approval') {
|
||||
return '待接单审核'
|
||||
}
|
||||
|
||||
// 3. 流水线任务且已审核通过,根据是否有接收人显示不同状态
|
||||
if (task.isPipeline === 1 && task.approvalStatus === 'approved') {
|
||||
if (task.receiverId) {
|
||||
return '进行中'
|
||||
} else {
|
||||
return '待接单'
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 普通任务已审核通过但status还是draft,修正显示
|
||||
if (task.approvalStatus === 'approved' && status === 'draft') {
|
||||
if (task.receiverId) {
|
||||
return '进行中'
|
||||
} else {
|
||||
return '待接单'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
@ -4371,6 +4408,39 @@ onUnmounted(() => {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 带预览图的卡片布局
|
||||
.card-with-preview {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.task-preview {
|
||||
flex-shrink: 0;
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&.has-preview {
|
||||
.task-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核任务卡片特殊样式
|
||||
.task-info-grid {
|
||||
display: grid;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user