diff --git a/app/Http/Controllers/WeekReportController.php b/app/Http/Controllers/WeekReportController.php index 88f1f0e..2fb7ca2 100644 --- a/app/Http/Controllers/WeekReportController.php +++ b/app/Http/Controllers/WeekReportController.php @@ -52,6 +52,9 @@ class WeekReportController extends Controller */ public function generate(Request $request) { + // 设置较长的执行时间,因为AI生成可能需要较长时间 + set_time_limit(300); // 5分钟 + $request->validate([ 'tasks' => 'required|array|min:1', 'tasks.*.description' => 'required|string|max:2000', @@ -140,35 +143,34 @@ class WeekReportController extends Controller $nextWeekRequirement = ''; if (!empty(trim($nextWeekList))) { - $nextWeekSection = "\n\n下周计划任务(用户提供):\n{$nextWeekList}"; - $nextWeekRequirement = "\n8. 添加\"下周工作计划\"部分,根据用户提供的下周任务列表进行专业化描述和扩展"; + $nextWeekSection = "\n\n下周计划任务:\n{$nextWeekList}"; + $nextWeekRequirement = "\n8. 添加\"下周工作计划\"部分,简要列出计划要做的事项"; } else { - $nextWeekRequirement = "\n8. 不要添加下周工作计划部分(用户未提供)"; + $nextWeekRequirement = "\n8. 不要添加下周工作计划部分"; } return <<withoutVerifying() ->withHeaders([ 'Authorization' => "Bearer {$apiKey}", diff --git a/app/Http/Controllers/ZentaoController.php b/app/Http/Controllers/ZentaoController.php new file mode 100644 index 0000000..01eabdd --- /dev/null +++ b/app/Http/Controllers/ZentaoController.php @@ -0,0 +1,103 @@ +zentaoService = $zentaoService; + } + + /** + * 获取禅道用户列表 + */ + public function getUsers(): JsonResponse + { + try { + $users = $this->zentaoService->getUsers(); + + return response()->json([ + 'success' => true, + 'users' => $users, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 500); + } + } + + /** + * 获取指定用户的本周任务和下周任务 + */ + public function getTasks(Request $request): JsonResponse + { + $request->validate([ + 'account' => 'required|string', + 'week_start' => 'nullable|date', + 'week_end' => 'nullable|date', + ]); + + $account = $request->input('account'); + $weekStart = $request->input('week_start'); + $weekEnd = $request->input('week_end'); + + try { + // 获取本周完成的任务 + $thisWeekTasks = $this->zentaoService->getWeeklyTasks($account, $weekStart, $weekEnd); + + // 获取下周任务(未完成的任务) + $nextWeekTasks = $this->zentaoService->getNextWeekTasks($account, $weekStart); + + return response()->json([ + 'success' => true, + 'this_week_tasks' => $thisWeekTasks, + 'next_week_tasks' => $nextWeekTasks, + 'summary' => [ + 'this_week_count' => count($thisWeekTasks), + 'next_week_count' => count($nextWeekTasks), + ], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 500); + } + } + + /** + * 测试禅道数据库连接 + */ + public function testConnection(): JsonResponse + { + try { + $connected = $this->zentaoService->testConnection(); + + if ($connected) { + return response()->json([ + 'success' => true, + 'message' => '禅道数据库连接成功', + ]); + } else { + return response()->json([ + 'success' => false, + 'message' => '禅道数据库连接失败', + ], 500); + } + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '连接测试失败: ' . $e->getMessage(), + ], 500); + } + } +} diff --git a/app/Services/ZentaoService.php b/app/Services/ZentaoService.php new file mode 100644 index 0000000..14d9a19 --- /dev/null +++ b/app/Services/ZentaoService.php @@ -0,0 +1,203 @@ +table('user') + ->select('id', 'account', 'realname', 'dept') + ->where('deleted', '0') + ->orderBy('realname') + ->get(); + + return $users->map(function ($user) { + return [ + 'id' => $user->id, + 'account' => $user->account, + 'realname' => $user->realname ?: $user->account, + 'dept' => $user->dept, + ]; + })->toArray(); + } catch (\Exception $e) { + throw new \Exception('连接禅道数据库失败: ' . $e->getMessage()); + } + } + + /** + * 获取指定用户本周完成的任务 + */ + public function getWeeklyTasks(string $account, ?string $startDate = null, ?string $endDate = null): array + { + // 默认本周 + $start = $startDate ? Carbon::parse($startDate)->format('Y-m-d') . ' 00:00:00' : Carbon::now()->startOfWeek()->format('Y-m-d') . ' 00:00:00'; + $end = $endDate ? Carbon::parse($endDate)->format('Y-m-d') . ' 23:59:59' : Carbon::now()->endOfWeek()->format('Y-m-d') . ' 23:59:59'; + + try { + // 查询本周完成的任务 - 根据finishedDate完成时间判断 + $tasks = DB::connection('zentao') + ->table('task') + ->leftJoin('project', 'task.project', '=', 'project.id') + ->select( + 'task.id', + 'task.name', + 'task.desc', + 'task.status', + 'task.finishedDate', + 'task.finishedBy', + 'task.assignedTo', + 'task.estimate', + 'task.consumed', + 'task.pri', + 'project.name as projectName' + ) + ->where('task.deleted', '0') + ->where(function ($query) use ($account) { + // 任务完成人或指派人是该用户 + $query->where('task.finishedBy', $account) + ->orWhere('task.assignedTo', $account); + }) + ->where(function ($query) use ($start, $end) { + // 本周完成的任务(根据完成时间判断) + $query->where(function ($q) use ($start, $end) { + $q->whereIn('task.status', ['done', 'closed']) + ->where('task.finishedDate', '>=', $start) + ->where('task.finishedDate', '<=', $end) + ->where('task.finishedDate', '!=', '0000-00-00 00:00:00'); + }); + }) + ->orderBy('task.finishedDate', 'desc') + ->get(); + + return $tasks->map(function ($task) { + return [ + 'id' => $task->id, + 'name' => $task->name, + 'description' => $this->formatTaskDescription($task), + 'status' => $this->translateStatus($task->status), + 'projectName' => $task->projectName ?: '未分类', + 'finishedDate' => $task->finishedDate, + 'consumed' => $task->consumed, + ]; + })->toArray(); + } catch (\Exception $e) { + throw new \Exception('获取任务失败: ' . $e->getMessage()); + } + } + + /** + * 获取指定用户下周的任务(已分配但未完成) + */ + public function getNextWeekTasks(string $account, ?string $startDate = null): array + { + // 下周开始时间 + $nextWeekStart = $startDate + ? Carbon::parse($startDate)->addWeek()->startOfWeek() + : Carbon::now()->addWeek()->startOfWeek(); + + try { + // 查询未完成的任务作为下周计划 + $tasks = DB::connection('zentao') + ->table('task') + ->leftJoin('project', 'task.project', '=', 'project.id') + ->select( + 'task.id', + 'task.name', + 'task.desc', + 'task.status', + 'task.deadline', + 'task.estimate', + 'task.pri', + 'project.name as projectName' + ) + ->where('task.assignedTo', $account) + ->where('task.deleted', '0') + ->whereIn('task.status', ['wait', 'doing', 'pause']) + ->orderBy('task.pri') + ->orderBy('task.deadline') + ->limit(20) + ->get(); + + return $tasks->map(function ($task) { + return [ + 'id' => $task->id, + 'name' => $task->name, + 'description' => $this->formatNextWeekTaskDescription($task), + 'status' => $this->translateStatus($task->status), + 'projectName' => $task->projectName ?: '未分类', + 'deadline' => $task->deadline, + 'estimate' => $task->estimate, + ]; + })->toArray(); + } catch (\Exception $e) { + throw new \Exception('获取下周任务失败: ' . $e->getMessage()); + } + } + + /** + * 格式化任务描述 + */ + private function formatTaskDescription($task): string + { + $desc = $task->name; + + if ($task->projectName) { + $desc = "[{$task->projectName}] {$desc}"; + } + + return $desc; + } + + /** + * 格式化下周任务描述 + */ + private function formatNextWeekTaskDescription($task): string + { + $desc = $task->name; + + if ($task->projectName) { + $desc = "[{$task->projectName}] {$desc}"; + } + + return $desc; + } + + /** + * 翻译任务状态 + */ + private function translateStatus(string $status): string + { + $map = [ + 'wait' => '未开始', + 'doing' => '进行中', + 'done' => '已完成', + 'pause' => '已暂停', + 'cancel' => '已取消', + 'closed' => '已关闭', + ]; + + return $map[$status] ?? $status; + } + + /** + * 测试数据库连接 + */ + public function testConnection(): bool + { + try { + DB::connection('zentao')->getPdo(); + return true; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/config/database.php b/config/database.php index 8315416..8b1b59e 100644 --- a/config/database.php +++ b/config/database.php @@ -12,6 +12,26 @@ return [ 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], + + // 禅道数据库连接 + 'zentao' => [ + 'driver' => 'mysql', + 'host' => env('ZENTAO_DB_HOST', '8.136.223.156'), + 'port' => env('ZENTAO_DB_PORT', '6630'), + 'database' => env('ZENTAO_DB_DATABASE', 'zentao96'), + 'username' => env('ZENTAO_DB_USERNAME', 'zs96'), + 'password' => env('ZENTAO_DB_PASSWORD', 'zs96zs96'), + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => 'zt_', + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], ], 'migrations' => 'migrations', ]; diff --git a/resources/views/weekreport/index.blade.php b/resources/views/weekreport/index.blade.php index b7a2cda..1c8619a 100644 --- a/resources/views/weekreport/index.blade.php +++ b/resources/views/weekreport/index.blade.php @@ -835,6 +835,29 @@ 0 个任务
+ +
+
+ + 从禅道导入任务 + +
+
+
+ + +
+ +
+ +
+
@@ -1035,6 +1058,149 @@ return date.toISOString().split('T')[0]; } + // ========== 禅道数据相关 ========== + let zentaoUsers = []; + + // 加载禅道用户列表 + async function loadZentaoUsers() { + const select = document.getElementById('zentao-user'); + const statusEl = document.getElementById('zentao-status'); + + try { + statusEl.textContent = '连接中...'; + statusEl.style.color = 'var(--accent)'; + + const response = await fetch('/zentao/users'); + const data = await response.json(); + + if (data.success) { + zentaoUsers = data.users; + select.innerHTML = ''; + + data.users.forEach(user => { + const option = document.createElement('option'); + option.value = user.account; + option.textContent = user.realname || user.account; + option.dataset.realname = user.realname || user.account; + select.appendChild(option); + }); + + statusEl.textContent = `已连接 (${data.users.length}人)`; + statusEl.style.color = 'var(--success)'; + } else { + throw new Error(data.message); + } + } catch (error) { + select.innerHTML = ''; + statusEl.textContent = '连接失败'; + statusEl.style.color = 'var(--danger)'; + console.error('加载禅道用户失败:', error); + } + } + + // 从禅道导入任务 + async function loadZentaoTasks() { + const account = document.getElementById('zentao-user').value; + const weekStart = document.getElementById('week-start').value; + const weekEnd = document.getElementById('week-end').value; + + if (!account) { + showToast('请先选择员工', 'error'); + return; + } + + const btn = document.getElementById('load-zentao-btn'); + const originalText = btn.innerHTML; + btn.innerHTML = ' 导入中...'; + btn.disabled = true; + + try { + const response = await fetch('/zentao/tasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + account: account, + week_start: weekStart, + week_end: weekEnd + }) + }); + + const data = await response.json(); + + if (data.success) { + // 清空现有任务 + tasks = []; + nextWeekTasks = []; + + // 填充本周任务 + data.this_week_tasks.forEach(task => { + tasks.push({ + id: Date.now() + Math.random(), + description: task.description, + image: null, + imagePath: '', + zentaoId: task.id + }); + }); + + // 填充下周任务 + if (data.next_week_tasks.length > 0) { + data.next_week_tasks.forEach(task => { + nextWeekTasks.push({ + id: Date.now() + Math.random(), + description: task.description, + zentaoId: task.id + }); + }); + + // 自动开启下周任务 + document.getElementById('next-week-toggle').checked = true; + document.getElementById('next-week-content').style.display = 'block'; + } + + // 自动填充作者姓名 + const selectedOption = document.getElementById('zentao-user').selectedOptions[0]; + if (selectedOption && selectedOption.dataset.realname) { + document.getElementById('author').value = selectedOption.dataset.realname; + } + + // 渲染任务列表 + renderTasks(); + updateTaskCount(); + renderNextWeekTasks(); + updateNextWeekCount(); + + // 显示导入信息 + const infoEl = document.getElementById('zentao-info'); + const infoText = document.getElementById('zentao-info-text'); + infoEl.style.display = 'block'; + infoText.textContent = `已导入 ${data.summary.this_week_count} 个本周任务,${data.summary.next_week_count} 个下周任务`; + + showToast(`成功导入 ${data.summary.this_week_count + data.summary.next_week_count} 个任务`, 'success'); + } else { + throw new Error(data.message); + } + } catch (error) { + showToast('导入失败: ' + error.message, 'error'); + } finally { + btn.innerHTML = originalText; + btn.disabled = false; + } + } + + // 员工选择变化时自动填充姓名 + document.addEventListener('DOMContentLoaded', function() { + document.getElementById('zentao-user').addEventListener('change', function() { + const selectedOption = this.selectedOptions[0]; + if (selectedOption && selectedOption.dataset.realname && selectedOption.value) { + document.getElementById('author').value = selectedOption.dataset.realname; + } + }); + }); + // Toast 通知 function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); @@ -1353,46 +1519,104 @@ try { // 创建临时容器用于PDF生成 const tempDiv = document.createElement('div'); - tempDiv.innerHTML = marked.parse(generatedReport); + tempDiv.id = 'pdf-content'; + tempDiv.innerHTML = ` +
+ ${marked.parse(generatedReport)} +
+ `; tempDiv.style.cssText = ` - font-family: "Microsoft YaHei", "SimHei", "PingFang SC", sans-serif; - font-size: 14px; - line-height: 1.8; - color: #333; - padding: 20px; + position: absolute; + left: -9999px; + top: 0; + width: 210mm; background: white; + font-family: "Microsoft YaHei", "SimHei", "PingFang SC", -apple-system, sans-serif; + font-size: 12pt; + line-height: 1.6; + color: #333; `; - // 添加样式 + // 添加内联样式 const style = document.createElement('style'); style.textContent = ` - h1 { font-size: 22px; color: #1a1a1a; border-bottom: 2px solid #4a90d9; padding-bottom: 8px; margin-bottom: 16px; } - h2 { font-size: 18px; color: #2c3e50; margin-top: 20px; margin-bottom: 12px; padding-left: 10px; border-left: 4px solid #4a90d9; } - h3 { font-size: 16px; color: #34495e; margin-top: 16px; margin-bottom: 8px; } - p { margin-bottom: 10px; text-align: justify; } - ul, ol { margin-left: 20px; margin-bottom: 12px; } - li { margin-bottom: 6px; } - img { max-width: 100%; height: auto; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; } - strong { color: #2c3e50; } + #pdf-content .pdf-wrapper { + padding: 15mm 20mm; + box-sizing: border-box; + } + #pdf-content h1 { + font-size: 18pt; + color: #1a1a1a; + border-bottom: 2px solid #4a90d9; + padding-bottom: 8px; + margin: 0 0 15px 0; + } + #pdf-content h2 { + font-size: 14pt; + color: #2c3e50; + margin: 20px 0 10px 0; + padding-left: 10px; + border-left: 4px solid #4a90d9; + } + #pdf-content h3 { + font-size: 12pt; + color: #34495e; + margin: 15px 0 8px 0; + } + #pdf-content p { + margin: 0 0 10px 0; + text-align: justify; + } + #pdf-content ul, #pdf-content ol { + margin: 0 0 10px 20px; + padding: 0; + } + #pdf-content li { + margin-bottom: 5px; + } + #pdf-content img { + max-width: 100%; + height: auto; + margin: 10px 0; + border: 1px solid #ddd; + border-radius: 4px; + } + #pdf-content strong { + color: #2c3e50; + } + #pdf-content blockquote { + margin: 10px 0; + padding: 10px 15px; + border-left: 4px solid #4a90d9; + background: #f8f9fa; + } `; tempDiv.prepend(style); document.body.appendChild(tempDiv); const opt = { - margin: [10, 10, 10, 10], + margin: 0, filename: `周报_${document.getElementById('week-start').value}.pdf`, image: { type: 'jpeg', quality: 0.98 }, - html2canvas: { scale: 2, useCORS: true, logging: false }, - jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } + 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).save(); + await html2pdf().set(opt).from(tempDiv.querySelector('.pdf-wrapper')).save(); document.body.removeChild(tempDiv); showToast('PDF 下载成功', 'success'); } catch (error) { showToast('下载失败: ' + error.message, 'error'); + console.error('PDF生成错误:', error); } finally { document.getElementById('loading-overlay').classList.remove('active'); document.querySelector('.loading-text').textContent = 'AI正在生成周报,请稍候...'; @@ -1434,6 +1658,7 @@ // 初始化 initDates(); addTask(); // 默认添加一个任务 + loadZentaoUsers(); // 加载禅道用户列表 diff --git a/routes/web.php b/routes/web.php index 8601ee7..490aaf1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\WeekReportController; +use App\Http\Controllers\ZentaoController; // 主页面 Route::get('/', [WeekReportController::class, 'index'])->name('home'); @@ -15,3 +16,10 @@ Route::post('/generate', [WeekReportController::class, 'generate'])->name('gener // 下载周报 Route::post('/download/markdown', [WeekReportController::class, 'downloadMarkdown'])->name('download.markdown'); Route::post('/download/pdf', [WeekReportController::class, 'downloadPdf'])->name('download.pdf'); + +// 禅道数据接口 +Route::prefix('zentao')->group(function () { + Route::get('/test', [ZentaoController::class, 'testConnection'])->name('zentao.test'); + Route::get('/users', [ZentaoController::class, 'getUsers'])->name('zentao.users'); + Route::post('/tasks', [ZentaoController::class, 'getTasks'])->name('zentao.tasks'); +});