- 将项目相关ID类型统一由number改为string,增强一致性 - 新增项目成员、里程碑、任务、资源、风险、时间线节点等详细类型定义 - 添加获取项目详情接口方法getProjectDetail - 在路由中新增项目详情页路由配置 - 实现项目详情页面,支持展示基本信息、成员、任务、风险及资源等数据 - 项目详情页面集成AI助手简易聊天交互功能展示 - 添加项目状态、风险等级及资源状态的辅助文本和样式方法 - 优化甘特图任务条样式计算,基于项目详情任务数据展现
This commit is contained in:
865
src/views/project/detail.vue
Normal file
865
src/views/project/detail.vue
Normal file
@@ -0,0 +1,865 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import {
|
||||
getProjectDetail,
|
||||
type ProjectDetail,
|
||||
type ProjectMember,
|
||||
type ProjectMilestone,
|
||||
type ProjectTask,
|
||||
type ProjectResource,
|
||||
type ProjectRisk
|
||||
} from "@/api/project";
|
||||
import { message } from "@/utils/message";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import ArrowLeftIcon from "~icons/ri/arrow-left-line";
|
||||
import DownloadIcon from "~icons/ri/download-line";
|
||||
import EditIcon from "~icons/ri/edit-line";
|
||||
import UserIcon from "~icons/ri/user-line";
|
||||
import AlertIcon from "~icons/ri/alert-line";
|
||||
import RobotIcon from "~icons/ri/robot-2-line";
|
||||
import RefreshIcon from "~icons/ri/refresh-line";
|
||||
import FullscreenIcon from "~icons/ri/fullscreen-line";
|
||||
import FileListIcon from "~icons/ri/file-list-line";
|
||||
import ArrowRightIcon from "~icons/ri/arrow-right-s-line";
|
||||
import CheckIcon from "~icons/ri/check-line";
|
||||
|
||||
defineOptions({
|
||||
name: "ProjectDetail"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const projectId = ref<string>(route.params.id as string);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
const ganttLoading = ref(false);
|
||||
|
||||
// 项目详情数据
|
||||
const projectDetail = ref<ProjectDetail | null>(null);
|
||||
|
||||
// 项目基本信息(计算属性)
|
||||
const projectInfo = computed(() => {
|
||||
const data = projectDetail.value;
|
||||
return {
|
||||
id: data?.id || projectId.value,
|
||||
projectName: data?.projectName || "",
|
||||
projectCode: data?.projectCode || "",
|
||||
status: data?.status || "ongoing",
|
||||
statusText: getStatusText(data?.status),
|
||||
startDate: data?.planStartDate || "",
|
||||
endDate: data?.planEndDate || "",
|
||||
progress: data?.progress || 0,
|
||||
progressChange: 0,
|
||||
budget: data?.budget || 0,
|
||||
cost: data?.cost || 0,
|
||||
teamSize: data?.memberCount || 0,
|
||||
riskLevel: data?.riskLevel || "low",
|
||||
riskText: getRiskText(data?.riskLevel),
|
||||
riskCount: data?.riskCount || 0,
|
||||
description: data?.description || "",
|
||||
objectives: data?.objectives || ""
|
||||
};
|
||||
});
|
||||
|
||||
// 甘特图数据(使用任务列表)
|
||||
|
||||
// AI助手消息
|
||||
const aiMessages = ref([
|
||||
{
|
||||
type: "ai",
|
||||
content:
|
||||
"您好!我是项目AI助手,我可以帮您分析项目进度和流程卡点。请问有什么可以帮助您的?",
|
||||
time: "上午 9:30"
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
content: "当前项目进度如何?有没有什么风险点需要注意?",
|
||||
time: "上午 9:32"
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
content: `**项目进度分析:** 当前项目整体进度为68%,比计划进度超前5%。
|
||||
|
||||
**关键风险点:**
|
||||
• 玻璃幕墙材料尚未发货,可能影响后续安装工序
|
||||
• 水电安装工序存在资源调配问题,可能导致延期
|
||||
• 室内装修团队人力不足,建议增加2名熟练工人
|
||||
|
||||
**建议措施:** 优先跟进玻璃幕墙供应商发货情况,协调水电安装资源,考虑从其他项目临时调配装修工人。`,
|
||||
time: "上午 9:33"
|
||||
}
|
||||
]);
|
||||
|
||||
const aiInput = ref("");
|
||||
|
||||
// 资源清单数据(从API获取)
|
||||
const resourceList = computed(() => {
|
||||
return projectDetail.value?.resources || [];
|
||||
});
|
||||
|
||||
// 风险列表数据(从API获取)
|
||||
const riskList = computed(() => {
|
||||
return projectDetail.value?.risks || [];
|
||||
});
|
||||
|
||||
// 成员列表数据(从API获取)
|
||||
const memberList = computed(() => {
|
||||
return projectDetail.value?.members || [];
|
||||
});
|
||||
|
||||
// 里程碑列表数据(从API获取)
|
||||
const milestoneList = computed(() => {
|
||||
return projectDetail.value?.milestones || [];
|
||||
});
|
||||
|
||||
// 任务列表数据(从API获取,用于甘特图)
|
||||
const taskList = computed(() => {
|
||||
return projectDetail.value?.tasks || [];
|
||||
});
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
router.push("/project");
|
||||
}
|
||||
|
||||
// 获取甘特图数据(现在使用项目详情中的任务数据)
|
||||
async function fetchGanttData() {
|
||||
// 任务数据已从项目详情API获取,无需单独请求
|
||||
ganttLoading.value = true;
|
||||
try {
|
||||
// 模拟加载延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
} catch (error) {
|
||||
console.error("获取甘特图数据失败:", error);
|
||||
} finally {
|
||||
ganttLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送AI消息
|
||||
function sendAiMessage() {
|
||||
if (!aiInput.value.trim()) return;
|
||||
const userMsg = aiInput.value.trim();
|
||||
aiMessages.value.push({
|
||||
type: "user",
|
||||
content: userMsg,
|
||||
time: dayjs().format("HH:mm")
|
||||
});
|
||||
aiInput.value = "";
|
||||
|
||||
// 模拟AI回复
|
||||
setTimeout(() => {
|
||||
aiMessages.value.push({
|
||||
type: "ai",
|
||||
content: "收到您的问题,我正在分析项目数据,请稍候...",
|
||||
time: dayjs().format("HH:mm")
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
function getStatusType(
|
||||
status?: string
|
||||
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "success";
|
||||
case "ongoing":
|
||||
return "primary";
|
||||
case "paused":
|
||||
return "warning";
|
||||
case "cancelled":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
// 获取物料状态类型
|
||||
function getMaterialStatusType(
|
||||
status: string
|
||||
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||
switch (status) {
|
||||
case "arrived":
|
||||
return "success";
|
||||
case "pending":
|
||||
return "info";
|
||||
case "delayed":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
// 计算甘特图任务条样式(使用真实任务数据)
|
||||
function getTaskBarStyle(task: ProjectTask) {
|
||||
if (!task.planStartDate || !task.planEndDate) return {};
|
||||
const start = dayjs(task.planStartDate);
|
||||
const end = dayjs(task.planEndDate);
|
||||
const projectStart = dayjs(projectInfo.value.startDate);
|
||||
const projectEnd = dayjs(projectInfo.value.endDate);
|
||||
if (!projectStart.isValid() || !projectEnd.isValid()) return {};
|
||||
const totalDays = projectEnd.diff(projectStart, "day");
|
||||
const offsetDays = start.diff(projectStart, "day");
|
||||
const duration = end.diff(start, "day") + 1;
|
||||
|
||||
return {
|
||||
left: `${(offsetDays / totalDays) * 100}%`,
|
||||
width: `${(duration / totalDays) * 100}%`
|
||||
};
|
||||
}
|
||||
|
||||
// 获取项目详情
|
||||
async function fetchProjectDetail() {
|
||||
if (!projectId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getProjectDetail(projectId.value);
|
||||
const result = res as any;
|
||||
if (result.code === 200 && result.data) {
|
||||
projectDetail.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取项目详情失败:", error);
|
||||
message("获取项目详情失败", { type: "error" });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
ongoing: "进行中",
|
||||
completed: "已完成",
|
||||
paused: "已暂停",
|
||||
cancelled: "已取消",
|
||||
planning: "规划中",
|
||||
draft: "草稿"
|
||||
};
|
||||
return statusMap[status || ""] || "未知";
|
||||
}
|
||||
|
||||
// 获取风险文本
|
||||
function getRiskText(risk?: string): string {
|
||||
const riskMap: Record<string, string> = {
|
||||
low: "低",
|
||||
medium: "中等",
|
||||
high: "高"
|
||||
};
|
||||
return riskMap[risk || ""] || "低";
|
||||
}
|
||||
|
||||
// 获取角色文本
|
||||
function getRoleText(roleCode?: string): string {
|
||||
const roleMap: Record<string, string> = {
|
||||
manager: "项目经理",
|
||||
leader: "负责人",
|
||||
member: "成员",
|
||||
sponsor: "发起人"
|
||||
};
|
||||
return roleMap[roleCode || ""] || roleCode || "成员";
|
||||
}
|
||||
|
||||
// 获取资源类型文本
|
||||
function getResourceTypeText(type?: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
equipment: "设备",
|
||||
material: "物料",
|
||||
human: "人力",
|
||||
service: "服务"
|
||||
};
|
||||
return typeMap[type || ""] || type || "其他";
|
||||
}
|
||||
|
||||
// 获取资源状态类型
|
||||
function getResourceStatusType(
|
||||
status?: string
|
||||
): "success" | "warning" | "info" | "primary" | "danger" {
|
||||
switch (status) {
|
||||
case "arrived":
|
||||
case "actual":
|
||||
return "success";
|
||||
case "planned":
|
||||
return "info";
|
||||
case "delayed":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
// 获取资源状态文本
|
||||
function getResourceStatusText(status?: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
arrived: "已到货",
|
||||
actual: "实际",
|
||||
planned: "计划中",
|
||||
delayed: "延期"
|
||||
};
|
||||
return statusMap[status || ""] || status || "计划中";
|
||||
}
|
||||
|
||||
// 获取风险等级标签类型
|
||||
function getRiskLevelType(
|
||||
level?: string
|
||||
): "success" | "warning" | "danger" | "info" {
|
||||
switch (level) {
|
||||
case "low":
|
||||
return "success";
|
||||
case "medium":
|
||||
return "warning";
|
||||
case "high":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjectDetail();
|
||||
fetchGanttData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-detail w-full">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex-bc mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<el-button link @click="goBack">
|
||||
<component :is="useRenderIcon(ArrowLeftIcon)" />
|
||||
</el-button>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-xl font-bold">{{ projectInfo.projectName }}</h2>
|
||||
<el-tag :type="getStatusType(projectInfo.status)" size="small">
|
||||
{{ projectInfo.statusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm mt-1">
|
||||
项目编号: {{ projectInfo.projectCode }} | 开始日期:
|
||||
{{ projectInfo.startDate }} | 预计结束日期:
|
||||
{{ projectInfo.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button>
|
||||
<template #icon>
|
||||
<component :is="useRenderIcon(DownloadIcon)" />
|
||||
</template>
|
||||
导出报告
|
||||
</el-button>
|
||||
<el-button type="primary">
|
||||
<template #icon>
|
||||
<component :is="useRenderIcon(EditIcon)" />
|
||||
</template>
|
||||
编辑项目
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="16" class="mb-4">
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="flex-bc">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">项目进度</p>
|
||||
<p class="text-2xl font-bold mt-1">{{ projectInfo.progress }}%</p>
|
||||
<p class="text-xs text-green-500 mt-1">
|
||||
<el-icon
|
||||
><component :is="useRenderIcon('ri/arrow-up-line')"
|
||||
/></el-icon>
|
||||
{{ projectInfo.progressChange }}%
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon bg-blue-100">
|
||||
<el-icon :size="24" color="#409eff">
|
||||
<component :is="useRenderIcon('ri/bar-chart-line')" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="projectInfo.progress"
|
||||
:status="projectInfo.progress === 100 ? 'success' : ''"
|
||||
class="mt-3"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="flex-bc">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">项目预算</p>
|
||||
<p class="text-2xl font-bold mt-1">¥{{ projectInfo.cost }}万</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
/ ¥{{ projectInfo.budget }}万
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon bg-orange-100">
|
||||
<el-icon :size="24" color="#e6a23c">
|
||||
<component :is="useRenderIcon('ri/money-cny-circle-line')" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="
|
||||
Math.round((projectInfo.cost / projectInfo.budget) * 100)
|
||||
"
|
||||
status="warning"
|
||||
class="mt-3"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="flex-bc">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">团队成员</p>
|
||||
<p class="text-2xl font-bold mt-1">
|
||||
{{ projectInfo.teamSize }}人
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-1">项目参与人员</p>
|
||||
</div>
|
||||
<div class="stat-icon bg-green-100">
|
||||
<el-icon :size="24" color="#67c23a">
|
||||
<component :is="useRenderIcon(UserIcon)" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex -space-x-2 mt-3">
|
||||
<el-avatar
|
||||
v-for="member in memberList.slice(0, 4)"
|
||||
:key="member.id"
|
||||
:size="28"
|
||||
:src="
|
||||
member.avatar ||
|
||||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.id}`
|
||||
"
|
||||
:title="getRoleText(member.roleCode)"
|
||||
/>
|
||||
<el-avatar
|
||||
v-if="memberList.length > 4"
|
||||
:size="28"
|
||||
class="bg-gray-200"
|
||||
>
|
||||
+{{ memberList.length - 4 }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="flex-bc">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">风险等级</p>
|
||||
<p class="text-2xl font-bold mt-1 text-orange-500">
|
||||
{{ projectInfo.riskText }}
|
||||
</p>
|
||||
<p class="text-xs text-orange-400 mt-1">
|
||||
<component :is="useRenderIcon(AlertIcon)" class="inline" />
|
||||
{{ projectInfo.riskCount }}个潜在风险需要关注
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon bg-orange-100">
|
||||
<el-icon :size="24" color="#e6a23c">
|
||||
<component :is="useRenderIcon(AlertIcon)" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 主要内容区 -->
|
||||
<el-row :gutter="16">
|
||||
<!-- 左侧:甘特图和物料清单 -->
|
||||
<el-col :xs="24" :lg="16">
|
||||
<!-- 甘特图 -->
|
||||
<el-card shadow="hover" class="mb-4">
|
||||
<template #header>
|
||||
<div class="flex-bc">
|
||||
<span class="font-medium">项目进度甘特图</span>
|
||||
<div class="flex gap-2">
|
||||
<el-button link @click="fetchGanttData">
|
||||
<component :is="useRenderIcon(RefreshIcon)" />
|
||||
</el-button>
|
||||
<el-button link>
|
||||
<component :is="useRenderIcon(FullscreenIcon)" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="ganttLoading" class="gantt-container">
|
||||
<!-- 图例 -->
|
||||
<div class="flex gap-4 mb-4 text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-4 h-3 bg-blue-400 rounded-sm" />
|
||||
<span>计划进度</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-4 h-3 bg-green-400 rounded-sm" />
|
||||
<span>实际进度</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 甘特图主体 -->
|
||||
<div class="gantt-chart">
|
||||
<!-- 时间轴 -->
|
||||
<div class="gantt-timeline">
|
||||
<div
|
||||
v-for="month in 6"
|
||||
:key="month"
|
||||
class="timeline-item"
|
||||
:style="{ left: `${(month - 1) * (100 / 6)}%` }"
|
||||
>
|
||||
{{
|
||||
dayjs(projectInfo.startDate)
|
||||
.add(month - 1, "month")
|
||||
.format("YYYY-MM")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="gantt-tasks">
|
||||
<div
|
||||
v-for="task in taskList.slice().reverse()"
|
||||
:key="task.id"
|
||||
class="gantt-task-row"
|
||||
>
|
||||
<div class="task-name">{{ task.taskName }}</div>
|
||||
<div class="task-bar-container">
|
||||
<div
|
||||
class="task-bar"
|
||||
:style="getTaskBarStyle(task)"
|
||||
:class="{ completed: task.progress === 100 }"
|
||||
>
|
||||
<div
|
||||
class="task-progress"
|
||||
:style="{ width: `${task.progress || 0}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 物料清单 -->
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="flex-bc">
|
||||
<div class="flex items-center gap-2">
|
||||
<component :is="useRenderIcon(FileListIcon)" />
|
||||
<span class="font-medium">项目物料清单</span>
|
||||
</div>
|
||||
<el-button link type="primary">
|
||||
查看全部
|
||||
<component :is="useRenderIcon(ArrowRightIcon)" />
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="resourceList" style="width: 100%">
|
||||
<el-table-column label="资源名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-avatar :size="32" shape="square" class="bg-gray-100">
|
||||
<component :is="useRenderIcon('ri/box-line')" />
|
||||
</el-avatar>
|
||||
<div>
|
||||
<div class="font-medium">{{ row.resourceName }}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{{ getResourceTypeText(row.resourceType) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" prop="specification" width="120" />
|
||||
<el-table-column label="数量" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.planQuantity }} {{ row.unit }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getResourceStatusType(row.status)" size="small">
|
||||
{{ getResourceStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计到货" width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ row.planArriveDate || "--" }}</span>
|
||||
<el-icon
|
||||
v-if="row.status === 'arrived' || row.status === 'actual'"
|
||||
color="#67c23a"
|
||||
class="ml-1"
|
||||
>
|
||||
<component :is="useRenderIcon(CheckIcon)" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<template #default>
|
||||
<el-button link type="primary" size="small">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:AI助手 -->
|
||||
<el-col :xs="24" :lg="8">
|
||||
<el-card shadow="hover" class="ai-card">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon :size="20" color="#409eff">
|
||||
<component :is="useRenderIcon(RobotIcon)" />
|
||||
</el-icon>
|
||||
<span class="font-medium">项目AI助手</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">分析项目进度和流程卡点</p>
|
||||
</template>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="ai-messages">
|
||||
<div
|
||||
v-for="(msg, index) in aiMessages"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="msg.type"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<el-avatar
|
||||
v-if="msg.type === 'ai'"
|
||||
:size="32"
|
||||
class="bg-blue-500"
|
||||
>
|
||||
<component :is="useRenderIcon(RobotIcon)" />
|
||||
</el-avatar>
|
||||
<el-avatar
|
||||
v-else
|
||||
:size="32"
|
||||
:src="'https://api.dicebear.com/7.x/avataaars/svg?seed=user'"
|
||||
/>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-bubble" v-html="msg.content" />
|
||||
<div class="message-time">{{ msg.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷问题 -->
|
||||
<div class="quick-questions">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
aiInput = '帮我分析一下结构施工阶段的流程卡点';
|
||||
sendAiMessage();
|
||||
"
|
||||
>
|
||||
帮我分析一下结构施工阶段的流程卡点
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="ai-input-area">
|
||||
<el-input
|
||||
v-model="aiInput"
|
||||
placeholder="请输入您的问题..."
|
||||
@keyup.enter="sendAiMessage"
|
||||
>
|
||||
<template #append>
|
||||
<el-button type="primary" @click="sendAiMessage">
|
||||
发送
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.project-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 甘特图样式
|
||||
.gantt-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.gantt-chart {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gantt-timeline {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.timeline-item {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.gantt-tasks {
|
||||
.gantt-task-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.task-bar-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.task-bar {
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
background-color: #e6f2ff;
|
||||
border: 1px solid #409eff;
|
||||
border-radius: 4px;
|
||||
|
||||
&.completed {
|
||||
background-color: #e6f7e6;
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
.task-progress {
|
||||
height: 100%;
|
||||
background-color: #409eff;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
.completed & {
|
||||
background-color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI助手样式
|
||||
.ai-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 280px);
|
||||
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-messages {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.user {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
color: white;
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: break-word;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 12px;
|
||||
|
||||
:deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-questions {
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
|
||||
.el-button {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-input-area {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user