From 25210ff20bfadf129f6dcb3d78e56d00c6737163 Mon Sep 17 00:00:00 2001 From: Ethanfly Date: Fri, 9 Jan 2026 11:26:22 +0800 Subject: [PATCH] Implement ZenTao integration for task import and enhance week report generation. Added ZenTao database connection settings, UI for importing tasks, and updated report generation logic for improved clarity and execution time. Adjusted PDF generation styles and added error handling for task loading. --- app/Http/Controllers/WeekReportController.php | 42 +-- app/Http/Controllers/ZentaoController.php | 103 +++++++ app/Services/ZentaoService.php | 203 ++++++++++++++ config/database.php | 20 ++ resources/views/weekreport/index.blade.php | 263 ++++++++++++++++-- routes/web.php | 8 + 6 files changed, 600 insertions(+), 39 deletions(-) create mode 100644 app/Http/Controllers/ZentaoController.php create mode 100644 app/Services/ZentaoService.php 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'); +});