Compare commits

...

5 Commits
zs96 ... main

2 changed files with 266 additions and 191 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);
@ -165,10 +169,12 @@ class WeekReportController extends Controller
4. 不要使用"高效完成""圆满完成""取得显著成效"等套话 4. 不要使用"高效完成""圆满完成""取得显著成效"等套话
5. 每个任务用简短的一两句话描述即可,说清楚做了什么 5. 每个任务用简短的一两句话描述即可,说清楚做了什么
6. 可以按项目或类型简单分组,但不要过度分类 6. 可以按项目或类型简单分组,但不要过度分类
7. 【重要】只有标注了"(有截图)"的任务才添加"[图片占位符-任务X]",其他任务不要添加{$nextWeekRequirement} 7. 【重要】只有标注了"(有截图)"的任务才在该任务描述后紧接着添加"[图片占位符-任务X]"X是原始任务序号,其他任务不要添加{$nextWeekRequirement}
9. 总结部分一两句话概括即可,不要写得太官方 9. 【重要】不要添加任何总结、小结、回顾等内容
10. 不要出现具体的完成时间、耗时等信息 10. 不要出现具体的完成时间、耗时等信息
11. 整体篇幅适中,不要太长 11. 整体篇幅适中,不要太长
12. 【重要】图片占位符的序号要与原始任务序号的序号一致,不要出现错位
直接输出周报内容: 直接输出周报内容:
PROMPT; PROMPT;
@ -224,10 +230,11 @@ PROMPT;
} }
/** /**
* 在报告中插入图片 * 在报告中插入图片(支持多图)
*/ */
private function insertImagesToReport(string $report, array $tasks): string private function insertImagesToReport(string $report, array $tasks): string
{ {
// 首先尝试替换AI生成的占位符
foreach ($tasks as $index => $task) { foreach ($tasks as $index => $task) {
$num = $index + 1; $num = $index + 1;
@ -241,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

@ -6,8 +6,8 @@
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<title>智能周报生成器</title> <title>智能周报生成器</title>
<link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.css" rel="stylesheet"> <link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/marked/9.1.6/marked.min.js"></script> <script src="https://unpkg.com/marked@9.1.6/marked.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script> <script src="https://unpkg.com/html2pdf.js@0.10.1/dist/html2pdf.bundle.min.js"></script>
<style> <style>
:root { :root {
@ -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">
<span class="task-number">${index + 1}</span> <div style="display: flex; align-items: center;">
<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,56 +1360,76 @@
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 formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('/upload', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken
},
body: formData
});
const data = await response.json();
if (data.success) {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.image = data.path;
task.imagePath = data.url;
renderTasks();
showToast('图片上传成功', 'success');
}
} else {
showToast(data.message || '图片上传失败', 'error');
}
} catch (error) {
showToast('图片上传失败: ' + error.message, 'error');
}
}
// 移除图片
function removeImage(taskId) {
const task = tasks.find(t => t.id === taskId); const task = tasks.find(t => t.id === taskId);
if (task) { if (!task) return;
task.image = null;
task.imagePath = ''; // 初始化数组(兼容旧数据)
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();
formData.append('image', file);
try {
const response = await fetch('/upload', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken
},
body: formData
});
const data = await response.json();
if (data.success) {
task.images.push(data.path);
task.imagePaths.push(data.url);
successCount++;
} else {
failCount++;
}
} catch (error) {
failCount++;
console.error('图片上传失败:', error);
}
}
renderTasks();
if (successCount > 0) {
showToast(`成功上传 ${successCount} 张图片`, 'success');
}
if (failCount > 0) {
showToast(`${failCount} 张图片上传失败`, 'error');
}
}
// 移除指定索引的图片
function removeImage(taskId, imageIndex) {
const task = tasks.find(t => t.id === taskId);
if (task && task.images && task.imagePaths) {
task.images.splice(imageIndex, 1);
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,
@ -1435,7 +1492,20 @@
document.getElementById('preview-placeholder').style.display = 'none'; document.getElementById('preview-placeholder').style.display = 'none';
const previewEl = document.getElementById('markdown-preview'); const previewEl = document.getElementById('markdown-preview');
previewEl.style.display = 'block'; previewEl.style.display = 'block';
previewEl.innerHTML = marked.parse(markdown);
// 检查 marked 是否可用
if (typeof marked !== 'undefined') {
previewEl.innerHTML = marked.parse(markdown);
} else {
// 简单的Markdown转换
previewEl.innerHTML = markdown
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>');
}
document.getElementById('download-buttons').style.display = 'flex'; document.getElementById('download-buttons').style.display = 'flex';
document.getElementById('copy-btn').disabled = false; document.getElementById('copy-btn').disabled = false;
@ -1517,103 +1587,90 @@
document.querySelector('.loading-text').textContent = '正在生成PDF...'; document.querySelector('.loading-text').textContent = '正在生成PDF...';
try { try {
// 创建临时容器用于PDF生成 // 转换Markdown为HTML
const tempDiv = document.createElement('div'); let htmlContent = '';
tempDiv.id = 'pdf-content'; if (typeof marked !== 'undefined') {
tempDiv.innerHTML = ` htmlContent = marked.parse(generatedReport);
<div class="pdf-wrapper"> } else {
${marked.parse(generatedReport)} htmlContent = generatedReport
</div> .replace(/^### (.*$)/gim, '<h3>$1</h3>')
`; .replace(/^## (.*$)/gim, '<h2>$1</h2>')
tempDiv.style.cssText = ` .replace(/^# (.*$)/gim, '<h1>$1</h1>')
position: absolute; .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
left: -9999px; .replace(/\*(.*)\*/gim, '<em>$1</em>')
top: 0; .replace(/\n/gim, '<br>');
width: 210mm; }
background: white;
font-family: "Microsoft YaHei", "SimHei", "PingFang SC", -apple-system, sans-serif;
font-size: 12pt;
line-height: 1.6;
color: #333;
`;
// 添加内联样式 // 使用新窗口打印方式生成PDF
const style = document.createElement('style'); const printWindow = window.open('', '_blank');
style.textContent = ` printWindow.document.write(`
#pdf-content .pdf-wrapper { <!DOCTYPE html>
padding: 15mm 20mm; <html>
box-sizing: border-box; <head>
} <meta charset="UTF-8">
#pdf-content h1 { <title>周报_${document.getElementById('week-start').value}</title>
font-size: 18pt; <style>
color: #1a1a1a; @media print {
border-bottom: 2px solid #4a90d9; body { margin: 0; padding: 20mm; }
padding-bottom: 8px; }
margin: 0 0 15px 0; body {
} font-family: "Microsoft YaHei", "SimHei", "PingFang SC", sans-serif;
#pdf-content h2 { font-size: 12pt;
font-size: 14pt; line-height: 1.8;
color: #2c3e50; color: #333;
margin: 20px 0 10px 0; padding: 20mm;
padding-left: 10px; max-width: 210mm;
border-left: 4px solid #4a90d9; margin: 0 auto;
} background: white;
#pdf-content h3 { }
font-size: 12pt; h1 {
color: #34495e; font-size: 18pt;
margin: 15px 0 8px 0; color: #1a1a1a;
} border-bottom: 2px solid #3b82f6;
#pdf-content p { padding-bottom: 8px;
margin: 0 0 10px 0; margin: 0 0 16px 0;
text-align: justify; }
} h2 {
#pdf-content ul, #pdf-content ol { font-size: 14pt;
margin: 0 0 10px 20px; color: #1e40af;
padding: 0; margin: 20px 0 10px 0;
} padding-left: 10px;
#pdf-content li { border-left: 4px solid #3b82f6;
margin-bottom: 5px; }
} h3 {
#pdf-content img { font-size: 12pt;
max-width: 100%; color: #374151;
height: auto; margin: 16px 0 8px 0;
margin: 10px 0; font-weight: 600;
border: 1px solid #ddd; }
border-radius: 4px; p { margin: 0 0 10px 0; }
} ul, ol { margin: 0 0 10px 20px; padding: 0; }
#pdf-content strong { li { margin-bottom: 5px; }
color: #2c3e50; strong { color: #1e40af; }
} img { max-width: 100%; height: auto; margin: 10px 0; }
#pdf-content blockquote { blockquote {
margin: 10px 0; margin: 10px 0;
padding: 10px 15px; padding: 10px 15px;
border-left: 4px solid #4a90d9; border-left: 4px solid #3b82f6;
background: #f8f9fa; background: #f0f9ff;
} }
`; </style>
tempDiv.prepend(style); </head>
<body>
${htmlContent}
<script>
window.onload = function() {
setTimeout(function() {
window.print();
}, 300);
};
<\/script>
</body>
</html>
`);
printWindow.document.close();
document.body.appendChild(tempDiv); showToast('请在打印对话框中选择"另存为PDF"', 'info');
const opt = {
margin: 0,
filename: `周报_${document.getElementById('week-start').value}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
width: tempDiv.querySelector('.pdf-wrapper').scrollWidth,
windowWidth: tempDiv.querySelector('.pdf-wrapper').scrollWidth
},
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
};
await html2pdf().set(opt).from(tempDiv.querySelector('.pdf-wrapper')).save();
document.body.removeChild(tempDiv);
showToast('PDF 下载成功', 'success');
} catch (error) { } catch (error) {
showToast('下载失败: ' + error.message, 'error'); showToast('下载失败: ' + error.message, 'error');
console.error('PDF生成错误:', error); console.error('PDF生成错误:', error);