You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/AI/index.vue

1301 lines
44 KiB
Vue

2 months ago
<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2025-03-03 14:13:35
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-07 10:05:57
* @FilePath: \financial-big-data\src\components\Ai\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="index">
<div class="min-h-screen" ref="header">
<div class="title">
<h1>在这里 AI 智能学习直通就业</h1>
</div>
<div class="tz-tobs mb10">
<template v-for="(tbas, idx) in tabList" :key="tbas.name">
<div :style="{ background: `url(${getAssetsFile(`${idx + 1}.webp`)}) no-repeat`, backgroundSize: '100% 100%' }" :class="[tbas == tabactive ? 'el-tobs-item active' : ' el-tobs-item', { 'el-tobs-item-active': claActive == tbas.name }]" @click="changeTab(tbas)">
<span>{{ tbas.name }}</span>
</div>
</template>
</div>
</div>
<div class="tz-centent" ref="container">
<div class="tz-step" ref="step">
<div class="tz-stop-info">
<template v-for="item in tabactive.children" :key="item.title">
<div :class="['info-title', { 'info-title-active': stopActive == item.title }]" @click="changeStop(item.title)">{{ item.title }}</div>
</template>
</div>
<div class="tz-stop-centent">
<div class="stop">
<template v-for="(item, index) in tabactive.children" :key="index">
<template v-if="item.title == stopActive">
<div :class="['stop-item', { 'info-title-completedSteps': childs.isTemplateBased }, { 'info-title-completedSteps-disdabled': !isTaskEnabled(item.child, idx) }]" v-for="(childs, idx) in item.child" :key="childs" @click="onClick(childs, idx)">{{ childs.name }}</div>
</template>
</template>
</div>
<div class="fbtn fr" style="display: flex; gap: 20px">
<el-upload :auto-upload="false" class="avatar-uploader" :show-file-list="false" :data="domfile" :on-change="submitDocument" accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
<template #trigger>
<el-button type="primary" :disabled="!allTrue()">提交</el-button>
</template>
</el-upload>
<el-button type="success" @click="exportSession"></el-button>
</div>
</div>
</div>
<el-scrollbar :height="scrollbarHeight" ref="scrollArea">
<ul class="QAlist">
<li :class="[item.role == 'user' ? 'questionItem' : 'answerItem']" v-for="item in messages" :key="item" :data-name="item.name">
<div :class="[item.role == 'user' ? 'qustionCon' : 'answerCon']">
<div v-if="item.role !== 'user'" class="answerCon-img">
<img src="@/assets/images/ai.webp" alt="" />
</div>
<MdPreview :editorId="id" :modelValue="item.content" :codeFoldable="false" theme="dark" codeTheme="a11y" />
<div class="copy">
<el-tooltip class="box-item" effect="dark" content="复制" placement="top">
<svg-icon icon-class="copy" v-copyText="item.content" v-copyText:callback="copyTextSuccess" />
</el-tooltip>
</div>
</div>
</li>
<li v-if="isRequestPending">
<div style="display: flex; align-items: center">
<img src="@/assets/images/ai.webp" alt="" />
<div style="height: 68px" v-loading="isRequestPending" :element-loading-svg="svg"></div>
</div>
</li>
<li>
<div class="task-introduction" v-if="messages.length <= 0">
<div class="title mb10">
<span>{{ tabactive.introduce }}</span>
</div>
<p style="font-size: 1rem !important">提示词小技巧:</p>
<ul>
<li>提问时给出具体知识点技能点等数量要求:</li>
<li>每个点或者整体加上字数限制:</li>
<li>要求AI输出表格进行图表等可视化展示:</li>
<li>针对AI给出内容某一点继续详细追问进行深入学习了解:</li>
</ul>
</div>
</li>
</ul>
</el-scrollbar>
</div>
<div class="tz-footer-ipt" ref="footer">
<el-input type="textarea" placeholder="可以问我任何问题" v-model.trim="inputMessage" @keyup.enter="sendMessage" :autosize="{ minRows: 4.5, maxRows: 3 }" />
<svg-icon icon-class="" class="fr" v-if="isTypingComplete" @click="sendMessage" :class="{ disdabled: !inputMessage }" />
<svg-icon icon-class="" class="fr" @click="gInterrupt" v-else />
</div>
</div>
</template>
<script setup>
import { MdPreview, MdCatalog } from "md-editor-v3";
import { getAssetsFile } from "@/utils/index.js";
import * as AI from "@/api/AI.js";
import * as indexApi from "@/api/index";
2 months ago
import "md-editor-v3/lib/style.css";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const scrollbarHeight = ref("auto"); // 动态高度
const container = ref(null); // 父容器
const header = ref(null); // 头部
const step = ref(null); // 头部
const footer = ref(null); // 底部
let resizeObserver = null; // ResizeObserver 实例
const inputMessage = ref("");
const isRequestPending = ref(false);
const isTypingComplete = ref(true);
const scrollArea = ref(null);
const tabList = ref([
{
name: "知识画布",
cor: "#55c5c3",
introduce: "本模块分为理论知识、实务知识、未来展望三个模块,每个模块分为五个步骤,学生根据相应步骤提示词,了解实训系统相关理论知识重点难点及学习建议,学习实训系统相关业务实务知识提作流程,相关单据协议及业务风险等,最后了解业务现状及理论研究,新技术应用等情况,了解未来发展趋势",
children: [
{
title: "理论知识",
child: [
{ name: "课程知识点", com: "请梳理xxx课程相关知识点每个知识点解释字数50字以内" },
{ name: "核心知识点", com: "请梳理xxx课程最核心的5个模块每个知识点解释字数50字以内" },
{ name: "最难的部分", com: "请指出xxx课程最难的1个模块并解释原因字数300字以内" },
{ name: "难点详解", com: "请详细解释xxx字数800字以内" },
{ name: "学习建议", com: "请给我一些xxx课程的学习建议字数300字以内" },
],
},
{
title: "实务知识",
child: [
{
name: "实务知识概述",
com: "xxx相关实务知识有哪些",
},
{
name: "业务操作流程",
com: "xxx业务在实务中是怎么操作的",
},
{
name: "相关单据协议",
com: "xxx业务在实操中会用到哪些合同协议文件",
},
{
name: "业务风险管理",
com: "xxx业务有哪些风险如何防范",
},
{
name: "流程优化建议",
com: "xxx业务流程可以进行优化以提高效率吗请给出优化建议",
},
],
},
{
title: "未来展望",
child: [
{
name: "业务现状",
com: "xxx国际和国内发展现状",
},
{
name: "理论研究",
com: "xxx相关理论有哪些研究现状如何是否有理论研究突破",
},
{
name: "新技术应用",
com: "xxx业务应用了哪些新技术比如人工智能、大数据、物联网等新技术",
},
{
name: "技术应用详解",
com: "xxx技术在xxx业务中是如何应用的有哪些应用案例",
},
{
name: "发展趋势",
com: "xxx发展趋势如何",
},
],
},
],
},
{
name: "行业企业",
cor: "#538af7",
introduce: "本模块分为就业行业分析、企业对比、企业调研三个模块,每个模块分为五个步骤。学生根据相应步骤提示词,了解目标行业的发展现状、就业趋势及核心技能要求,掌握不同企业的业务模式、文化特点及竞争优势,并通过调研工具和方法,深入分析企业的组织架构、岗位需求及职业发展路径。最后,学生将结合行业动态和企业实践,形成对就业市场的全面认知,为职业规划提供科学依据。",
children: [
{
title: "就业行业",
child: [
{
name: "行业类型",
com: "xxx相关的就业行业类型主要有哪些列出xx个行业",
},
{
name: "行业发展状况",
com: "xxx行业发展状况如何",
},
{
name: "行业对比分析",
com: "请比较xx行业和xx行业分析行业相关优劣及发展前景输出表格",
},
{
name: "最佳客户化",
com: "请从行业特点及前景出发客观推荐一个xx相关行业",
},
{
name: "行业详细分析",
com: "请详细分析xx行业可以从多个方面进行分析",
},
],
},
{
title: "企业对比",
child: [
{
name: "企业规模",
com: "请对比前述相关xx家企业规模并按企业规模大小排序",
},
{
name: "企业所在地",
com: "请根据前述xx家企业所在地分析相关企业地域集聚现象",
},
{
name: "企业盈利",
com: "请根据企业规模对比分析xx家企业盈利能力并根据盈利能力进行排序",
},
{
name: "企业福利",
com: "请对比分析前述xx家企业福利状况并根据福利状况进行排序",
},
{
name: "招聘情况",
com: "请对比分析前述xx家企业招聘情况哪些企业招聘量较大",
},
],
},
{
title: "企业调研",
child: [
{
name: "行业相关企业",
com: "xxx行业相关企业有哪些请列出xx家",
},
{
name: "企业发展状况",
com: "xx企业发展历程及现状",
},
{
name: "企业财务状况",
com: "xx企业财务状况如何盈利能力如何财务风险如何",
},
{
name: "企业面临风险",
com: "xx企业面临哪些风险从政策、区域、行业、企业等多层面多角度分析",
},
{
name: "就业企业建议",
com: "上述相关企业推荐哪些企业作为就业企业?最佳荐哪家企业?",
},
],
},
],
},
{
name: "岗位类型",
cor: "#eabd42",
introduce: "本模块分为岗位类型、岗位发展、推荐岗位三个模块,每个模块分为五个步骤。学生根据相应步骤提示词,了解不同岗位的职责要求、技能需求及工作特点,掌握岗位的职业发展路径及晋升机会,并通过分析行业趋势和个人兴趣,获得适合自己的岗位推荐及发展建议。最后,学生将结合岗位需求和自身优势,制定清晰的职业规划,为未来的就业选择提供科学指导。",
children: [
{
title: "岗位类型",
child: [
{
name: "岗位类型",
com: "xx相关的岗位类型有哪些请列举出xx个",
},
{
name: "类型详解",
com: "请详细解释xx类岗位",
},
{
name: "工作内容",
com: "xx类岗位工作内容有哪些",
},
{
name: "人员画像",
com: "xx类岗位工作人员画像哪些人适合从事xx类岗位",
},
{
name: "推荐适合类型",
com: "我是一个xxx样的人请为我推荐合适的岗位类型",
},
],
},
{
title: "岗位发展",
child: [
{
name: "基层岗位",
com: "xxx相关基层岗位",
},
{
name: "中层岗位",
com: "xx相关中层岗位",
},
{
name: "高层岗位",
com: "xx相关高层岗位",
},
{
name: "岗位现状",
com: "xx岗位现状",
},
{
name: "岗位前景",
com: "xx岗位发展前景",
},
],
},
{
title: "推荐岗位",
child: [
{
name: "具体岗位",
com: "xxx相关岗位有哪些请列出xx个岗位",
},
{
name: "岗位对比",
com: "请列出以上几个岗位,分析岗位优缺点,输出表格",
},
{
name: "岗位晋升路径",
com: "xx岗位晋升路径是怎样的",
},
{
name: "岗位可替代性",
com: "随着社会发展xx岗位是否容易被替代",
},
{
name: "推荐合适岗位",
com: "简单描述个人信息及性格特点让AI推荐合适岗位",
},
],
},
],
},
{
name: "技能要求",
cor: "#f1858b",
children: [
{
title: "岗位技能",
child: [
{
name: "岗位技能",
com: "XX类岗位技能有哪些请列出xx种技能",
},
{
name: "理论支撑",
com: "XX技能需要哪些理论知识支撑需要学习哪些理论课程",
},
{
name: "实训实习",
com: "可以通过哪些实训或者实习来学习相关技能?",
},
{
name: "感兴趣技能",
com: "我对XX技能感兴趣请详细解释如何学习该技能",
},
{
name: "实习推荐",
com: "如果想加强学习XX技能可以推荐相关企业实习吗请列出XX家企业",
},
],
},
{
title: "技能发展",
child: [
{
name: "技能发展路径",
com: "XXX技能发展路径",
},
{
name: "初级技能",
com: "XX相关初级技能",
},
{
name: "中级技能",
com: "XXX相关中级技能",
},
{
name: "高级技能",
com: "XXX相关高级技能",
},
{
name: "发展前景",
com: "XXX相关技能发展前景",
},
],
},
{
title: "技能学习",
child: [
{
name: "岗位核心技能",
com: "XXX岗位核心技能有哪些列出x种",
},
{
name: "核心技能详解",
com: "请详细解释上述xx技能",
},
{
name: "学习渠道推荐",
com: "请推荐学习XX技能的渠道免费的和收费的各列",
},
{
name: "学习方法",
com: "请推荐XX技能的学习方法帮助制定学习计划",
},
{
name: "学习建议",
com: "请给我一些XX技能学习建议",
},
],
},
],
},
{
name: "薪资水平",
cor: "#4da3f0",
introduce: "本模块分为地域薪资、企业薪资、岗位薪资三个模块,每个模块分为五个步骤。学生根据相应步骤提示词,了解不同地区的薪资水平及生活成本,掌握不同类型企业的薪资结构及福利待遇,并通过分析岗位的薪资差异及增长潜力,获得科学的薪资预期及职业选择建议。最后,学生将结合地域、企业及岗位的薪资数据,制定合理的职业发展目标。",
children: [
{
title: "地域新资",
child: [
{
name: "各地新资概况",
com: "我国各大主要城市xx行业新资水平",
},
{
name: "各地新资对比",
com: "请对比上述城市新资水平并进行排序,输出表格",
},
{
name: "意向地新资",
com: "我想在xx城市工作请根据该城市xx相关行业新资水平",
},
{
name: "意向地新资",
com: "xx城市针对大学生就业有何帮扶政策及福利",
},
{
name: "地点推荐",
com: "请根据当地新资水平和福利政策,客观推荐工作地点",
},
],
},
{
title: "企业新资",
child: [
{
name: "各企业新资",
com: "xx相关行业企业新资水平如何请列出xx家企业新资水平",
},
{
name: "新资对比分析",
com: "请对比分析上述企业新资福利情况,并进行排序,输出表格",
},
{
name: "意向企业新资",
com: "xx企业新资水平如何该企业各岗位新资水平如何输出表格",
},
{
name: "意向企业福利",
com: "xx企业整体福利状况如何各类岗位福利状况如何输出表格",
},
{
name: "企业推荐",
com: "请根据企业新资福利状况客观推荐xx家适合就业的企业",
},
],
},
{
title: "岗位新资",
child: [
{
name: "岗位新资对比",
com: "xxx相关岗位新资对比情况输出表格",
},
{
name: "初级岗位新资",
com: "xxx相关初级岗位新资水平列出x个岗位输出表格",
},
{
name: "中级岗位新资",
com: "xxx相关中级岗位新资水平列出x个岗位输出表格",
},
{
name: "高级岗位新资",
com: "xxx相关高级岗位新资水平列出x个岗位输出表格",
},
{
name: "适合岗位",
com: "针对刚毕业的xx学校大学生期望新资是xxx到xxx喜欢xx不喜欢xx请推荐合适的岗位",
},
],
},
],
},
{
name: "简历优化",
cor: "#56c8c5",
introduce: "本模块分为个人信息、简历撰写、个人技能三个模块,每个模块分为五个步骤。学生根据相应步骤提示词,了解如何系统化整理个人信息,掌握简历撰写的核心技巧与注意事项,并通过针对性的技能提升建议,增强个人竞争力。最后,学生将结合个人信息、简历优化及技能提升,打造一份高质量的求职材料,为成功就业奠定坚实基础。",
children: [
{
title: "个人信息",
child: [
{
name: "基础信息",
com: "年龄、性别、籍贯、所在地等信息",
},
{
name: "教育信息",
com: "毕业院校、专业、所学课程等信息",
},
{
name: "性格特点",
com: "个人性格特点概述,例如外向、开朗、擅长交际、稳重、谨慎等外界评价",
},
{
name: "自我评价",
com: "个人自我评价,个人兴趣爱好等自我认知",
},
{
name: "自我期待",
com: "未来希望在哪个方向发展,期待自己成为一个什么样的人",
},
],
},
{
title: "简历撰写",
child: [
{
name: "简历模板",
com: "上传自己撰写的简历初稿",
},
{
name: "针对企业",
com: "想要给xx企业投简历请给出修改建议",
},
{
name: "针对岗位类型",
com: "意向岗位类型是xx想要针对xx岗位请给出修改建议",
},
{
name: "AI版本简历",
com: "继续补充相关信息,个性化进行简历修改",
},
{
name: "终稿导出",
com: "导出修改后的简历终稿",
},
],
},
{
title: "个人技能",
child: [
{
name: "知识技能",
com: "掌握的实务性较强的知识技能学会的一些工具如Python等",
},
{
name: "技能证书",
com: "考取的相关职业资格证,各类技能证书等",
},
{
name: "实验实训",
com: "开展的哪些技能实训",
},
{
name: "企业实习",
com: "是否有企业实习经历",
},
{
name: "技能发展",
com: "未来想在哪方面继续学习,精进技能",
},
],
},
],
},
]);
const claActive = ref(tabList.value[0].name);
const tabactive = ref(tabList.value[0]);
const stopActive = ref("理论知识");
const currentNode = ref(null);
const taskinfo = ref({});
let abortController = null;
const messages = ref([]);
const domfile = ref(null);
// **计算每个任务是否可点击**
const isTaskEnabled = (list, index) => {
if (index === 0) return true; // 第一个任务默认可点击
console.log(list[index - 1].isTemplateBased, "list[index - 1].isTemplateBased");
return list[index - 1].isTemplateBased ?? false;
};
// 发送消息
const sendMessage = async () => {
if (!inputMessage.value.trim() || isRequestPending.value) return proxy.$message.warning("请输入内容");
isRequestPending.value = true;
isTypingComplete.value = true; // 重置为初始状态
let isTemplateBased = false;
if (currentNode.value) {
isTemplateBased = isBasedOnCom(inputMessage.value, currentNode.value.com);
if (!isTemplateBased) {
isRequestPending.value = false;
isTypingComplete.value = false; // 重置为初始状态
} else {
isRequestPending.value = true;
isTypingComplete.value = true; // 重置为初始状态
}
}
// 重置状态
// 添加用户消息
messages.value.push({
id: Date.now(),
role: "user",
content: inputMessage.value,
module: claActive.value,
stop: stopActive.value,
isTemplateBased: isTemplateBased,
name: currentNode.value?.name ?? "",
});
// 用户提问后立即滚动到底部
nextTick(() => {
scrollToBottom();
inputMessage.value = "";
});
abortController = new AbortController();
const signal = abortController.signal;
// 调用后端API
let params = {
systemOwner: "电商互联网产品",
userId: userStore.userInfo.userId,
type: "2",
};
isTypingComplete.value = false;
// 将参数对象转换为查询字符串
const queryString = new URLSearchParams(params);
const aiBase = (import.meta.env.VITE_APP_AI_API || "/ai/").replace(/\/?$/, "/");
const resp = await fetch(`${aiBase}api/kimi4?${queryString}`, {
2 months ago
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: inputMessage.value.trim(),
signal,
});
// 添加AI消息占位
let newMessage = reactive({ id: Date.now(), role: "assistant", content: "", module: claActive.value, stop: stopActive.value, name: currentNode.value?.name ?? "" });
messages.value.push(newMessage); // 滚动到底部
// 处理流式响应
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { done, value } = await reader.read();
isRequestPending.value = false;
if (done) break;
const text = decoder.decode(value, { stream: true });
await typeText(newMessage, text);
//滚动到最新消息
nextTick(() => {
const scrollEl = scrollArea.value.wrapRef;
scrollEl.scrollTop = scrollEl.scrollHeight;
});
}
};
const isBasedOnCom = (input, com) => {
// 1. 将 'xxx' 替换为匹配任意字符(包括换行)
const comPattern = com.replace(/xxx/g, "([\\s\\S]+)");
// 2. 构造正则,确保其严格匹配其他部分
const regex = new RegExp(`^${comPattern}$`);
// 3. 测试用户输入是否符合模式
return regex.test(input);
};
const typeText = (message, text) => {
return new Promise((resolve) => {
isTypingComplete.value = false; // 重置状态
let index = 0;
message.content = ""; // 清空原内容
const textLength = text.length;
const chunkSize = textLength > 100 ? 3 : 1; // 根据文本长度动态调整批量字数
const typingSpeed = textLength > 100 ? 30 : 60; // 适配长短文本
const interval = setInterval(() => {
if (index < text.length) {
message.content += text.slice(index, index + chunkSize);
index += chunkSize;
if (index % 10 === 0) nextTick(() => scrollToBottom()); // 限制滚动更新频率
} else {
clearInterval(interval);
isTypingComplete.value = true;
AI.submitTaskStep({
...taskinfo.value,
contentOne: JSON.stringify(messages.value),
}).then((res) => {
createPracticalTraining();
});
nextTick(() => scrollToBottom());
resolve();
}
}, typingSpeed);
});
};
// 滚动到底部的函数
const scrollToBottom = () => {
const scrollEl = scrollArea.value.wrapRef;
if (scrollEl) {
scrollEl.scrollTop = scrollEl.scrollHeight;
}
};
const scrollToElement = async (dataName) => {
await nextTick(); // 确保 DOM 更新完成
const scrollContainer = scrollArea.value?.wrapRef; // 获取 el-scrollbar 内部滚动容器
const targetElement = scrollContainer?.querySelector(`[data-name="${dataName}"]`);
if (targetElement) {
const top = targetElement.offsetTop; // 计算元素距离父容器顶部的偏移量
scrollContainer.scrollTo({ top, behavior: "smooth" }); // 平滑滚动
} else {
console.error(`未找到 data-name="${dataName}" 的元素`);
}
};
// 中断对话
const gInterrupt = () => {
if (abortController) {
abortController.abort();
abortController = null;
isRequestPending.value = false;
isTypingComplete.value = true;
}
};
const onClick = (type, idx) => {
const element = document.querySelectorAll(".stop-item"); // 选择你的元素
if (element && element[idx].classList.contains("info-title-completedSteps-disdabled")) return (inputMessage.value = "");
if (!type?.isTemplateBased) {
let arr = ["课程知识点", "核心知识点", "最难的部分", "实务知识概述", "学习建议", "业务现状", "理论研究", "发展趋势"];
if (arr.includes(type.name)) {
inputMessage.value = type.com.replace("xxx", "电商互联网产品开发实训系统");
} else {
inputMessage.value = type.com;
}
}
currentNode.value = type; //记录当前步骤节点
scrollToElement(type.name);
};
const changeTab = (type) => {
tabactive.value = type;
claActive.value = type.name;
stopActive.value = type.children[0].title;
createPracticalTraining();
};
const copyTextSuccess = () => {
proxy.$modal.msgSuccess("复制成功");
};
const getCurrentTeachingClassId = async () => {
const cachedClassId = proxy.$cache.session.get("currentTeachingClassId");
if (cachedClassId) return cachedClassId;
const res = await indexApi.getCurrentTeachingClass({ userId: userStore.userInfo.userId });
const classId = res?.data?.schoolClassId || "";
if (classId) {
proxy.$cache.session.set("currentTeachingClassId", classId);
}
return classId;
};
2 months ago
// 选择步骤
const changeStop = (stop) => {
stopActive.value = stop;
setTimeout(() => {
createPracticalTraining();
}, 0);
};
// 创建任务
const createPracticalTraining = async () => {
await nextTick(); // 等待 Vue 状态更新完成
const currentClassId = await getCurrentTeachingClassId();
if (!currentClassId) return;
2 months ago
const requestData = {
systemOwner: "区块链金融",
userId: userStore.userInfo.userId,
classId: currentClassId,
2 months ago
schoolId: userStore.userInfo.schoolId,
module: claActive.value,
step: stopActive.value, // 确保这里传递的是最新的值
};
AI.createTask(requestData).then((res) => {
if (res) {
taskinfo.value = res.data;
messages.value = res.data.contentOne ? JSON.parse(res.data.contentOne) : [];
setTimeout(() => {
syncBackendStatus(messages.value);
}, 0);
}
});
};
// 同步后端状态到前端 tabList
const syncBackendStatus = (backendMessages) => {
// 分离 user 和 assistant 消息
const userMessages = backendMessages.filter((message) => message.role === "user" && message.isTemplateBased === true);
const assistantMessages = backendMessages.filter((message) => message.role === "assistant");
// 遍历 user 消息
userMessages.forEach((userMessage) => {
const { module, stop, name } = userMessage;
// 查找对应的 assistant 消息,要求 name、module、stop 一致且 content 不为空
const matchingAssistant = assistantMessages.find((assistant) => assistant.module === module && assistant.stop === stop && assistant.name === name && assistant.content && assistant.content.trim() !== "");
// 如果没有匹配的 assistant 消息或 content 为空,直接返回
if (!matchingAssistant) return;
// 查找对应的选项卡
const tab = tabList.value.find((tab) => tab.name === module);
if (!tab) return;
// 查找对应的任务
const task = tab.children.find((child) => child.title === stop);
if (!task) return;
// 查找对应的步骤
const step = task.child.find((child) => child.name === name);
if (step) {
// 只有当 assistant 消息匹配且 content 不为空时,设置为 true
step.isTemplateBased = true;
// console.log('同步后的步骤:', step);
}
});
};
// 会话记录导出
const exportSession = (id) => {
AI.exportSession({
missionModuleId: taskinfo.value.missionModuleId,
}).then((res) => {
// 创建 Blob 对象
const blob = new Blob([res], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
// 创建下载链接
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${stopActive.value}会话数据.xlsx`; // 这里可以自定义文件名
document.body.appendChild(link);
// 触发下载
link.click();
// 清理
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
};
const submitDocument = (val) => {
domfile.value = val.raw;
const formData = new FormData();
formData.append("file", domfile.value);
formData.append("missionModuleId", taskinfo.value.missionModuleId);
AI.uploadResourceFile(formData).then((res) => {
AI.submitTask({
...taskinfo.value,
uploadUrl: res.data,
}).then((res) => {
createPracticalTraining();
proxy.$modal.msgSuccess("提交成功");
});
});
};
// 判断数组中所有元素是否都是 true且数组不为空
const allTrue = () => {
// 根据stopActive和claActive的值筛选tabList中的数据
const filteredTabList = tabList.value.filter((tab) => tab.name === claActive.value);
const filteredTasks = filteredTabList[0].children.filter((task) => task.title === stopActive.value);
const steps = filteredTasks[0].child;
return steps.every((step) => step.isTemplateBased);
};
// 计算高度的函数
const updateHeight = () => {
const windowHeight = window.innerHeight; // 视口高度
const headerHeight = header.value?.offsetHeight || 0; // 头部高度
const footerHeight = footer.value?.offsetHeight || 0; // 底部高度
const stepHeight = step.value?.offsetHeight || 0; // 底部高度
const totalOffset = headerHeight + footerHeight + stepHeight; // 总偏移量
scrollbarHeight.value = `${windowHeight - totalOffset - 150}px`;
};
onMounted(() => {
createPracticalTraining();
updateHeight();
// 监听窗口大小变化
window.addEventListener("resize", updateHeight);
// 使用 ResizeObserver 监听头部和底部的尺寸变化
resizeObserver = new ResizeObserver(() => {
updateHeight(); // 元素尺寸变化时重新计算
});
if (header.value) resizeObserver.observe(header.value);
if (footer.value) resizeObserver.observe(footer.value);
});
// 清理监听
onUnmounted(() => {
window.removeEventListener("resize", updateHeight);
if (resizeObserver) {
resizeObserver.disconnect();
}
});
</script>
<style lang="scss">
.index {
padding: 20px 40px;
height: calc(100vh - 2.5rem);
position: relative;
background: url("@/assets/images/背景.webp") no-repeat;
background-size: 100% 100%;
.min-h-screen {
text-align: center;
.title {
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
margin-top: 0px;
color: #ffffff;
font-weight: 600;
}
}
}
.tz-tobs {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-gap: 1rem;
margin-top: 1rem;
width: calc(100vw - 50rem);
margin: 0 auto;
margin-bottom: 10px;
.el-tobs-item {
position: relative;
height: 3rem;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
cursor: pointer;
color: #ffffff;
font-weight: normal;
// 超出
overflow: hidden;
.svg-icon {
width: 2rem;
height: 2rem;
}
.el-tobs-item-active {
.svg-icon {
// 翻转180度
transform: rotate(0deg);
fill: #19578b;
}
}
}
.active {
//放大
transform: scale(1.12);
font-size: 18px;
font-weight: 600;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
color: #ffffff;
}
.el-tobs-item-active {
.svg-icon {
// 翻转180度
transform: rotate(0deg);
}
.svg-icon {
transform: rotate(180deg);
}
&:hover {
transition: all 0.2s;
background-color: #eff6ff;
}
}
.el-tobs-item-active {
.svg-icon {
// 翻转180度
transform: rotate(0deg);
fill: #19578b;
}
}
.active {
background: #eff6ff;
border-color: rgb(191 219 254);
}
}
.tz-centent {
width: calc(100vw - 50rem);
height: calc(100vh - 25rem);
margin: 0 auto;
.QAlist {
padding-left: 0px;
li {
display: flex !important;
overflow: hidden;
margin-bottom: 12px;
:deep(.el-loading-mask) {
background: transparent !important;
}
}
.task-introduction {
color: #ffffff;
.title {
font-size: 1.2rem;
}
// p除了第一个
p:not(:first-child) {
margin-top: 1rem;
font-size: 0.8rem !important;
}
ul {
margin: 0;
padding: 0;
width: 30rem;
li {
margin-top: 10px;
background: #011031;
border: 1px solid #003e7c;
border-radius: 12px 12px 12px 0;
padding: 10px 12px 10px 12px;
font-size: 14px;
color: #ffffff;
// 鼠标小手
cursor: pointer;
line-height: 22px;
&:hover {
background: #002c5b;
}
}
}
}
:deep(.md-editor-preview-wrapper) {
padding: 0px 0px !important;
.md-editor-preview {
font-size: 15px !important;
}
}
.questionItem {
// display: flex;
justify-content: flex-end;
.qustionCon {
background: #ffffff;
border-radius: 12px 12px 0 12px;
// padding: 0px 12px 0px 16px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
max-width: calc(100% - 51px);
word-break: break-all;
}
}
.answerItem {
display: flex;
margin-bottom: 12px;
overflow: hidden;
:deep(.answerCon) {
display: flex;
font-weight: 400;
font-size: 14px;
// color: rgb(68, 68, 68);
line-height: 22px;
// width: calc(100% - 51px);
word-break: break-all;
flex: 1 1 0%;
padding: 0px 16px 0px 16px;
}
}
margin-top: 10px;
}
.tz-step {
margin-top: 2rem;
width: 100%;
height: 8rem;
display: flex;
gap: 2rem;
.tz-stop-info {
width: calc(100% - 90%);
overflow: hidden;
.info-title {
font-size: 1rem;
font-weight: 600;
color: #ffffff;
background: #19578b;
text-emphasis: none;
text-align: center;
padding: 0.5rem 0px;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
width: 100%;
// 鼠标小手
cursor: pointer;
&:hover {
background: #2d89e3;
transition: all 0.3s;
}
}
.info-title-active {
background: #2d89e3;
transition: all 0.3s;
}
}
.tz-stop-centent {
flex: 1;
background: #002c5b;
border-radius: 0.5rem;
min-width: calc(100% - 20%); /* 确保宽度自适应 */
overflow: hidden; /* 防止内容溢出 */
// padding-left: 5rem;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 4rem 0 4rem;
.stop {
flex: 1; /* 让 .stop 容器占据剩余空间 */
height: 100%;
display: flex;
align-items: center;
gap: 2rem;
.stop-item {
width: 6rem;
height: 3rem;
line-height: 3rem;
text-align: center;
border-radius: 0.4rem;
background: #06233f;
font-size: 0.9rem;
color: #ffffff;
position: relative;
cursor: pointer;
}
.stop-item:not(:last-child) {
&::after {
content: "→";
position: absolute;
top: 0;
right: -1.8rem;
display: block;
color: red;
font-size: 23px;
}
}
.info-title-completedSteps {
background: #2378cb;
}
.info-title-completedSteps:not(:last-child) {
&::after {
content: "→";
position: absolute;
top: 0;
right: -1.8rem;
display: block;
color: #2378cb !important;
font-size: 23px;
}
}
.info-title-completedSteps-disdabled {
background: #d6dee8;
cursor: not-allowed;
}
}
.fbtn {
.el-button {
height: 3rem;
}
}
}
}
}
.tz-footer-ipt {
position: absolute;
left: 50%;
bottom: 20px;
transform: translateX(-50%);
width: calc(100vw - 50rem);
height: 11rem;
background: #001d40;
margin: 0 auto;
border-radius: 35px;
padding: 10px;
border: 1px solid #003e7c;
.svg-icon {
width: 3rem;
height: 2.7rem;
// 鼠标小手
cursor: pointer;
}
.disdabled {
// 禁用
cursor: not-allowed;
.svg-icon {
fill: #d6dee8;
}
}
.el-textarea {
.el-textarea__inner {
background: transparent !important;
box-shadow: none !important;
// 光标颜色
border-radius: 0px;
color: #ffffff;
&::placeholder {
color: #ffffff;
}
}
}
}
}
.qustionCon {
.md-editor {
--md-color: #333333 !important;
--md-bk-color: transparent !important;
}
}
.answerCon {
display: flex;
gap: 1rem;
position: relative;
padding-right: 10px;
.copy {
position: absolute;
top: 5px;
right: 12px;
display: none;
.svg-icon {
fill: #ffffff;
width: 1.5rem;
height: 1.5rem;
}
}
&:hover {
.copy {
display: block;
cursor: pointer;
}
}
.md-editor {
background: #011031;
border-radius: 10px;
border: 1px solid #003e7c;
--md-color: #ffffff !important;
--md-bk-color: #02132f !important;
}
}
.el-loading-mask {
background: transparent !important;
.el-loading-spinner {
text-align: left;
padding-left: 2.5rem;
}
}
.qustionCon {
position: relative;
padding-left: 10px;
.copy {
position: absolute;
top: 5px;
left: 8px;
display: none;
.svg-icon {
fill: #333333;
width: 1.5rem;
height: 1.5rem;
}
}
&:hover {
.copy {
display: block;
cursor: pointer;
}
}
}
.el-scrollbar {
--el-scrollbar-bg-color: transparent !important;
}
// 媒体查询监听视口小于1920
@media screen and (max-width: 1700px) {
.el-textarea {
height: 2rem !important;
.el-textarea__inner {
font-size: 0.8rem;
}
}
.tz-step {
.tz-stop-info {
display: none;
}
.tz-stop-centent {
padding: 0px 1rem 0 1rem !important;
.stop-item {
width: 4rem !important;
font-size: 0.7rem !important;
}
}
}
.tz-footer-ipt {
height: 6rem !important;
}
}
</style>