feat(project): 支持项目初始化任务的异步查询和状态展示
Some checks failed
Lint Code / Lint Code (push) Failing after 23m42s

- 新增项目初始化任务相关类型定义及接口封装,包括任务列表、任务统计和单个任务状态查询
- 集成 Element Plus 通知组件,增加任务完成和失败时的用户通知提醒
- 在 SSE 状态管理版块添加任务列表状态管理,支持任务状态的动态获取和展示
- 在创建项目向导组件中集成任务列表展示,支持查看任务进度、完成结果及错误信息
- 增加“使用此结果”按钮,允许用户直接应用已完成任务的项目初始化结果
- 对任务列表样式进行设计,区分不同状态的任务视觉效果提升用户体验
- 打开项目创建对话框时自动刷新并加载最新的任务列表数据
This commit is contained in:
2026-03-28 18:03:08 +08:00
parent ac4d43fd01
commit c4509b42fa
4 changed files with 867 additions and 3 deletions

View File

@@ -239,3 +239,53 @@ export const confirmProjectInit = (data: ProjectInitResult) => {
{ data } { data }
); );
}; };
// ==================== 项目初始化任务SSE ====================
/** 项目初始化任务 VO - 根据 OpenAPI 定义 */
export type ProjectInitTaskVO = {
taskId: string;
userId?: number;
status: string; // pending, processing, completed, failed
statusDesc: string;
progress: number;
progressMessage: string;
originalFilename: string;
createTime: string;
startTime?: string;
completeTime?: string;
result?: ProjectInitResult;
errorMessage?: string;
};
/** 任务统计信息 */
export type TaskStats = {
total: number;
processing: number;
completed: number;
failed: number;
};
/** 查询我的任务列表 */
export const getMyTasks = () => {
return http.request<Result<ProjectInitTaskVO[]>>(
"get",
"/api/v1/project-init/my-tasks"
);
};
/** 查询我的任务统计信息 */
export const getMyTaskStats = () => {
return http.request<Result<TaskStats>>(
"get",
"/api/v1/project-init/my-tasks/stats"
);
};
/** 查询单个任务状态 */
export const getTaskStatus = (taskId: string) => {
return http.request<Result<ProjectInitTaskVO>>(
"get",
`/api/v1/project-init/task/${taskId}`
);
};

View File

