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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @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";
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}`, {
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;
};
// 选择步骤
const changeStop = (stop) => {
stopActive.value = stop;
setTimeout(() => {
createPracticalTraining();
}, 0);
};
// 创建任务
const createPracticalTraining = async () => {
await nextTick(); // 等待 Vue 状态更新完成
const currentClassId = await getCurrentTeachingClassId();
if (!currentClassId) return;
const requestData = {
systemOwner: "区块链金融",
userId: userStore.userInfo.userId,
classId: currentClassId,
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: var(--student-fs-h1);
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: var(--student-fs-body);
color: #ffffff;
//
cursor: pointer;
line-height: 22px;
&:hover {
background: #002c5b;
}
}
}
}
:deep(.md-editor-preview-wrapper) {
padding: 0px 0px !important;
.md-editor-preview {
font-size: var(--student-fs-body) !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: var(--student-fs-body);
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: var(--student-fs-body);
// 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>