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.
This commit is contained in:
parent
98e2950b2e
commit
25210ff20b
@ -52,6 +52,9 @@ class WeekReportController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function generate(Request $request)
|
public function generate(Request $request)
|
||||||
{
|
{
|
||||||
|
// 设置较长的执行时间,因为AI生成可能需要较长时间
|
||||||
|
set_time_limit(300); // 5分钟
|
||||||
|
|
||||||
$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',
|
||||||
@ -140,35 +143,34 @@ class WeekReportController extends Controller
|
|||||||
$nextWeekRequirement = '';
|
$nextWeekRequirement = '';
|
||||||
|
|
||||||
if (!empty(trim($nextWeekList))) {
|
if (!empty(trim($nextWeekList))) {
|
||||||
$nextWeekSection = "\n\n下周计划任务(用户提供):\n{$nextWeekList}";
|
$nextWeekSection = "\n\n下周计划任务:\n{$nextWeekList}";
|
||||||
$nextWeekRequirement = "\n8. 添加\"下周工作计划\"部分,根据用户提供的下周任务列表进行专业化描述和扩展";
|
$nextWeekRequirement = "\n8. 添加\"下周工作计划\"部分,简要列出计划要做的事项";
|
||||||
} else {
|
} else {
|
||||||
$nextWeekRequirement = "\n8. 不要添加下周工作计划部分(用户未提供)";
|
$nextWeekRequirement = "\n8. 不要添加下周工作计划部分";
|
||||||
}
|
}
|
||||||
|
|
||||||
return <<<PROMPT
|
return <<<PROMPT
|
||||||
你是一位专业的周报撰写助手。请根据以下任务列表,生成一份专业、详细的Markdown格式工作周报。
|
你是一位经验丰富的职场人士,擅长撰写简洁实用的工作周报。请根据以下任务信息,用自然、朴实的语言写一份周报。
|
||||||
|
|
||||||
周报时间范围:{$weekStart} 至 {$weekEnd}
|
时间:{$weekStart} 至 {$weekEnd}
|
||||||
{$authorInfo}
|
{$authorInfo}
|
||||||
|
|
||||||
本周完成的任务:
|
本周工作内容:
|
||||||
{$taskList}{$nextWeekSection}
|
{$taskList}{$nextWeekSection}
|
||||||
|
|
||||||
要求:
|
写作要求:
|
||||||
1. 使用Markdown格式输出
|
1. 使用Markdown格式
|
||||||
2. 包含周报标题(使用一级标题)
|
2. 标题简洁,如"工作周报"或"本周工作汇报"
|
||||||
3. 包含时间范围和作者信息(如果提供)
|
3. 语言要自然朴实,像正常人写的,避免过于华丽或模板化的表述
|
||||||
4. 对每个任务进行合理的分类和扩展描述
|
4. 不要使用"高效完成"、"圆满完成"、"取得显著成效"等套话
|
||||||
5. 每个任务需要包含:
|
5. 每个任务用简短的一两句话描述即可,说清楚做了什么
|
||||||
- 任务名称
|
6. 可以按项目或类型简单分组,但不要过度分类
|
||||||
- 任务详细描述(根据原始描述进行适当扩展和专业化表述)
|
7. 【重要】只有标注了"(有截图)"的任务才添加"[图片占位符-任务X]",其他任务不要添加{$nextWeekRequirement}
|
||||||
- 完成情况
|
9. 总结部分一两句话概括即可,不要写得太官方
|
||||||
6. 添加一个"本周工作总结"部分
|
10. 不要出现具体的完成时间、耗时等信息
|
||||||
7. 【重要】关于图片占位符:只有任务描述中明确标注了"(有截图)"的任务,才在该任务描述末尾单独一行添加占位符"[图片占位符-任务X]"(X为任务序号)。没有标注"(有截图)"的任务,绝对不要添加任何图片占位符{$nextWeekRequirement}
|
11. 整体篇幅适中,不要太长
|
||||||
9. 语言专业、简洁、条理清晰
|
|
||||||
|
|
||||||
请直接输出Markdown格式的周报内容,不要有任何额外说明。
|
直接输出周报内容:
|
||||||
PROMPT;
|
PROMPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +187,7 @@ PROMPT;
|
|||||||
throw new \Exception('API Key 未配置');
|
throw new \Exception('API Key 未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = Http::timeout(120)
|
$response = Http::timeout(180)
|
||||||
->withoutVerifying()
|
->withoutVerifying()
|
||||||
->withHeaders([
|
->withHeaders([
|
||||||
'Authorization' => "Bearer {$apiKey}",
|
'Authorization' => "Bearer {$apiKey}",
|
||||||
|
|||||||
103
app/Http/Controllers/ZentaoController.php
Normal file
103
app/Http/Controllers/ZentaoController.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\ZentaoService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class ZentaoController extends Controller
|
||||||
|
{
|
||||||
|
protected ZentaoService $zentaoService;
|
||||||
|
|
||||||
|
public function __construct(ZentaoService $zentaoService)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
203
app/Services/ZentaoService.php
Normal file
203
app/Services/ZentaoService.php
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class ZentaoService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取所有用户列表
|
||||||
|
*/
|
||||||
|
public function getUsers(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$users = DB::connection('zentao')
|
||||||
|
->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,26 @@ return [
|
|||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'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',
|
'migrations' => 'migrations',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -835,6 +835,29 @@
|
|||||||
<span id="task-count" style="color: var(--text-muted); font-size: 0.9rem;">0 个任务</span>
|
<span id="task-count" style="color: var(--text-muted); font-size: 0.9rem;">0 个任务</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<!-- 禅道数据源 -->
|
||||||
|
<div class="zentao-section" style="margin-bottom: 20px; padding: 15px; background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(14, 165, 233, 0.1)); border: 1px solid var(--border); border-radius: var(--radius-sm);">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;">
|
||||||
|
<i class="ri-database-2-line" style="color: var(--success); font-size: 1.2rem;"></i>
|
||||||
|
<span style="font-weight: 600; color: var(--success);">从禅道导入任务</span>
|
||||||
|
<span id="zentao-status" style="font-size: 0.8rem; color: var(--text-muted);"></span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;">
|
||||||
|
<div class="form-group" style="flex: 1; min-width: 200px; margin: 0;">
|
||||||
|
<label for="zentao-user" style="font-size: 0.85rem;">选择员工</label>
|
||||||
|
<select id="zentao-user" style="width: 100%; padding: 10px 12px; background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.95rem;">
|
||||||
|
<option value="">-- 加载中... --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success" id="load-zentao-btn" onclick="loadZentaoTasks()" style="white-space: nowrap;">
|
||||||
|
<i class="ri-download-cloud-line"></i> 导入任务
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="zentao-info" style="margin-top: 10px; font-size: 0.85rem; color: var(--text-muted); display: none;">
|
||||||
|
<i class="ri-information-line"></i> <span id="zentao-info-text"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -1035,6 +1058,149 @@
|
|||||||
return date.toISOString().split('T')[0];
|
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 = '<option value="">-- 请选择员工 --</option>';
|
||||||
|
|
||||||
|
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 = '<option value="">-- 连接失败 --</option>';
|
||||||
|
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 = '<i class="ri-loader-4-line" style="animation: spin 1s linear infinite;"></i> 导入中...';
|
||||||
|
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 通知
|
// Toast 通知
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
const container = document.getElementById('toast-container');
|
const container = document.getElementById('toast-container');
|
||||||
@ -1353,46 +1519,104 @@
|
|||||||
try {
|
try {
|
||||||
// 创建临时容器用于PDF生成
|
// 创建临时容器用于PDF生成
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = marked.parse(generatedReport);
|
tempDiv.id = 'pdf-content';
|
||||||
|
tempDiv.innerHTML = `
|
||||||
|
<div class="pdf-wrapper">
|
||||||
|
${marked.parse(generatedReport)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
tempDiv.style.cssText = `
|
tempDiv.style.cssText = `
|
||||||
font-family: "Microsoft YaHei", "SimHei", "PingFang SC", sans-serif;
|
position: absolute;
|
||||||
font-size: 14px;
|
left: -9999px;
|
||||||
line-height: 1.8;
|
top: 0;
|
||||||
color: #333;
|
width: 210mm;
|
||||||
padding: 20px;
|
|
||||||
background: white;
|
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');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
h1 { font-size: 22px; color: #1a1a1a; border-bottom: 2px solid #4a90d9; padding-bottom: 8px; margin-bottom: 16px; }
|
#pdf-content .pdf-wrapper {
|
||||||
h2 { font-size: 18px; color: #2c3e50; margin-top: 20px; margin-bottom: 12px; padding-left: 10px; border-left: 4px solid #4a90d9; }
|
padding: 15mm 20mm;
|
||||||
h3 { font-size: 16px; color: #34495e; margin-top: 16px; margin-bottom: 8px; }
|
box-sizing: border-box;
|
||||||
p { margin-bottom: 10px; text-align: justify; }
|
}
|
||||||
ul, ol { margin-left: 20px; margin-bottom: 12px; }
|
#pdf-content h1 {
|
||||||
li { margin-bottom: 6px; }
|
font-size: 18pt;
|
||||||
img { max-width: 100%; height: auto; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; }
|
color: #1a1a1a;
|
||||||
strong { color: #2c3e50; }
|
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);
|
tempDiv.prepend(style);
|
||||||
|
|
||||||
document.body.appendChild(tempDiv);
|
document.body.appendChild(tempDiv);
|
||||||
|
|
||||||
const opt = {
|
const opt = {
|
||||||
margin: [10, 10, 10, 10],
|
margin: 0,
|
||||||
filename: `周报_${document.getElementById('week-start').value}.pdf`,
|
filename: `周报_${document.getElementById('week-start').value}.pdf`,
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
html2canvas: { scale: 2, useCORS: true, logging: false },
|
html2canvas: {
|
||||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
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);
|
document.body.removeChild(tempDiv);
|
||||||
showToast('PDF 下载成功', 'success');
|
showToast('PDF 下载成功', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('下载失败: ' + error.message, 'error');
|
showToast('下载失败: ' + error.message, 'error');
|
||||||
|
console.error('PDF生成错误:', error);
|
||||||
} finally {
|
} finally {
|
||||||
document.getElementById('loading-overlay').classList.remove('active');
|
document.getElementById('loading-overlay').classList.remove('active');
|
||||||
document.querySelector('.loading-text').textContent = 'AI正在生成周报,请稍候...';
|
document.querySelector('.loading-text').textContent = 'AI正在生成周报,请稍候...';
|
||||||
@ -1434,6 +1658,7 @@
|
|||||||
// 初始化
|
// 初始化
|
||||||
initDates();
|
initDates();
|
||||||
addTask(); // 默认添加一个任务
|
addTask(); // 默认添加一个任务
|
||||||
|
loadZentaoUsers(); // 加载禅道用户列表
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\WeekReportController;
|
use App\Http\Controllers\WeekReportController;
|
||||||
|
use App\Http\Controllers\ZentaoController;
|
||||||
|
|
||||||
// 主页面
|
// 主页面
|
||||||
Route::get('/', [WeekReportController::class, 'index'])->name('home');
|
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/markdown', [WeekReportController::class, 'downloadMarkdown'])->name('download.markdown');
|
||||||
Route::post('/download/pdf', [WeekReportController::class, 'downloadPdf'])->name('download.pdf');
|
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');
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user