@@ -0,0 +1,606 @@
{
"openapi": "3.0.1",
"info": {
"title": "默认模块",
"description": "",
"version": "1.0.0"
},
"tags": [],
"paths": {
"/api/v1/project-init/sse/submit-task": {
"post": {
"summary": "通过 SSE 提交项目初始化任务",
"deprecated": false,
"description": "使用通用 SSE 通道,通过 userId 推送进度",
"tags": [],
"parameters": [
{
"name": "userId",
"in": "query",
"description": "用户ID",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc",
"schema": {
"type": "string",
"default": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"file": {
"description": "项目资料文件",
"type": "string",
"format": "binary"
}
},
"required": ["file"]
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseMapObject",
"description": "提交结果"
}
}
}
}
},
"security": []
}
},
"/api/v1/project-init/my-tasks": {
"get": {
"summary": "查询我的任务列表",
"deprecated": false,
"description": "根据当前登录用户的token查询其所有任务",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc",
"schema": {
"type": "string",
"default": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseListProjectInitTaskVO",
"description": "任务列表"
}
}
}
}
},
"security": []
}
},
"/api/v1/project-init/my-tasks/stats": {
"get": {
"summary": "查询我的任务统计信息",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc",
"schema": {
"type": "string",
"default": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseMapObject",
"description": "统计信息"
}
}
}
}
},
"security": []
}
},
"/api/v1/project-init/task/{taskId}": {
"get": {
"summary": "查询单个任务状态",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "taskId",
"in": "path",
"description": "任务ID",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc",
"schema": {
"type": "string",
"default": "Bearer 6bf1de0f-9edf-413f-a98f-1103b8dc30fc"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseProjectInitTaskVO",
"description": "任务状态"
}
}
}
}
},
"security": []
}
}
},
"components": {
"schemas": {
"ProjectInfo": {
"type": "object",
"properties": {
"project_name": {
"type": "string",
"description": ""
},
"project_type": {
"type": "string",
"description": ""
},
"description": {
"type": "string",
"description": ""
},
"objectives": {
"type": "string",
"description": ""
},
"plan_start_date": {
"type": "string",
"description": ""
},
"plan_end_date": {
"type": "string",
"description": ""
},
"budget": {
"type": "number",
"description": ""
},
"currency": {
"type": "string",
"description": ""
},
"priority": {
"type": "string",
"description": ""
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": ""
}
}
},
"MilestoneInfo": {
"type": "object",
"properties": {
"milestone_name": {
"type": "string",
"description": ""
},
"description": {
"type": "string",
"description": ""
},
"plan_date": {
"type": "string",
"description": ""
},
"deliverables": {
"type": "string",
"description": ""
},
"owner_role": {
"type": "string",
"description": ""
}
}
},
"TaskInfo": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": ""
},
"task_name": {
"type": "string",
"description": ""
},
"parent_task_id": {
"type": "string",
"description": ""
},
"description": {
"type": "string",
"description": ""
},
"plan_start_date": {
"type": "string",
"description": ""
},
"plan_end_date": {
"type": "string",
"description": ""
},
"estimated_hours": {
"type": "integer",
"description": ""
},
"priority": {
"type": "string",
"description": ""
},
"assignee_role": {
"type": "string",
"description": ""
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": ""
},
"deliverables": {
"type": "string",
"description": ""
}
}
},
"BaseResponseMapObject": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": ""
},
"data": {
"type": "object",
"properties": {
"total": {
"type": "integer"
},
"processing": {
"type": "integer"
},
"completed": {
"type": "null"
},
"failed": {
"type": "null"
}
},
"description": ""
},
"message": {
"type": "string",
"description": ""
}
}
},
"MemberInfo": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": ""
},
"role_code": {
"type": "string",
"description": ""
},
"responsibility": {
"type": "string",
"description": ""
},
"department": {
"type": "string",
"description": ""
},
"weekly_hours": {
"type": "integer",
"description": ""
}
}
},
"ResourceInfo": {
"type": "object",
"properties": {
"resource_name": {
"type": "string",
"description": ""
},
"resource_type": {
"type": "string",
"description": ""
},
"quantity": {
"type": "number",
"description": ""
},
"unit": {
"type": "string",
"description": ""
},
"unit_price": {
"type": "number",
"description": ""
},
"supplier": {
"type": "string",
"description": ""
}
}
},
"RiskInfo": {
"type": "object",
"properties": {
"risk_name": {
"type": "string",
"description": ""
},
"category": {
"type": "string",
"description": ""
},
"description": {
"type": "string",
"description": ""
},
"probability": {
"type": "integer",
"description": ""
},
"impact": {
"type": "integer",
"description": ""
},
"mitigation_plan": {
"type": "string",
"description": ""
}
}
},
"TimelineNodeInfo": {
"type": "object",
"properties": {
"node_name": {
"type": "string",
"description": ""
},
"node_type": {
"type": "string",
"description": ""
},
"plan_date": {
"type": "string",
"description": ""
},
"description": {
"type": "string",
"description": ""
},
"kb_scope": {
"type": "array",
"items": {
"type": "string"
},
"description": ""
}
}
},
"ProjectInitResult": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/ProjectInfo",
"description": "项目基本信息"
},
"milestones": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MilestoneInfo",
"description": "cn.yinlihupo.domain.dto.ProjectInitResult.MilestoneInfo"
},
"description": "里程碑列表"
},
"tasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TaskInfo",
"description": "cn.yinlihupo.domain.dto.ProjectInitResult.TaskInfo"
},
"description": "任务清单"
},
"members": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MemberInfo",
"description": "cn.yinlihupo.domain.dto.ProjectInitResult.MemberInfo"
},
"description": "项目成员"
},
"resources": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ResourceInfo",
"description": "cn.yinlihupo.domain.dto.ProjectInitResult.ResourceInfo"
},
"description": "资源需求"
},
"risks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RiskInfo",
"description": "cn.yinlihupo.domain.dto.ProjectInitResult.RiskInfo"
},
"description": "风险识别"
},
"timeline_nodes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TimelineNodeInfo",
"description": "cn.yinlihupo.domain.dto.ProjectInitResult.TimelineNodeInfo"
},
"description": "时间节点"
}
}
},
"ProjectInitTaskVO": {
"type": "object",
"properties": {
"taskId": {
"type": "string",
"description": "任务ID"
},
"userId": {
"type": "integer",
"description": "用户ID任务所属用户",
"format": "int64"
},
"status": {
"type": "string",
"description": "任务状态: pending-待处理, processing-处理中, completed-已完成, failed-失败"
},
"statusDesc": {
"type": "string",
"description": "状态描述"
},
"progress": {
"type": "integer",
"description": "当前进度百分比 (0-100)"
},
"progressMessage": {
"type": "string",
"description": "进度描述信息"
},
"originalFilename": {
"type": "string",
"description": "原始文件名"
},
"createTime": {
"type": "string",
"description": "任务创建时间"
},
"startTime": {
"type": "string",
"description": "任务开始处理时间"
},
"completeTime": {
"type": "string",
"description": "任务完成时间"
},
"result": {
"$ref": "#/components/schemas/ProjectInitResult",
"description": "处理结果仅当status=completed时有值"
},
"errorMessage": {
"type": "string",
"description": "错误信息仅当status=failed时有值"
}
}
},
"BaseResponseListProjectInitTaskVO": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": ""
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProjectInitTaskVO",
"description": "项目初始化异步任务VO"
},
"description": ""
},
"message": {
"type": "string",
"description": ""
}
}
},
"BaseResponseProjectInitTaskVO": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": ""
},
"data": {
"$ref": "#/components/schemas/ProjectInitTaskVO",
"description": ""
},
"message": {
"type": "string",
"description": ""
}
}
}
},
"responses": {},
"securitySchemes": {}
},
"servers": [],
"security": []
}

