feat(project): 添加日报进度分析建议功能
All checks were successful
Lint Code / Lint Code (push) Successful in 3m0s
All checks were successful
Lint Code / Lint Code (push) Successful in 3m0s
- 在项目详情页新增进度更新建议面板,展示AI分析的进度评估和具体建议 - 添加获取和应用日报建议的API接口及类型定义 - 支持批量选择和同意建议,自动更新项目状态 - 优化权限管理表格的树形选择配置,启用严格模式 - 更新.gitignore文件,排除.trae相关文件
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,4 +23,8 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
#qoder
|
#qoder
|
||||||
|
|
||||||
**.qoder**
|
**.qoder**
|
||||||
|
|
||||||
|
#trae
|
||||||
|
|
||||||
|
**.trae**
|
||||||
@@ -333,6 +333,64 @@ export const getProjectDetail = (projectId: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OverallProgressAssessment = {
|
||||||
|
status?: string;
|
||||||
|
deviationPercentage?: number;
|
||||||
|
description?: string;
|
||||||
|
keyIssues?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DailyReportUpdateSuggestionVO = {
|
||||||
|
suggestionId?: string;
|
||||||
|
targetType?: string;
|
||||||
|
targetId?: string;
|
||||||
|
targetName?: string;
|
||||||
|
currentStatus?: string;
|
||||||
|
currentProgress?: number;
|
||||||
|
suggestedStatus?: string;
|
||||||
|
suggestedProgress?: number;
|
||||||
|
reason?: string;
|
||||||
|
confidence?: number;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DailyReportAnalysisSuggestionsVO = {
|
||||||
|
analysisId?: string;
|
||||||
|
reportId?: string;
|
||||||
|
projectId?: string;
|
||||||
|
reportDate?: string;
|
||||||
|
overallProgressAssessment?: OverallProgressAssessment;
|
||||||
|
suggestions?: DailyReportUpdateSuggestionVO[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDailyReportAnalysisSuggestions = (params: {
|
||||||
|
projectId: string;
|
||||||
|
reportId?: string;
|
||||||
|
reportDate?: string;
|
||||||
|
submitterUsername?: string;
|
||||||
|
}) => {
|
||||||
|
return http.request<Result<DailyReportAnalysisSuggestionsVO>>(
|
||||||
|
"get",
|
||||||
|
"/api/v1/daily-report/analysis/suggestions",
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApplyDailyReportSuggestionsRequest = {
|
||||||
|
projectId: string;
|
||||||
|
suggestionIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyDailyReportAnalysisSuggestions = (
|
||||||
|
data: ApplyDailyReportSuggestionsRequest
|
||||||
|
) => {
|
||||||
|
return http.request<Result<number>>(
|
||||||
|
"post",
|
||||||
|
"/api/v1/daily-report/analysis/suggestions/apply",
|
||||||
|
{ data }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ==================== 项目初始化(复用 system.ts 中的定义) ====================
|
// ==================== 项目初始化(复用 system.ts 中的定义) ====================
|
||||||
|
|
||||||
/** 项目信息 */
|
/** 项目信息 */
|
||||||
|
|||||||
295
src/api/日报分析建议.openapi.json
Normal file
295
src/api/日报分析建议.openapi.json
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.1",
|
||||||
|
"info": {
|
||||||
|
"title": "默认模块",
|
||||||
|
"description": "",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/daily-report/analysis/suggestions": {
|
||||||
|
"get": {
|
||||||
|
"summary": "获取日报进度更新建议",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "projectId",
|
||||||
|
"in": "query",
|
||||||
|
"description": "项目ID",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reportId",
|
||||||
|
"in": "query",
|
||||||
|
"description": "日报ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reportDate",
|
||||||
|
"in": "query",
|
||||||
|
"description": "日报日期",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "submitterUsername",
|
||||||
|
"in": "query",
|
||||||
|
"description": "日报提交人用户名",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/BaseResponseDailyReportAnalysisSuggestionsVO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/daily-report/analysis/suggestions/apply": {
|
||||||
|
"post": {
|
||||||
|
"summary": "应用日报进度回写建议",
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "",
|
||||||
|
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApplyDailyReportSuggestionsRequest",
|
||||||
|
"description": "建议ID列表"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/BaseResponseInteger",
|
||||||
|
"description": "应用结果"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"OverallProgressAssessment": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "进度状态:ahead-提前,on_track-正常,delayed-滞后"
|
||||||
|
},
|
||||||
|
"deviationPercentage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "进度偏差百分比 (正数表示提前,负数表示滞后)"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "评估说明"
|
||||||
|
},
|
||||||
|
"keyIssues": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "关键问题"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DailyReportUpdateSuggestionVO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"suggestionId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"targetType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"targetId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"targetName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"currentStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"currentProgress": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"suggestedStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"suggestedProgress": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DailyReportAnalysisSuggestionsVO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"analysisId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"reportId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"projectId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"reportDate": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"overallProgressAssessment": {
|
||||||
|
"$ref": "#/components/schemas/OverallProgressAssessment",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/DailyReportUpdateSuggestionVO",
|
||||||
|
"description": "cn.yinlihupo.domain.vo.DailyReportUpdateSuggestionVO"
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BaseResponseDailyReportAnalysisSuggestionsVO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/DailyReportAnalysisSuggestionsVO",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BaseResponseInteger": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApplyDailyReportSuggestionsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"projectId": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"suggestionIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["projectId", "suggestionIds"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {},
|
||||||
|
"securitySchemes": {}
|
||||||
|
},
|
||||||
|
"servers": [],
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { useRoute, useRouter } from "vue-router";
|
|||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
import {
|
import {
|
||||||
getProjectDetail,
|
getProjectDetail,
|
||||||
|
getDailyReportAnalysisSuggestions,
|
||||||
|
applyDailyReportAnalysisSuggestions,
|
||||||
createTask,
|
createTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
@@ -19,6 +21,8 @@ import {
|
|||||||
type ProjectTask,
|
type ProjectTask,
|
||||||
type ProjectResource,
|
type ProjectResource,
|
||||||
type ProjectRisk,
|
type ProjectRisk,
|
||||||
|
type DailyReportAnalysisSuggestionsVO,
|
||||||
|
type DailyReportUpdateSuggestionVO,
|
||||||
type Resource,
|
type Resource,
|
||||||
type ResourceUpdateRequest
|
type ResourceUpdateRequest
|
||||||
} from "@/api/project";
|
} from "@/api/project";
|
||||||
@@ -88,6 +92,13 @@ const marginStyle = computed(() => ({
|
|||||||
// 项目详情数据
|
// 项目详情数据
|
||||||
const projectDetail = ref<ProjectDetail | null>(null);
|
const projectDetail = ref<ProjectDetail | null>(null);
|
||||||
|
|
||||||
|
const suggestionsLoading = ref(false);
|
||||||
|
const suggestionsApplying = ref(false);
|
||||||
|
const dailyReportSuggestions = ref<DailyReportAnalysisSuggestionsVO | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const selectedSuggestionIds = ref<string[]>([]);
|
||||||
|
|
||||||
// 成员详情模态框
|
// 成员详情模态框
|
||||||
const memberDetailModal = ref(false);
|
const memberDetailModal = ref(false);
|
||||||
const selectedMember = ref<ProjectMember | null>(null);
|
const selectedMember = ref<ProjectMember | null>(null);
|
||||||
@@ -548,6 +559,140 @@ async function fetchProjectDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDailyReportSuggestions() {
|
||||||
|
if (!projectId.value) return;
|
||||||
|
suggestionsLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getDailyReportAnalysisSuggestions({
|
||||||
|
projectId: projectId.value
|
||||||
|
});
|
||||||
|
const result = res as any;
|
||||||
|
if (result.code === 200) {
|
||||||
|
dailyReportSuggestions.value = result.data || null;
|
||||||
|
selectedSuggestionIds.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取进度更新建议失败:", error);
|
||||||
|
message("获取进度更新建议失败", { type: "error" });
|
||||||
|
} finally {
|
||||||
|
suggestionsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSuggestionSelectionChange(
|
||||||
|
rows: DailyReportUpdateSuggestionVO[]
|
||||||
|
) {
|
||||||
|
selectedSuggestionIds.value = rows
|
||||||
|
.map(r => String(r.suggestionId || ""))
|
||||||
|
.filter(id => id.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOverallAssessmentType(
|
||||||
|
status?: string
|
||||||
|
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||||
|
switch (status) {
|
||||||
|
case "ahead":
|
||||||
|
return "success";
|
||||||
|
case "on_track":
|
||||||
|
return "primary";
|
||||||
|
case "delayed":
|
||||||
|
return "danger";
|
||||||
|
default:
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOverallAssessmentText(status?: string) {
|
||||||
|
switch (status) {
|
||||||
|
case "ahead":
|
||||||
|
return "提前";
|
||||||
|
case "on_track":
|
||||||
|
return "正常";
|
||||||
|
case "delayed":
|
||||||
|
return "滞后";
|
||||||
|
default:
|
||||||
|
return status || "未知";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDeviation(deviationPercentage?: number) {
|
||||||
|
if (deviationPercentage === undefined || deviationPercentage === null)
|
||||||
|
return "--";
|
||||||
|
const value = Number(deviationPercentage);
|
||||||
|
if (!Number.isFinite(value)) return "--";
|
||||||
|
const sign = value > 0 ? "+" : "";
|
||||||
|
return `${sign}${value}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSuggestionApplied(status?: string) {
|
||||||
|
const normalized = (status || "").toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized === "applied" ||
|
||||||
|
normalized === "accepted" ||
|
||||||
|
normalized === "done" ||
|
||||||
|
normalized === "completed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuggestionStatusType(
|
||||||
|
status?: string
|
||||||
|
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||||
|
if (isSuggestionApplied(status)) return "success";
|
||||||
|
if (!status) return "info";
|
||||||
|
const normalized = status.toLowerCase();
|
||||||
|
if (normalized.includes("reject") || normalized.includes("fail"))
|
||||||
|
return "danger";
|
||||||
|
if (normalized.includes("pending") || normalized.includes("new"))
|
||||||
|
return "warning";
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuggestionStatusText(status?: string) {
|
||||||
|
if (isSuggestionApplied(status)) return "已应用";
|
||||||
|
if (!status) return "待处理";
|
||||||
|
const normalized = status.toLowerCase();
|
||||||
|
if (normalized.includes("reject")) return "已拒绝";
|
||||||
|
if (normalized.includes("fail")) return "失败";
|
||||||
|
if (normalized.includes("pending") || normalized.includes("new"))
|
||||||
|
return "待处理";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleApplySuggestions(
|
||||||
|
ids: Array<string | number | undefined | null>
|
||||||
|
) {
|
||||||
|
if (!projectId.value) return;
|
||||||
|
const suggestionIds = Array.from(
|
||||||
|
new Set(ids.map(i => String(i || "")).filter(Boolean))
|
||||||
|
);
|
||||||
|
if (suggestionIds.length === 0) return;
|
||||||
|
if (suggestionsApplying.value) return;
|
||||||
|
suggestionsApplying.value = true;
|
||||||
|
try {
|
||||||
|
const res = await applyDailyReportAnalysisSuggestions({
|
||||||
|
projectId: projectId.value,
|
||||||
|
suggestionIds
|
||||||
|
});
|
||||||
|
const result = res as any;
|
||||||
|
if (result.code === 200) {
|
||||||
|
message("已应用进度更新建议", { type: "success" });
|
||||||
|
await fetchProjectDetail();
|
||||||
|
await fetchDailyReportSuggestions();
|
||||||
|
} else {
|
||||||
|
message(result.message || "应用建议失败", { type: "error" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("应用进度更新建议失败:", error);
|
||||||
|
message("应用建议失败", { type: "error" });
|
||||||
|
} finally {
|
||||||
|
suggestionsApplying.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleApplySelectedSuggestions() {
|
||||||
|
await handleApplySuggestions(selectedSuggestionIds.value);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取状态文本
|
// 获取状态文本
|
||||||
function getStatusText(status?: string): string {
|
function getStatusText(status?: string): string {
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
@@ -914,6 +1059,7 @@ async function handleDeleteResource(resourceId: string) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchProjectDetail();
|
fetchProjectDetail();
|
||||||
|
fetchDailyReportSuggestions();
|
||||||
fetchGanttData();
|
fetchGanttData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -1076,9 +1222,8 @@ onMounted(() => {
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 主要内容区 -->
|
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<!-- 左侧:甘特图和物料清单 -->
|
<!-- 左侧:甘特图 -->
|
||||||
<el-col :xs="24" :lg="24">
|
<el-col :xs="24" :lg="24">
|
||||||
<!-- 甘特图 -->
|
<!-- 甘特图 -->
|
||||||
<el-card shadow="hover" class="mb-4">
|
<el-card shadow="hover" class="mb-4">
|
||||||
@@ -1244,141 +1389,354 @@ onMounted(() => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<!-- 里程碑时间线 -->
|
<el-row :gutter="16" class="mb-4">
|
||||||
<div class="milestone-section">
|
<el-col :xs="24" :lg="12" class="mb-4 lg:mb-0">
|
||||||
<div class="flex-bc mb-4">
|
<el-card shadow="hover">
|
||||||
<div class="flex items-center gap-2">
|
<template #header>
|
||||||
<el-icon :size="18" color="#f56c6c">
|
<div class="flex-bc">
|
||||||
<component :is="useRenderIcon('ri/flag-line')" />
|
<div class="flex items-center gap-2">
|
||||||
</el-icon>
|
<el-icon :size="18" color="#f56c6c">
|
||||||
<span class="font-medium text-base">项目里程碑</span>
|
<component :is="useRenderIcon('ri/flag-line')" />
|
||||||
<el-tag size="small" type="info"
|
</el-icon>
|
||||||
>{{ milestoneList.length }} 个</el-tag
|
<span class="font-medium">项目里程碑</span>
|
||||||
>
|
<el-tag size="small" type="info"
|
||||||
</div>
|
>{{ milestoneList.length }} 个</el-tag
|
||||||
<el-button
|
|
||||||
v-if="canCreateMilestone"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="openAddMilestoneModal"
|
|
||||||
>
|
>
|
||||||
<template #icon>
|
|
||||||
<component :is="useRenderIcon('ri/add-line')" />
|
|
||||||
</template>
|
|
||||||
新增里程碑
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="milestone-timeline">
|
<el-button
|
||||||
|
v-if="canCreateMilestone"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="openAddMilestoneModal"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<component :is="useRenderIcon('ri/add-line')" />
|
||||||
|
</template>
|
||||||
|
新增里程碑
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty
|
||||||
|
v-if="milestoneList.length === 0"
|
||||||
|
description="暂无里程碑"
|
||||||
|
/>
|
||||||
|
<div v-else class="milestone-timeline">
|
||||||
|
<div
|
||||||
|
v-for="(milestone, index) in sortedMilestones"
|
||||||
|
:key="milestone.id"
|
||||||
|
class="milestone-item"
|
||||||
|
:class="{
|
||||||
|
'is-completed': milestone.status === 'completed',
|
||||||
|
'is-key': milestone.isKey === 1
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="milestone-marker">
|
||||||
<div
|
<div
|
||||||
v-for="(milestone, index) in sortedMilestones"
|
class="milestone-dot"
|
||||||
:key="milestone.id"
|
:style="{
|
||||||
class="milestone-item"
|
backgroundColor: getMilestoneColor(
|
||||||
:class="{
|
milestone.status,
|
||||||
'is-completed': milestone.status === 'completed',
|
milestone.isKey
|
||||||
'is-key': milestone.isKey === 1
|
)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="milestone-marker">
|
<el-icon
|
||||||
<div
|
v-if="milestone.status === 'completed'"
|
||||||
class="milestone-dot"
|
:size="12"
|
||||||
:style="{
|
color="#fff"
|
||||||
backgroundColor: getMilestoneColor(
|
>
|
||||||
milestone.status,
|
<component :is="useRenderIcon('ri/check-line')" />
|
||||||
milestone.isKey
|
</el-icon>
|
||||||
)
|
<el-icon
|
||||||
}"
|
v-else-if="milestone.isKey === 1"
|
||||||
>
|
:size="12"
|
||||||
<el-icon
|
color="#fff"
|
||||||
v-if="milestone.status === 'completed'"
|
>
|
||||||
:size="12"
|
<component :is="useRenderIcon('ri/star-fill')" />
|
||||||
color="#fff"
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="index < sortedMilestones.length - 1"
|
||||||
|
class="milestone-line"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="milestone-content">
|
||||||
|
<div class="milestone-header">
|
||||||
|
<span class="milestone-name">{{
|
||||||
|
milestone.milestoneName
|
||||||
|
}}</span>
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="
|
||||||
|
milestone.status === 'completed'
|
||||||
|
? 'success'
|
||||||
|
: milestone.status === 'in_progress'
|
||||||
|
? 'primary'
|
||||||
|
: 'info'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ getMilestoneStatusText(milestone.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="milestone.isKey === 1"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
effect="dark"
|
||||||
|
>
|
||||||
|
关键
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="milestone-date">
|
||||||
|
<el-icon :size="12">
|
||||||
|
<component :is="useRenderIcon('ri/calendar-line')" />
|
||||||
|
</el-icon>
|
||||||
|
<span>计划日期: {{ milestone.planDate }}</span>
|
||||||
|
<span v-if="milestone.actualDate" class="actual-date">
|
||||||
|
(实际: {{ milestone.actualDate }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="milestone.description" class="milestone-desc">
|
||||||
|
{{ milestone.description }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="canUpdateMilestone || canDeleteMilestone"
|
||||||
|
class="milestone-actions"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="canUpdateMilestone"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="openEditMilestoneModal(milestone)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
v-if="canDeleteMilestone"
|
||||||
|
title="确定要删除该里程碑吗?"
|
||||||
|
@confirm="handleDeleteMilestone(milestone.id)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button link type="danger" size="small"
|
||||||
|
>删除</el-button
|
||||||
>
|
>
|
||||||
<component :is="useRenderIcon('ri/check-line')" />
|
</template>
|
||||||
</el-icon>
|
</el-popconfirm>
|
||||||
<el-icon
|
|
||||||
v-else-if="milestone.isKey === 1"
|
|
||||||
:size="12"
|
|
||||||
color="#fff"
|
|
||||||
>
|
|
||||||
<component :is="useRenderIcon('ri/star-fill')" />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="index < sortedMilestones.length - 1"
|
|
||||||
class="milestone-line"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="milestone-content">
|
|
||||||
<div class="milestone-header">
|
|
||||||
<span class="milestone-name">{{
|
|
||||||
milestone.milestoneName
|
|
||||||
}}</span>
|
|
||||||
<el-tag
|
|
||||||
size="small"
|
|
||||||
:type="
|
|
||||||
milestone.status === 'completed'
|
|
||||||
? 'success'
|
|
||||||
: milestone.status === 'in_progress'
|
|
||||||
? 'primary'
|
|
||||||
: 'info'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ getMilestoneStatusText(milestone.status) }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag
|
|
||||||
v-if="milestone.isKey === 1"
|
|
||||||
size="small"
|
|
||||||
type="danger"
|
|
||||||
effect="dark"
|
|
||||||
>关键</el-tag
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="milestone-date">
|
|
||||||
<el-icon :size="12"
|
|
||||||
><component :is="useRenderIcon('ri/calendar-line')"
|
|
||||||
/></el-icon>
|
|
||||||
<span>计划日期: {{ milestone.planDate }}</span>
|
|
||||||
<span v-if="milestone.actualDate" class="actual-date">
|
|
||||||
(实际: {{ milestone.actualDate }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="milestone.description" class="milestone-desc">
|
|
||||||
{{ milestone.description }}
|
|
||||||
</div>
|
|
||||||
<!-- 里程碑操作按钮 -->
|
|
||||||
<div
|
|
||||||
v-if="canUpdateMilestone || canDeleteMilestone"
|
|
||||||
class="milestone-actions"
|
|
||||||
>
|
|
||||||
<el-button
|
|
||||||
v-if="canUpdateMilestone"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="openEditMilestoneModal(milestone)"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</el-button>
|
|
||||||
<el-popconfirm
|
|
||||||
v-if="canDeleteMilestone"
|
|
||||||
title="确定要删除该里程碑吗?"
|
|
||||||
@confirm="handleDeleteMilestone(milestone.id)"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<el-button link type="danger" size="small">
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :lg="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex-bc">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<component :is="useRenderIcon(RobotIcon)" />
|
||||||
|
<span class="font-medium">进度更新建议</span>
|
||||||
|
<el-tag
|
||||||
|
v-if="dailyReportSuggestions?.suggestions?.length"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
{{ dailyReportSuggestions.suggestions.length }} 条
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-popconfirm
|
||||||
|
title="确认同意并应用所选建议吗?"
|
||||||
|
@confirm="handleApplySelectedSuggestions"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="selectedSuggestionIds.length === 0"
|
||||||
|
:loading="suggestionsApplying"
|
||||||
|
>
|
||||||
|
同意所选
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
:disabled="suggestionsLoading"
|
||||||
|
@click="fetchDailyReportSuggestions"
|
||||||
|
>
|
||||||
|
<component :is="useRenderIcon(RefreshIcon)" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-loading="suggestionsLoading">
|
||||||
|
<el-empty
|
||||||
|
v-if="!dailyReportSuggestions?.suggestions?.length"
|
||||||
|
description="暂无进度更新建议"
|
||||||
|
/>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-if="dailyReportSuggestions.overallProgressAssessment"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="font-medium">总体评估</span>
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="
|
||||||
|
getOverallAssessmentType(
|
||||||
|
dailyReportSuggestions.overallProgressAssessment
|
||||||
|
.status
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
getOverallAssessmentText(
|
||||||
|
dailyReportSuggestions.overallProgressAssessment
|
||||||
|
.status
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</el-tag>
|
||||||
|
<span class="text-sm text-gray-500">
|
||||||
|
偏差
|
||||||
|
{{
|
||||||
|
formatDeviation(
|
||||||
|
dailyReportSuggestions.overallProgressAssessment
|
||||||
|
.deviationPercentage
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
dailyReportSuggestions.overallProgressAssessment
|
||||||
|
.description
|
||||||
|
"
|
||||||
|
class="text-sm text-gray-600"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
dailyReportSuggestions.overallProgressAssessment
|
||||||
|
.description
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
dailyReportSuggestions.overallProgressAssessment.keyIssues
|
||||||
|
?.length
|
||||||
|
"
|
||||||
|
class="mt-2 flex flex-wrap gap-2"
|
||||||
|
>
|
||||||
|
<el-tag
|
||||||
|
v-for="(issue, index) in dailyReportSuggestions
|
||||||
|
.overallProgressAssessment.keyIssues"
|
||||||
|
:key="index"
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
>
|
||||||
|
{{ issue }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="dailyReportSuggestions.suggestions || []"
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="handleSuggestionSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="50" />
|
||||||
|
<el-table-column label="目标" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ row.targetName || "--" }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
{{ row.targetType || "--" }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="当前" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="text-sm">{{ row.currentStatus || "--" }}</div>
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
{{ row.currentProgress ?? "--" }}%
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="建议" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="text-sm">
|
||||||
|
{{ row.suggestedStatus || "--" }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
{{ row.suggestedProgress ?? "--" }}%
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="原因" min-width="260" prop="reason" />
|
||||||
|
<el-table-column label="置信度" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
row.confidence !== undefined &&
|
||||||
|
row.confidence !== null
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ Math.round(Number(row.confidence) * 100) }}%
|
||||||
|
</span>
|
||||||
|
<span v-else>--</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="getSuggestionStatusType(row.status)"
|
||||||
|
>
|
||||||
|
{{ getSuggestionStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-popconfirm
|
||||||
|
title="确认同意并应用该建议吗?"
|
||||||
|
@confirm="handleApplySuggestions([row.suggestionId])"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="
|
||||||
|
!row.suggestionId ||
|
||||||
|
isSuggestionApplied(row.status)
|
||||||
|
"
|
||||||
|
:loading="suggestionsApplying"
|
||||||
|
>
|
||||||
|
同意
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 主要内容区 -->
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<!-- 左侧:物料清单 -->
|
||||||
|
<el-col :xs="24" :lg="24">
|
||||||
<!-- 物料清单 -->
|
<!-- 物料清单 -->
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|||||||
@@ -137,7 +137,11 @@ const {
|
|||||||
background: 'var(--el-fill-color-light)',
|
background: 'var(--el-fill-color-light)',
|
||||||
color: 'var(--el-text-color-primary)'
|
color: 'var(--el-text-color-primary)'
|
||||||
}"
|
}"
|
||||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
:tree-props="{
|
||||||
|
children: 'children',
|
||||||
|
hasChildren: 'hasChildren',
|
||||||
|
checkStrictly: true
|
||||||
|
}"
|
||||||
row-class-name="permission-table-row"
|
row-class-name="permission-table-row"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
@page-size-change="handleSizeChange"
|
@page-size-change="handleSizeChange"
|
||||||
|
|||||||
Reference in New Issue
Block a user