mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-12 11:19:56 +00:00
feat: 添加AI分析工作汇报功能
This commit is contained in:
parent
39ff0d1516
commit
e0443aa336
@ -6,10 +6,12 @@ use App\Exceptions\ApiException;
|
|||||||
use App\Models\AbstractModel;
|
use App\Models\AbstractModel;
|
||||||
use App\Models\ProjectTask;
|
use App\Models\ProjectTask;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
|
use App\Models\ReportAnalysis;
|
||||||
use App\Models\ReportLink;
|
use App\Models\ReportLink;
|
||||||
use App\Models\ReportReceive;
|
use App\Models\ReportReceive;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\WebSocketDialogMsg;
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\AI;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
use App\Tasks\PushTask;
|
use App\Tasks\PushTask;
|
||||||
@ -501,9 +503,121 @@ class ReportController extends AbstractController
|
|||||||
$one->report_link = $link;
|
$one->report_link = $link;
|
||||||
$link->increment("num");
|
$link->increment("num");
|
||||||
}
|
}
|
||||||
|
$analysis = ReportAnalysis::query()
|
||||||
|
->whereRid($one->id)
|
||||||
|
->whereUserid($user->userid)
|
||||||
|
->first();
|
||||||
|
if ($analysis) {
|
||||||
|
$updatedAt = $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null;
|
||||||
|
$one->setAttribute('ai_analysis', [
|
||||||
|
'id' => $analysis->id,
|
||||||
|
'text' => $analysis->analysis_text,
|
||||||
|
'model' => $analysis->model,
|
||||||
|
'updated_at' => $updatedAt,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$one->setAttribute('ai_analysis', null);
|
||||||
|
}
|
||||||
|
|
||||||
return Base::retSuccess("success", $one);
|
return Base::retSuccess("success", $one);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/report/ai_analyze 生成工作汇报 AI 分析
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,仅支持报告提交人或接收人发起分析
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup report
|
||||||
|
* @apiName ai_analyze
|
||||||
|
*
|
||||||
|
* @apiParam {Number} id 报告ID
|
||||||
|
* @apiParam {Array|String} [focus] 分析关注点(可选)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {Number} data.id 分析记录ID
|
||||||
|
* @apiSuccess {String} data.text 分析内容(Markdown)
|
||||||
|
* @apiSuccess {String} data.updated_at 最近更新时间
|
||||||
|
*/
|
||||||
|
public function ai_analyze(): array
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$id = intval(Request::input("id"));
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Base::retError("缺少ID参数");
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = Report::getOne($id);
|
||||||
|
|
||||||
|
if (!$this->userCanAccessReport($report, $user)) {
|
||||||
|
return Base::retError("无权访问该工作汇报");
|
||||||
|
}
|
||||||
|
|
||||||
|
$analysis = ReportAnalysis::query()
|
||||||
|
->whereRid($report->id)
|
||||||
|
->whereUserid($user->userid)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$context = [
|
||||||
|
'viewer_name' => $user->nickname ?? ('用户' . $user->userid),
|
||||||
|
];
|
||||||
|
if (!empty($user->profession)) {
|
||||||
|
$context['viewer_role'] = $user->profession;
|
||||||
|
} elseif (is_array($user->identity) && !empty($user->identity)) {
|
||||||
|
$context['viewer_role'] = implode('/', $user->identity);
|
||||||
|
}
|
||||||
|
if ($analysis && $analysis->analysis_text) {
|
||||||
|
$context['previous_feedback'] = $analysis->analysis_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$focus = Request::input('focus');
|
||||||
|
if (is_array($focus)) {
|
||||||
|
$context['focus'] = $focus;
|
||||||
|
} elseif (is_string($focus) && trim($focus) !== '') {
|
||||||
|
$context['focus_note'] = trim($focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = AI::analyzeReport($report, $context);
|
||||||
|
if (Base::isError($result)) {
|
||||||
|
return Base::retError("生成AI分析失败", $result);
|
||||||
|
}
|
||||||
|
$data = $result['data'];
|
||||||
|
|
||||||
|
if (!$analysis) {
|
||||||
|
$analysis = ReportAnalysis::fillInstance([
|
||||||
|
'rid' => $report->id,
|
||||||
|
'userid' => $user->userid,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = array_filter([
|
||||||
|
'viewer_role' => $context['viewer_role'] ?? null,
|
||||||
|
'viewer_name' => $context['viewer_name'] ?? null,
|
||||||
|
'focus' => $context['focus'] ?? null,
|
||||||
|
], function ($value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
return !empty($value);
|
||||||
|
}
|
||||||
|
return $value !== null && $value !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
$analysis->updateInstance([
|
||||||
|
'model' => $data['model'] ?? '',
|
||||||
|
'analysis_text' => $data['text'],
|
||||||
|
'meta' => $meta,
|
||||||
|
]);
|
||||||
|
$analysis->save();
|
||||||
|
|
||||||
|
$analysis->refresh();
|
||||||
|
|
||||||
|
return Base::retSuccess("success", [
|
||||||
|
'id' => $analysis->id,
|
||||||
|
'text' => $analysis->analysis_text,
|
||||||
|
'updated_at' => $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/mark 标记已读/未读
|
* @api {get} api/report/mark 标记已读/未读
|
||||||
*
|
*
|
||||||
@ -691,4 +805,22 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
return Base::retSuccess("success", $data);
|
return Base::retSuccess("success", $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否有权限查看/分析指定工作汇报
|
||||||
|
* @param Report $report
|
||||||
|
* @param User $user
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function userCanAccessReport(Report $report, User $user): bool
|
||||||
|
{
|
||||||
|
if ($report->userid === $user->userid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReportReceive::query()
|
||||||
|
->whereRid($report->id)
|
||||||
|
->whereUserid($user->userid)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use JetBrains\PhpStorm\Pure;
|
use JetBrains\PhpStorm\Pure;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,6 +79,16 @@ class Report extends AbstractModel
|
|||||||
->withPivot("receive_at", "read");
|
->withPivot("receive_at", "read");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function aiAnalyses(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ReportAnalysis::class, 'rid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aiAnalysis(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(ReportAnalysis::class, 'rid');
|
||||||
|
}
|
||||||
|
|
||||||
public function sendUser()
|
public function sendUser()
|
||||||
{
|
{
|
||||||
return $this->hasOne(User::class, "userid", "userid");
|
return $this->hasOne(User::class, "userid", "userid");
|
||||||
|
|||||||
27
app/Models/ReportAnalysis.php
Normal file
27
app/Models/ReportAnalysis.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ReportAnalysis extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $table = 'report_ai_analyses';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'rid',
|
||||||
|
'userid',
|
||||||
|
'model',
|
||||||
|
'analysis_text',
|
||||||
|
'meta',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'meta' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function report(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Report::class, 'rid');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Module;
|
namespace App\Module;
|
||||||
|
|
||||||
|
use App\Models\Report;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Cache;
|
use Cache;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@ -621,6 +622,67 @@ class AI
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对工作汇报内容进行分析
|
||||||
|
* @param Report $report
|
||||||
|
* @param array $context
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function analyzeReport(Report $report, array $context = [])
|
||||||
|
{
|
||||||
|
$prompt = self::buildReportAnalysisPrompt($report, $context);
|
||||||
|
if ($prompt === '') {
|
||||||
|
return Base::retError("报告内容为空,无法进行分析");
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = "gpt-5-mini";
|
||||||
|
$post = json_encode([
|
||||||
|
"model" => $model,
|
||||||
|
"reasoning_effort" => "minimal",
|
||||||
|
"messages" => [
|
||||||
|
[
|
||||||
|
"role" => "system",
|
||||||
|
"content" => <<<EOF
|
||||||
|
你是一名经验丰富的团队管理顾问,擅长阅读和分析员工提交的工作汇报,能够快速提炼重点并给出可执行建议。
|
||||||
|
|
||||||
|
输出要求:
|
||||||
|
1. 使用简洁的 Markdown 结构(标题、无序列表、引用等),不要使用代码块或 JSON
|
||||||
|
2. 先给出整体概览,再列出具体亮点、风险或问题,以及明确的改进建议
|
||||||
|
3. 如有数据或目标,应评估其完成情况和后续跟进要点
|
||||||
|
4. 语气保持专业、客观,中立,不过度夸赞或批评
|
||||||
|
5. 控制在 200-400 字之间,必要时可略微增减,但保持紧凑
|
||||||
|
EOF
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"role" => "user",
|
||||||
|
"content" => $prompt,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ai = new self($post);
|
||||||
|
$ai->setTimeout(60);
|
||||||
|
|
||||||
|
$res = $ai->request();
|
||||||
|
if (Base::isError($res)) {
|
||||||
|
return Base::retError("工作汇报分析失败", $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = trim($res['data']);
|
||||||
|
$content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content);
|
||||||
|
$content = preg_replace('/\s*```\s*$/', '', $content);
|
||||||
|
$content = trim($content);
|
||||||
|
|
||||||
|
if ($content === '') {
|
||||||
|
return Base::retError("工作汇报分析结果为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess("success", [
|
||||||
|
'text' => $content,
|
||||||
|
'model' => $model,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建任务生成的上下文提示信息
|
* 构建任务生成的上下文提示信息
|
||||||
* @param array $context 上下文信息
|
* @param array $context 上下文信息
|
||||||
@ -890,6 +952,64 @@ class AI
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建工作汇报分析的提示词
|
||||||
|
* @param Report $report
|
||||||
|
* @param array $context
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function buildReportAnalysisPrompt(Report $report, array $context = []): string
|
||||||
|
{
|
||||||
|
$sections = [];
|
||||||
|
|
||||||
|
$metaLines = [];
|
||||||
|
$metaLines[] = "标题:" . ($report->title ?: "(未填写)");
|
||||||
|
$metaLines[] = "类型:" . match ($report->type) {
|
||||||
|
Report::WEEKLY => "周报",
|
||||||
|
Report::DAILY => "日报",
|
||||||
|
default => $report->type,
|
||||||
|
};
|
||||||
|
if ($report->created_at) {
|
||||||
|
$createdAt = $report->created_at instanceof Carbon
|
||||||
|
? $report->created_at->toDateTimeString()
|
||||||
|
: (string)$report->created_at;
|
||||||
|
$metaLines[] = "提交时间:" . $createdAt;
|
||||||
|
}
|
||||||
|
if (!empty($context['viewer_role'])) {
|
||||||
|
$metaLines[] = "查看人角色:" . trim($context['viewer_role']);
|
||||||
|
}
|
||||||
|
if (!empty($context['viewer_name'])) {
|
||||||
|
$metaLines[] = "查看人:" . trim($context['viewer_name']);
|
||||||
|
}
|
||||||
|
if (!empty($metaLines)) {
|
||||||
|
$sections[] = "### 基础信息\n" . implode("\n", array_map(function ($line) {
|
||||||
|
return "- " . $line;
|
||||||
|
}, $metaLines));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($context['focus']) && is_array($context['focus'])) {
|
||||||
|
$focusItems = array_filter(array_map('trim', $context['focus']));
|
||||||
|
if (!empty($focusItems)) {
|
||||||
|
$sections[] = "### 关注重点\n" . implode("\n", array_map(function ($line) {
|
||||||
|
return "- " . $line;
|
||||||
|
}, $focusItems));
|
||||||
|
}
|
||||||
|
} elseif (!empty($context['focus_note'])) {
|
||||||
|
$sections[] = "### 关注重点\n- " . trim($context['focus_note']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($context['previous_feedback'])) {
|
||||||
|
$sections[] = "### 历史反馈\n" . trim($context['previous_feedback']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentMarkdown = trim(Base::html2markdown($report->content));
|
||||||
|
if ($contentMarkdown !== '') {
|
||||||
|
$sections[] = "### 汇报正文\n" . $contentMarkdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim(implode("\n\n", array_filter($sections)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 ollama 模型
|
* 获取 ollama 模型
|
||||||
* @param $baseUrl
|
* @param $baseUrl
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateReportAiAnalysesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('report_ai_analyses')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('report_ai_analyses', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('rid')->comment('报告ID');
|
||||||
|
$table->unsignedBigInteger('userid')->comment('生成分析的会员ID');
|
||||||
|
$table->string('model')->default('')->comment('使用的模型名称');
|
||||||
|
$table->longText('analysis_text')->comment('AI 分析的原始文本(Markdown)');
|
||||||
|
$table->json('meta')->nullable()->comment('额外的上下文信息');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['rid', 'userid'], 'uk_report_ai_analysis_rid_userid');
|
||||||
|
$table->index(['userid', 'updated_at'], 'idx_report_ai_analysis_user_updated');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('report_ai_analyses');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="report-detail">
|
<div class="report-detail">
|
||||||
<div class="report-title user-select-auto">
|
<div class="report-title user-select-auto">
|
||||||
{{ data.title }}
|
{{ currentDetail.title }}
|
||||||
<Icon v-if="loadIng > 0" type="ios-loading" class="icon-loading"/>
|
<Icon v-if="loadIng > 0" type="ios-loading" class="icon-loading"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="data.id" class="report-detail-context">
|
<div v-if="currentDetail.id" class="report-detail-context">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<div class="report-label">
|
<div class="report-label">
|
||||||
{{ $L("汇报人") }}
|
{{ $L("汇报人") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="report-value">
|
<div class="report-value">
|
||||||
<UserAvatar :userid="data.userid" :size="28" clickOpenDetail/>
|
<UserAvatar :userid="currentDetail.userid" :size="28" clickOpenDetail/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
{{ $L("提交时间") }}
|
{{ $L("提交时间") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="report-value">
|
<div class="report-value">
|
||||||
{{ data.created_at }}
|
{{ currentDetail.created_at }}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -27,31 +27,60 @@
|
|||||||
{{ $L("汇报对象") }}
|
{{ $L("汇报对象") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="report-value">
|
<div class="report-value">
|
||||||
<template v-if="data.receives_user && data.receives_user.length === 0">-</template>
|
<template v-if="currentDetail.receives_user && currentDetail.receives_user.length === 0">-</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<UserAvatar v-for="(item, key) in data.receives_user" :key="key" :userid="item.userid" :size="28" clickOpenDetail/>
|
<UserAvatar v-for="(item, key) in currentDetail.receives_user" :key="key" :userid="item.userid" :size="28" clickOpenDetail/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="data.report_link" :title="$L('分享时间') + ':' + data.report_link.created_at">
|
<li v-if="currentDetail.report_link" :title="$L('分享时间') + ':' + currentDetail.report_link.created_at">
|
||||||
<div class="report-label">
|
<div class="report-label">
|
||||||
{{ $L("分享人") }}
|
{{ $L("分享人") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="report-value">
|
<div class="report-value">
|
||||||
<UserAvatar :userid="data.report_link.userid" :size="28" clickOpenDetail/>
|
<UserAvatar :userid="currentDetail.report_link.userid" :size="28" clickOpenDetail/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div ref="reportContent" @click="onClick" class="report-content user-select-auto" v-html="data.content"></div>
|
<div ref="reportContent" @click="onClick" class="report-content user-select-auto" v-html="currentDetail.content"></div>
|
||||||
|
<div v-if="currentDetail.id" class="report-ai-analysis">
|
||||||
|
<div class="analysis-header">
|
||||||
|
<div class="analysis-title">{{ $L("AI 分析") }}</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="aiLoading"
|
||||||
|
@click="onAnalyze">
|
||||||
|
{{ aiAnalysis ? $L("重新分析") : $L("生成分析") }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="aiLoading" class="analysis-loading">
|
||||||
|
<Icon type="ios-loading" class="icon-loading"/>
|
||||||
|
<span>{{ $L("AI 正在生成分析...") }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="aiAnalysis" class="analysis-content">
|
||||||
|
<div v-if="aiAnalysis.updated_at" class="analysis-meta">
|
||||||
|
{{ $L("最后更新:") }}{{ aiAnalysis.updated_at }}
|
||||||
|
</div>
|
||||||
|
<div class="analysis-body user-select-auto">
|
||||||
|
<VMPreview :value="aiAnalysis.text"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="analysis-empty">
|
||||||
|
{{ $L("暂无 AI 分析,点击右侧按钮生成。") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const VMPreview = () => import('../../../components/VMEditor/preview');
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ReportDetail",
|
name: "ReportDetail",
|
||||||
|
components: {VMPreview},
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
default: {},
|
default: {},
|
||||||
@ -63,16 +92,30 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loadIng: 0,
|
loadIng: 0,
|
||||||
|
aiLoading: false,
|
||||||
|
aiAnalysis: null,
|
||||||
|
detail: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['formOptions']),
|
...mapState(['formOptions']),
|
||||||
|
currentDetail() {
|
||||||
|
return this.detail || this.data || {};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'data.id': {
|
'data.id': {
|
||||||
handler(id) {
|
handler(id) {
|
||||||
if (id > 0 && this.type === 'view') {
|
if (id > 0) {
|
||||||
this.sendRead();
|
this.aiAnalysis = this.data?.ai_analysis || null;
|
||||||
|
this.detail = null;
|
||||||
|
if (this.type === 'view') {
|
||||||
|
this.sendRead();
|
||||||
|
this.fetchDetail();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.aiAnalysis = null;
|
||||||
|
this.detail = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
@ -99,7 +142,46 @@ export default {
|
|||||||
const list = $A.getTextImagesInfo(this.$refs.reportContent?.outerHTML);
|
const list = $A.getTextImagesInfo(this.$refs.reportContent?.outerHTML);
|
||||||
this.$store.dispatch("previewImage", {index: target.currentSrc, list})
|
this.$store.dispatch("previewImage", {index: target.currentSrc, list})
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
fetchDetail() {
|
||||||
|
if (!this.data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch("call", {
|
||||||
|
url: 'report/detail',
|
||||||
|
data: {
|
||||||
|
id: this.data.id,
|
||||||
|
},
|
||||||
|
}).then(({data}) => {
|
||||||
|
this.detail = data;
|
||||||
|
this.aiAnalysis = data?.ai_analysis || null;
|
||||||
|
}).catch(({msg}) => {
|
||||||
|
msg && $A.messageError(msg);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onAnalyze() {
|
||||||
|
if (!this.currentDetail.id || this.aiLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.aiLoading = true;
|
||||||
|
this.$store.dispatch("call", {
|
||||||
|
url: 'report/ai_analyze',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
id: this.currentDetail.id,
|
||||||
|
},
|
||||||
|
timeout: 60 * 1000,
|
||||||
|
}).then(({data}) => {
|
||||||
|
this.aiAnalysis = data;
|
||||||
|
if (this.detail) {
|
||||||
|
this.detail.ai_analysis = data;
|
||||||
|
}
|
||||||
|
}).catch(({msg}) => {
|
||||||
|
$A.messageError(msg);
|
||||||
|
}).finally(() => {
|
||||||
|
this.aiLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
51
resources/assets/sass/components/report.scss
vendored
51
resources/assets/sass/components/report.scss
vendored
@ -328,3 +328,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-ai-analysis {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafbff;
|
||||||
|
|
||||||
|
.analysis-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #17233d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #515a6e;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-meta {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #808695;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-empty {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #808695;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuepress-markdown-body {
|
||||||
|
h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user