View File

@@ -2,6 +2,11 @@ import { defineStore } from "pinia";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { SseClient, type ProjectInitTaskVO } from "@/utils/sse/SseClient"; import { SseClient, type ProjectInitTaskVO } from "@/utils/sse/SseClient";
import { store } from "../utils"; import { store } from "../utils";
import {
getMyTasks as fetchTasksApi,
type ProjectInitTaskVO as ApiTaskVO
} from "@/api/project";
import { ElNotification } from "element-plus";
export const useSseStore = defineStore("sse", () => { export const useSseStore = defineStore("sse", () => {
// State // State
@@ -13,12 +18,20 @@ export const useSseStore = defineStore("sse", () => {
"idle" | "submitted" | "processing" | "completed" | "error" "idle" | "submitted" | "processing" | "completed" | "error"
>("idle"); >("idle");
const errorMessage = ref(""); const errorMessage = ref("");
// 我的任务列表
const myTasks = ref<ApiTaskVO[]>([]);
// 是否有待处理的任务
const hasProcessingTask = computed(() =>
myTasks.value.some(t => t.status === "processing" || t.status === "pending")
);
// Getters // Getters
const getIsConnected = computed(() => isConnected.value); const getIsConnected = computed(() => isConnected.value);
const getCurrentTask = computed(() => currentTask.value); const getCurrentTask = computed(() => currentTask.value);
const getTaskProgress = computed(() => taskProgress.value); const getTaskProgress = computed(() => taskProgress.value);
const getTaskStatus = computed(() => taskStatus.value); const getTaskStatus = computed(() => taskStatus.value);
const getMyTasks = computed(() => myTasks.value);
const getHasProcessingTask = computed(() => hasProcessingTask.value);
// Actions // Actions
/** /**
@@ -55,6 +68,18 @@ export const useSseStore = defineStore("sse", () => {
taskProgress.value = 100; taskProgress.value = 100;
taskStatus.value = "completed"; taskStatus.value = "completed";
console.log("SSE Store: 任务完成", data.result); console.log("SSE Store: 任务完成", data.result);
// 发送通知
ElNotification({
title: "项目解析完成",
message: `文件 "${data.originalFilename}" 已解析完成,请前往新建项目查看预览数据。`,
type: "success",
duration: 5000,
position: "top-right"
});
// 刷新任务列表
fetchMyTasks();
}); });
// 监听任务提交 // 监听任务提交
@@ -71,6 +96,18 @@ export const useSseStore = defineStore("sse", () => {
taskStatus.value = "error"; taskStatus.value = "error";
errorMessage.value = data.error; errorMessage.value = data.error;
console.error("SSE Store: 任务错误", data.error); console.error("SSE Store: 任务错误", data.error);
// 发送错误通知
ElNotification({
title: "项目解析失败",
message: data.error || "文件解析失败,请重试",
type: "error",
duration: 5000,
position: "top-right"
});
// 刷新任务列表
fetchMyTasks();
}); });
// 监听连接错误 // 监听连接错误
@@ -122,6 +159,20 @@ export const useSseStore = defineStore("sse", () => {
errorMessage.value = ""; errorMessage.value = "";
} }
/**
* 查询我的任务列表
*/
async function fetchMyTasks() {
try {
const { code, data } = await fetchTasksApi();
if (code === 200 && data) {
myTasks.value = data;
}
} catch (error) {
console.error("获取任务列表失败", error);
}
}
return { return {
// State // State
sseClient, sseClient,
@@ -130,16 +181,20 @@ export const useSseStore = defineStore("sse", () => {
taskProgress, taskProgress,
taskStatus, taskStatus,
errorMessage, errorMessage,
myTasks,
// Getters // Getters
getIsConnected, getIsConnected,
getCurrentTask, getCurrentTask,
getTaskProgress, getTaskProgress,
getTaskStatus, getTaskStatus,
getMyTasks,
getHasProcessingTask,
// Actions // Actions
initSse, initSse,
closeSse, closeSse,
submitProjectInitTask, submitProjectInitTask,
resetTaskStatus resetTaskStatus,
fetchMyTasks
}; };
}); });

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"; import { ref, reactive, computed, watch, onMounted } from "vue";
import { message } from "@/utils/message"; import { message } from "@/utils/message";
import { confirmProjectInit } from "@/api/project"; import { confirmProjectInit, type ProjectInitTaskVO } from "@/api/project";
import type { ProjectInitResult } from "@/api/project"; import type { ProjectInitResult } from "@/api/project";
import { import {
WizardStep, WizardStep,
@@ -16,6 +16,7 @@ import CheckIcon from "~icons/ri/check-line";
import DeleteIcon from "~icons/ri/delete-bin-line"; import DeleteIcon from "~icons/ri/delete-bin-line";
import AddIcon from "~icons/ri/add-line"; import AddIcon from "~icons/ri/add-line";
import LoadingIcon from "~icons/ri/loader-4-line"; import LoadingIcon from "~icons/ri/loader-4-line";
import TimeIcon from "~icons/ri/time-line";
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
@@ -42,6 +43,18 @@ const sseStore = useSseStoreHook();
const taskProgress = computed(() => sseStore.taskProgress); const taskProgress = computed(() => sseStore.taskProgress);
const taskStatus = computed(() => sseStore.taskStatus); const taskStatus = computed(() => sseStore.taskStatus);
const currentTask = computed(() => sseStore.currentTask); const currentTask = computed(() => sseStore.currentTask);
const myTasks = computed(() => sseStore.getMyTasks);
const hasProcessingTask = computed(() => sseStore.getHasProcessingTask);
// 对话框打开时查询任务列表
watch(
() => props.visible,
visible => {
if (visible) {
sseStore.fetchMyTasks();
}
}
);
// 监听任务完成 // 监听任务完成
watch( watch(
@@ -272,6 +285,15 @@ function removeRisk(index: number) {
projectData.risks.splice(index, 1); projectData.risks.splice(index, 1);
} }
// 使用已完成任务的结果
function handleUseTaskResult(task: ProjectInitTaskVO) {
if (task.result) {
Object.assign(projectData, task.result);
currentStep.value = WizardStep.Preview;
message("已加载任务结果", { type: "success" });
}
}
// 标签输入 // 标签输入
const tagInput = ref(""); const tagInput = ref("");
function handleTagInput() { function handleTagInput() {
@@ -331,6 +353,69 @@ function removeTag(tag: string) {
</template> </template>
</el-upload> </el-upload>
<!-- 待处理任务列表 -->
<div v-if="myTasks.length > 0" class="task-list-container mt-4">
<div class="flex items-center gap-2 mb-3">
<el-icon class="text-blue-500">
<component :is="useRenderIcon(TimeIcon)" />
</el-icon>
<span class="text-sm font-medium">我的任务</span>
</div>
<div class="task-list">
<div
v-for="task in myTasks"
:key="task.taskId"
class="task-item"
:class="{
'task-processing': task.status === 'processing',
'task-completed': task.status === 'completed',
'task-failed': task.status === 'failed'
}"
>
<div class="task-info">
<span class="task-filename">{{ task.originalFilename }}</span>
<el-tag
:type="
task.status === 'completed'
? 'success'
: task.status === 'processing'
? 'warning'
: task.status === 'failed'
? 'danger'
: 'info'
"
size="small"
>
{{ task.statusDesc }}
</el-tag>
</div>
<div v-if="task.status === 'processing'" class="task-progress">
<el-progress
:percentage="task.progress"
:stroke-width="4"
:show-text="false"
/>
<span class="text-xs text-gray-500 ml-2"
>{{ task.progress }}%</span
>
</div>
<div v-if="task.status === 'completed'" class="task-actions">
<el-button
type="primary"
size="small"
link
@click="handleUseTaskResult(task)"
>
使用此结果
</el-button>
</div>
<div v-if="task.status === 'failed'" class="task-error">
<span class="text-xs text-red-500">{{ task.errorMessage }}</span>
</div>
</div>
</div>
</div>
<!-- 进度显示 --> <!-- 进度显示 -->
<div v-if="uploading" class="progress-container mt-6"> <div v-if="uploading" class="progress-container mt-6">
<div class="flex-c gap-2 mb-2"> <div class="flex-c gap-2 mb-2">
@@ -770,6 +855,74 @@ function removeTag(tag: string) {
padding: 20px; padding: 20px;
} }
.task-list-container {
padding: 12px 16px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
.task-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-item {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 10px 12px;
background: white;
border-left: 3px solid var(--el-color-info);
border-radius: 6px;
&.task-processing {
background: var(--el-color-warning-light-9);
border-left-color: var(--el-color-warning);
}
&.task-completed {
border-left-color: var(--el-color-success);
}
&.task-failed {
background: var(--el-color-danger-light-9);
border-left-color: var(--el-color-danger);
}
.task-info {
display: flex;
flex: 1;
gap: 10px;
align-items: center;
min-width: 0;
}
.task-filename {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
white-space: nowrap;
}
.task-progress {
display: flex;
align-items: center;
width: 120px;
margin-left: auto;
}
.task-actions {
margin-left: auto;
}
.task-error {
width: 100%;
margin-top: 4px;
}
}
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
gap: 10px; gap: 10px;