Enhance week report functionality to support multiple images per task. Updated validation rules and report generation logic to accommodate an array of image paths. Improved UI for image previews and added functionality for batch image uploads, ensuring a more flexible and user-friendly experience.

This commit is contained in:
Ethanfly 2026-01-15 11:50:52 +08:00
parent 432e16358f
commit 560f6515d2
2 changed files with 150 additions and 77 deletions

View File

@ -58,7 +58,8 @@ class WeekReportController extends Controller
$request->validate([ $request->validate([
'tasks' => 'required|array|min:1', 'tasks' => 'required|array|min:1',
'tasks.*.description' => 'required|string|max:2000', 'tasks.*.description' => 'required|string|max:2000',
'tasks.*.image' => 'nullable|string', 'tasks.*.images' => 'nullable|array', // 支持多图
'tasks.*.images.*' => 'nullable|string', // 每张图片路径
'week_start' => 'nullable|date', 'week_start' => 'nullable|date',
'week_end' => 'nullable|date', 'week_end' => 'nullable|date',
'author' => 'nullable|string|max:100', 'author' => 'nullable|string|max:100',
@ -106,7 +107,10 @@ class WeekReportController extends Controller
foreach ($tasks as $index => $task) { foreach ($tasks as $index => $task) {
$num = $index + 1; $num = $index + 1;
$desc = trim($task['description']); $desc = trim($task['description']);
$hasImage = !empty($task['image']) ? '(有截图)' : ''; // 支持多图:检查 images 数组
$images = $task['images'] ?? [];
$imageCount = count(array_filter($images));
$hasImage = $imageCount > 0 ? "(有{$imageCount}张截图)" : '';
$list[] = "{$num}. {$desc}{$hasImage}"; $list[] = "{$num}. {$desc}{$hasImage}";
} }
return implode("\n", $list); return implode("\n", $list);
@ -166,10 +170,11 @@ class WeekReportController extends Controller
5. 每个任务用简短的一两句话描述即可,说清楚做了什么 5. 每个任务用简短的一两句话描述即可,说清楚做了什么
6. 可以按项目或类型简单分组,但不要过度分类 6. 可以按项目或类型简单分组,但不要过度分类
7. 【重要】只有标注了"(有截图)"的任务才在该任务描述后紧接着添加"[图片占位符-任务X]"X是原始任务序号其他任务不要添加{$nextWeekRequirement} 7. 【重要】只有标注了"(有截图)"的任务才在该任务描述后紧接着添加"[图片占位符-任务X]"X是原始任务序号其他任务不要添加{$nextWeekRequirement}
8. 【重要】图片占位符的序号要与原始任务序号的序号一致,不要出现错位
9. 【重要】不要添加任何总结、小结、回顾等内容 9. 【重要】不要添加任何总结、小结、回顾等内容
10. 不要出现具体的完成时间、耗时等信息 10. 不要出现具体的完成时间、耗时等信息
11. 整体篇幅适中,不要太长 11. 整体篇幅适中,不要太长
12. 【重要】图片占位符的序号要与原始任务序号的序号一致,不要出现错位
直接输出周报内容: 直接输出周报内容:
PROMPT; PROMPT;
@ -225,7 +230,7 @@ PROMPT;
} }
/** /**
* 在报告中插入图片 * 在报告中插入图片(支持多图)
*/ */
private function insertImagesToReport(string $report, array $tasks): string private function insertImagesToReport(string $report, array $tasks): string
{ {
@ -243,11 +248,22 @@ PROMPT;
"【图片占位符{$num}", "【图片占位符{$num}",
]; ];
if (!empty($task['image'])) { // 获取该任务的所有图片
$imageUrl = asset('storage/' . $task['image']); $images = $task['images'] ?? [];
$imageMarkdown = "\n\n![任务{$num}截图]({$imageUrl})\n"; $validImages = array_filter($images);
if (!empty($validImages)) {
// 构建多图Markdown
$imagesMarkdown = "\n\n";
foreach ($validImages as $imgIndex => $imagePath) {
$imageUrl = asset('storage/' . $imagePath);
$imgNum = $imgIndex + 1;
$imagesMarkdown .= "![任务{$num}-截图{$imgNum}]({$imageUrl})\n\n";
}
// 替换占位符
foreach ($placeholders as $placeholder) { foreach ($placeholders as $placeholder) {
$report = str_replace($placeholder, $imageMarkdown, $report); $report = str_replace($placeholder, $imagesMarkdown, $report);
} }
} else { } else {
// 没有图片时移除所有占位符 // 没有图片时移除所有占位符

View File

@ -353,24 +353,32 @@
display: none; display: none;
} }
.image-preview { .image-preview-list {
position: relative; display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px; margin-top: 10px;
} }
.image-preview img { .image-preview-item {
max-width: 100%; position: relative;
max-height: 150px; display: inline-block;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
} }
.image-preview .remove-image { .image-preview-item img {
max-width: 120px;
max-height: 100px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
object-fit: cover;
}
.image-preview-item .remove-image {
position: absolute; position: absolute;
top: -8px; top: -8px;
right: -8px; right: -8px;
width: 24px; width: 22px;
height: 24px; height: 22px;
background: var(--danger); background: var(--danger);
border: none; border: none;
border-radius: 50%; border-radius: 50%;
@ -379,14 +387,26 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.9rem; font-size: 0.8rem;
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.image-preview .remove-image:hover { .image-preview-item .remove-image:hover {
transform: scale(1.1); transform: scale(1.1);
} }
.image-count-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: var(--primary);
color: white;
border-radius: 10px;
font-size: 0.75rem;
margin-left: 8px;
}
/* 按钮样式 */ /* 按钮样式 */
.btn { .btn {
display: inline-flex; display: inline-flex;
@ -1140,8 +1160,8 @@
tasks.push({ tasks.push({
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
description: task.description, description: task.description,
image: null, images: [],
imagePath: '', imagePaths: [],
zentaoId: task.id zentaoId: task.id
}); });
}); });
@ -1235,8 +1255,8 @@
tasks.push({ tasks.push({
id: taskId, id: taskId,
description: '', description: '',
image: null, images: [], // 支持多图
imagePath: '' imagePaths: [] // 对应的URL列表
}); });
renderTasks(); renderTasks();
updateTaskCount(); updateTaskCount();
@ -1260,11 +1280,35 @@
container.innerHTML = ''; container.innerHTML = '';
tasks.forEach((task, index) => { tasks.forEach((task, index) => {
// 生成多图预览HTML
let imagesPreviewHtml = '';
if (task.imagePaths && task.imagePaths.length > 0) {
imagesPreviewHtml = `
<div class="image-preview-list">
${task.imagePaths.map((url, imgIndex) => `
<div class="image-preview-item">
<img src="${url}" alt="截图${imgIndex + 1}">
<button class="remove-image" onclick="removeImage(${task.id}, ${imgIndex})">
<i class="ri-close-line"></i>
</button>
</div>
`).join('')}
</div>
`;
}
const imageCountBadge = task.imagePaths && task.imagePaths.length > 0
? `<span class="image-count-badge"><i class="ri-image-line"></i>${task.imagePaths.length}</span>`
: '';
const taskEl = document.createElement('div'); const taskEl = document.createElement('div');
taskEl.className = 'task-item'; taskEl.className = 'task-item';
taskEl.innerHTML = ` taskEl.innerHTML = `
<div class="task-header"> <div class="task-header">
<div style="display: flex; align-items: center;">
<span class="task-number">${index + 1}</span> <span class="task-number">${index + 1}</span>
${imageCountBadge}
</div>
<button class="task-delete" onclick="deleteTask(${task.id})" title="删除任务"> <button class="task-delete" onclick="deleteTask(${task.id})" title="删除任务">
<i class="ri-delete-bin-line"></i> <i class="ri-delete-bin-line"></i>
</button> </button>
@ -1282,19 +1326,12 @@
ondragover="handleDragOver(event, this)" ondragover="handleDragOver(event, this)"
ondragleave="handleDragLeave(event, this)" ondragleave="handleDragLeave(event, this)"
ondrop="handleDrop(event, ${task.id}, this)"> ondrop="handleDrop(event, ${task.id}, this)">
<input type="file" id="image-input-${task.id}" accept="image/*" <input type="file" id="image-input-${task.id}" accept="image/*" multiple
onchange="handleImageSelect(${task.id}, this)" hidden> onchange="handleImageSelect(${task.id}, this)" hidden>
<i class="ri-image-add-line"></i> <i class="ri-image-add-line"></i>
<span>点击或拖拽上传任务截图(可选)</span> <span>点击或拖拽上传任务截图(支持多张,可选)</span>
</div> </div>
${task.imagePath ? ` ${imagesPreviewHtml}
<div class="image-preview">
<img src="${task.imagePath}" alt="任务截图">
<button class="remove-image" onclick="removeImage(${task.id})">
<i class="ri-close-line"></i>
</button>
</div>
` : ''}
</div> </div>
`; `;
container.appendChild(taskEl); container.appendChild(taskEl);
@ -1323,20 +1360,33 @@
e.preventDefault(); e.preventDefault();
el.classList.remove('dragover'); el.classList.remove('dragover');
const files = e.dataTransfer.files; const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) { // 支持拖拽多张图片
uploadImage(taskId, files[0]); const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
if (imageFiles.length > 0) {
uploadImages(taskId, imageFiles);
} }
} }
// 选择图片 // 选择图片(支持多选)
function handleImageSelect(taskId, input) { function handleImageSelect(taskId, input) {
if (input.files.length > 0) { if (input.files.length > 0) {
uploadImage(taskId, input.files[0]); uploadImages(taskId, Array.from(input.files));
} }
} }
// 上传图片 // 批量上传图片
async function uploadImage(taskId, file) { async function uploadImages(taskId, files) {
const task = tasks.find(t => t.id === taskId);
if (!task) return;
// 初始化数组(兼容旧数据)
if (!task.images) task.images = [];
if (!task.imagePaths) task.imagePaths = [];
let successCount = 0;
let failCount = 0;
for (const file of files) {
const formData = new FormData(); const formData = new FormData();
formData.append('image', file); formData.append('image', file);
@ -1352,27 +1402,34 @@
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
const task = tasks.find(t => t.id === taskId); task.images.push(data.path);
if (task) { task.imagePaths.push(data.url);
task.image = data.path; successCount++;
task.imagePath = data.url;
renderTasks();
showToast('图片上传成功', 'success');
}
} else { } else {
showToast(data.message || '图片上传失败', 'error'); failCount++;
} }
} catch (error) { } catch (error) {
showToast('图片上传失败: ' + error.message, 'error'); failCount++;
console.error('图片上传失败:', error);
} }
} }
// 移除图片 renderTasks();
function removeImage(taskId) {
if (successCount > 0) {
showToast(`成功上传 ${successCount} 张图片`, 'success');
}
if (failCount > 0) {
showToast(`${failCount} 张图片上传失败`, 'error');
}
}
// 移除指定索引的图片
function removeImage(taskId, imageIndex) {
const task = tasks.find(t => t.id === taskId); const task = tasks.find(t => t.id === taskId);
if (task) { if (task && task.images && task.imagePaths) {
task.image = null; task.images.splice(imageIndex, 1);
task.imagePath = ''; task.imagePaths.splice(imageIndex, 1);
renderTasks(); renderTasks();
} }
} }
@ -1403,7 +1460,7 @@
body: JSON.stringify({ body: JSON.stringify({
tasks: validTasks.map(t => ({ tasks: validTasks.map(t => ({
description: t.description, description: t.description,
image: t.image images: t.images || [] // 支持多图
})), })),
week_start: weekStart, week_start: weekStart,
week_end: weekEnd, week_end: weekEnd,