feat-实现AI洞察功能

This commit is contained in:
zhang.weiwei
2025-03-23 06:58:52 +08:00
parent 823287b054
commit 900aefc926
29 changed files with 493 additions and 40 deletions

View File

@@ -60,3 +60,4 @@ See [Configuration Reference](https://cli.vuejs.org/config/).
- keepAlive: 是否页面使用 keepAlive 缓存 - keepAlive: 是否页面使用 keepAlive 缓存
- shared: 是否页面为分享功能页面,不需要登录即可访问 - shared: 是否页面为分享功能页面,不需要登录即可访问
- showAiInspection: 问卷设计是否显示AI质检按钮 - showAiInspection: 问卷设计是否显示AI质检按钮
- showAiInsight: 数据分析是否显示AI洞察按钮

View File

@@ -26,12 +26,12 @@
<script> <script>
import locale from 'ant-design-vue/es/locale/zh_CN'; import locale from 'ant-design-vue/es/locale/zh_CN';
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import { watch, onMounted, ref } from 'vue'; import { watch, onMounted,onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import Loading from '@/components/layout/loading/Loading.vue'; import Loading from '@/components/layout/loading/Loading.vue';
import useEmitter from '@/composables/useEmitter'; import useEmitter from '@/composables/useEmitter';
import aiTaskManager from '@/utils/ai/ai-task-manager'; // 引入AI任务管理器
export default { export default {
components: { components: {
Loading Loading
@@ -76,13 +76,12 @@ export default {
// window.localStorage.plantId = data?.id // window.localStorage.plantId = data?.id
// window.localStorage.plantUserInfo = JSON.stringify(data) // window.localStorage.plantUserInfo = JSON.stringify(data)
// } // }
// onMounted(()=>{ onMounted(() => {
// router.beforeEach((to, from) => { aiTaskManager.runTasks();
// if(to.path!= "/answer"){ });
// getNewToken() onUnmounted(() => {
// } aiTaskManager.saveTasks();
// }) });
// })
const clickAction = (e) => { const clickAction = (e) => {
if (e.target.getAttribute('data-linkType') === '1') { if (e.target.getAttribute('data-linkType') === '1') {
show.value = true; show.value = true;

View File

@@ -26,11 +26,12 @@
></span> ></span>
</div> </div>
</div> </div>
<!-- add by zhangweiwei 20250331_ai AI 质检 start -->
<div class="right" v-if="showAiInspection || showPreview || isRenderBtn || showShare"> <div class="right" v-if="showAiInspection || showPreview || isRenderBtn || showShare">
<template v-if="!showLoading"> <template v-if="!showLoading">
<!-- add by zhangweiwei 20250331_ai AI 质检 start -->
<div v-show="showAiInspection" class="preview-btn" @click="toAiInspection"> <div v-show="showAiInspection" class="preview-btn" @click="toAiInspection">
<img class="ai-inspection-btn" :src="require('@/assets/img/ai/ai-inspection-btn.png')" /> <img class="ai-btn" :src="require('@/assets/img/ai/ai-inspection-btn.png')" />
</div> </div>
<!-- add by zhangweiwei 20250331_ai AI 质检 end --> <!-- add by zhangweiwei 20250331_ai AI 质检 end -->
<div v-show="showPreview" class="preview-btn" @click="toPreview"> <div v-show="showPreview" class="preview-btn" @click="toPreview">
@@ -88,7 +89,12 @@
</a-button> </a-button>
<!-- <Avatar :onlyUserShow="false" /> --> <!-- <Avatar :onlyUserShow="false" /> -->
</div> </div>
<div class="right" v-if="!showPreview && !isRenderBtn && !showShare"> <div class="right" v-if="(!showPreview && !isRenderBtn && !showShare) || showAiInsight">
<!-- add by zhangweiwei 20250331_ai AI 洞察 start -->
<div v-show="showAiInsight" class="preview-btn" @click="toAiInsight">
<img class="ai-btn" :src="require('@/assets/img/ai/data-analyse-ai-insight-btn.png')" />
</div>
<!-- add by zhangweiwei 20250331_ai AI 洞察 end -->
<a-button <a-button
class="publish-btn share-button" class="publish-btn share-button"
style="display: flex; align-items: center" style="display: flex; align-items: center"
@@ -138,10 +144,7 @@
<!-- 下载中心 --> <!-- 下载中心 -->
<DownloadCenter v-model:visible="downloadVisible" v-if="downloadVisible"></DownloadCenter> <DownloadCenter v-model:visible="downloadVisible" v-if="downloadVisible"></DownloadCenter>
<!-- add by zhangweiwei 20250331_ai AI 质检 start --> <!-- add by zhangweiwei 20250331_ai AI 质检 start -->
<ai-loading <ai-loading :visible="showAiLoading" :desc="aiLoadingDesc" />
:visible="showAiLoading"
desc="我正在为您检查问卷内容大概需要20-30s请稍等哦"
/>
<!-- add by zhangweiwei 20250331_ai AI 质检 end --> <!-- add by zhangweiwei 20250331_ai AI 质检 end -->
</template> </template>
@@ -160,6 +163,7 @@ import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import useEmitter from '@/composables/useEmitter'; import useEmitter from '@/composables/useEmitter';
import { publishSurvey, getSurveyInfo, aiQualityInspection } from '@/api/publish'; import { publishSurvey, getSurveyInfo, aiQualityInspection } from '@/api/publish';
import { aiInsight } from '@/api/data-analyse';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { import {
ExclamationCircleFilled, ExclamationCircleFilled,
@@ -176,6 +180,8 @@ import { canPlanetPublish } from './utils';
import { checkShowInsightTab } from '@/views/DataAnalyse/insight/consts'; import { checkShowInsightTab } from '@/views/DataAnalyse/insight/consts';
import { text } from 'cheerio/lib/static'; import { text } from 'cheerio/lib/static';
import AiLoading from '@/components/layout/loading/AiLoading.vue'; import AiLoading from '@/components/layout/loading/AiLoading.vue';
import aiTaskManager from '@/utils/ai/ai-task-manager'; // 引入AI任务管理器
import { TASK_TYPE, Task } from '@/utils/ai/ai-task';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@@ -244,14 +250,24 @@ const isRenderBtn = computed(() => {
); );
}); });
/** add by zhangweiwei AI 质检 start */ /** add by zhangweiwei AI 质检/洞察 start */
/** /**
* 是否展示 AI 质检 * 是否展示 AI 质检
*/ */
const showAiInspection = computed(() => { const showAiInspection = computed(() => {
return route.matched.some((item) => item.meta?.showAiInspection); return route.matched.some((item) => item.meta?.showAiInspection);
}); });
/** add by zhangweiwei AI 质检 end */
const showAiInsight = computed(() => {
return route.matched.some((item) => item.meta?.showAiInsight);
});
const aiLoadingDesc = computed(() => {
return showAiInspection.value
? '我正在为您检查问卷内容大概需要20-30s请稍等哦'
: '正在激活AI显做镜为您解析问卷背后的情感温度与数据密码大约需要1-2min请您先忙一会记得刷新页面查看结果哦~';
});
/** add by zhangweiwei AI 质检/洞察 end */
const showPreview = computed(() => { const showPreview = computed(() => {
return route.matched.some((item) => item.meta?.showPreview); return route.matched.some((item) => item.meta?.showPreview);
@@ -362,6 +378,40 @@ const toAiInspection = () => {
}; };
/** add by zhangweiwei 20250331_ai AI 质检 end */ /** add by zhangweiwei 20250331_ai AI 质检 end */
/** add by zhangweiwei 20250331_ai AI 洞察 start */
/**
* AI 洞察
*/
const toAiInsight = () => {
const task = aiTaskManager.findTask(TASK_TYPE.INSIGHT, sn);
if (task) {
// 已经在AI洞察中
message.warn(
'正在为您进行AI洞察这次需要多分析一会儿才能确保洞察精准度认真工作的我马上带着结果回来哦'
);
return;
}
aiInsight(route.query.sn, { sn })
.then(() => {
showAiLoading.value = true;
// 10s后关闭
setTimeout(() => {
// 开启轮询模式
aiTaskManager.addTask(new Task(TASK_TYPE.INSIGHT, sn));
showAiLoading.value = false;
}, 10000);
})
.catch((error) => {
if (error.data?.code === 10015) {
// 正在AI洞察中
message.warn(error.data?.message);
} else {
message.error(error.data?.message || '服务器错误');
}
});
};
/** add by zhangweiwei 20250331_ai AI 洞察 end */
const toPreview = async () => { const toPreview = async () => {
var res = await canPlanetPublish(route.query.sn, 1); var res = await canPlanetPublish(route.query.sn, 1);
if (res) { if (res) {
@@ -614,7 +664,7 @@ onUnmounted(() => store.dispatch('polling/stopPollingSurveyState', { sn }));
.preview-btn:hover { .preview-btn:hover {
color: #70b936; color: #70b936;
} }
.ai-inspection-btn { .ai-btn {
width: 82px; width: 82px;
} }
.publish-btn { .publish-btn {

View File

@@ -207,4 +207,28 @@ export function cancelAiMark(sn, data) {
data data
}); });
} }
/** add by zhangweiwei 20250331_ai AI 样本标记 end */ /** add by zhangweiwei 20250331_ai AI 样本标记 end */
/** add by zhangweiwei 20250331_ai AI 洞察 start */
/**
* 数据分析AI 洞察
* @param {String} sn 问卷编号
* @param {Object} data
*/
export function aiInsight(sn, data) {
return request({
url: `/console/surveys/${sn}/analysis_insights`,
method: 'post',
toastError: false,
data
});
}
export function queryAiStatus(sn, data) {
return request({
url: `/console/surveys/${sn}/status`,
method: 'post',
data
});
}
/** add by zhangweiwei 20250331_ai AI 洞察 end */

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -157,7 +157,7 @@ const constantRoutes = [
showPublish: true, showPublish: true,
showDownload: true showDownload: true
}, },
/** add by zhangweiwei 20250331_ai AI 质检 新增 showAiInspection属性 end */ /** add by zhangweiwei 20250331_ai AI 质检 新增 showAiInspection属性 end */
component: () => component: () =>
import( import(
/* webpackChunkName: "planet" */ '../views/planetDesign/Design/DesignContent.vue' /* webpackChunkName: "planet" */ '../views/planetDesign/Design/DesignContent.vue'
@@ -260,8 +260,9 @@ const constantRoutes = [
{ {
path: 'diagram', path: 'diagram',
name: 'Diagram', name: 'Diagram',
meta: { keepAlive: true, showDownload: true }, /** add by zhangweiwei 20250331_ai AI 洞察 新增 showAiInsight 属性 start */
meta: { keepAlive: true, showDownload: true, showAiInsight: true },
/** add by zhangweiwei 20250331_ai AI 洞察 新增 showAiInsight 属性 start */
component: () => component: () =>
import(/* webpackChunkName: "analyse" */ '@/views/DataAnalyse/diagram/test') import(/* webpackChunkName: "analyse" */ '@/views/DataAnalyse/diagram/test')
}, },

View File

@@ -18,3 +18,34 @@
position: relative; position: relative;
top: -2px; top: -2px;
} }
.ant-notification-notice {
padding: 6px 10px 10px !important;
border-radius: 8px !important;
background-color: #f9fff5 !important;
&-icon {
font-size: 14px !important;
}
&-close{
top: 10px !important;
}
&-message {
margin-left: 22px !important;
font-size: 12px !important;
color: #000000 !important;
line-height: 1.4;
}
&-with-icon &-description {
margin-top: 8px;
margin-left: 0 !important;
font-size: 12px !important;
color: #6d6d6d !important;
line-height: 1.4;
}
}

View File

@@ -0,0 +1,45 @@
import { ref } from 'vue';
export class TaskManager {
constructor() {
this.tasks = ref(this.loadTasks());
}
loadTasks() {
const storedTasks = localStorage.getItem('ai-tasks');
return storedTasks ? JSON.parse(storedTasks) : [];
}
saveTasks() {
localStorage.setItem('tasks', JSON.stringify(this.tasks.value));
}
addTask(task) {
this.tasks.value.push(task);
this.saveTasks();
task.run();
}
removeTask(taskId) {
this.tasks.value = this.tasks.value.filter((task) => task.id !== taskId);
this.saveTasks();
}
findTask(aiType,sn) {
return this.tasks.value.find((task) => task.id === `${aiType}-${sn}`);
}
runTasks() {
if (this.tasks.value.length === 0) {
console.log('No ai tasks to run');
return;
}
this.tasks.value.forEach((task) => {
if (task.isRunning) {
task.run();
}
});
}
}
const taskManager = new TaskManager();
export default taskManager;

89
src/utils/ai/ai-task.js Normal file
View File

@@ -0,0 +1,89 @@
import { queryAiStatus } from '@/api/data-analyse'; // 引入查询AI状态的API函数
import taskManager from './ai-task-manager'; // 引入任务管理器
import { notification } from 'ant-design-vue';
import InfoIcon from '@/assets/img/ai/data-analyse-ai-insight-icon.png'; // 导入你的自定义图标
export const TASK_TYPE = {
MARK: 'mark',
INSIGHT: 'insight'
};
const TYPE_CODE = {
[TASK_TYPE.MARK]: 1,
[TASK_TYPE.INSIGHT]: 2
};
const TASK_SUCCESS_NOTICE = {
[TASK_TYPE.MARK]: 'AI小助手已经完成了数据洞察工作请前往数据分析-基础分析页面进行查看吧',
[TASK_TYPE.INSIGHT]: 'AI小助手已经完成了无效样本标记工作请前往数据分析-明细数据页面进行查看吧'
};
export class Task {
constructor(aiType, sn) {
this.id = `${aiType}-${sn}`;
this.sn = sn;
this.aiType = aiType;
this.interval = 15000;
this.maxRetries = 3;
this.status = null;
this.isPolling = true;
this.retryCount = 0;
this.timer = null;
}
run() {
const poll = async () => {
if (this.status === 1 || this.status === 2) {
this.stopPolling();
}
if (!this.isPolling) return;
try {
// type 1=AI样本标记;2=AI洞察
const response = await queryAiStatus(this.sn, {
surveySn: this.sn,
type: TYPE_CODE[this.aiType]
});
console.log('response--->', response);
// 标记状态:0=标记中,1=标记完成,2=标记失败
this.status = response.data?.status;
// 如果状态为成功,停止轮询
if (this.status === 1 || this.status === 2) {
this.stopPolling();
taskManager.removeTask(this.id); // 从任务管理器中移除任务
this.showNotice(); // 显示通知
} else {
this.retryCount = 0; // 重置重试计数
}
} catch (error) {
console.error('Polling error:', error);
// 当发生错误时,增加重试计数
this.retryCount++;
// 如果超过最大重试次数,停止轮询
if (this.retryCount >= this.maxRetries) {
console.log('已达到最大重试次数Stopping polling.');
this.stopPolling();
}
}
};
poll();
this.timer = setInterval(() => {
poll(); // 定时执行任务
}, this.interval);
}
stopPolling() {
this.isPolling = false;
clearInterval(this.timer);
}
showNotice() {
notification.info({
message: '提示',
duration: null,
key: this.aiType,
icon: <img src={InfoIcon} alt="" style="width:12px; height: 12px " />,
description: TASK_SUCCESS_NOTICE[this.aiType]
});
}
}

View File

@@ -0,0 +1,46 @@
<!-- add by zhangweiwei 20250331_ai AI 洞察 start -->
<template>
<div class="ai-insight-item">
<div>
<img
class="ai-insight-item_icon"
:src="require('@/assets/img/ai/data-analyse-ai-insight-icon.png')"
/>
</div>
<div class="ai-insight-item_result" v-html="result"></div>
</div>
</template>
<script setup>
const props = defineProps({
result: {
type: String,
default: () => ''
}
});
</script>
<style scoped lang="scss">
.ai-insight-item {
box-sizing: border-box;
display: flex;
align-items: baseline;
width: 100%;
margin-top: 8px;
margin-bottom: 15px;
padding: 4px 9px;
background: linear-gradient(124deg, #fdfaff 0%, #fbf9ff 1%, #f8f9ff 100%);
border-radius: 6px;
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);
&_icon {
width: 11px;
}
&_result {
margin-left: 4px;
font-size: 12px;
color: #333333;
line-height: 1.5;
}
}
</style>
<!-- add by zhangweiwei 20250331_ai AI 洞察 end -->

View File

@@ -0,0 +1,81 @@
<!-- add by zhangweiwei 20250331_ai AI 洞察 start -->
<template>
<div class="ai-insight" :class="{ 'loading-height': loading }">
<img
class="ai-insight_icon"
:src="require('@/assets/img/ai/data-analyse-ai-insight-result.png')"
/>
<div v-if="loading" class="ai-insight_loading">
<img
class="ai-insight_loading--img"
:src="require('@/assets/img/ai/ai-loading-transparent.png')"
alt=""
/>
<span class="ai-insight_loading--text">AI洞察加速中...</span>
</div>
<div v-else class="ai-insight_result" v-html="result"></div>
</div>
</template>
<script setup>
const props = defineProps({
result: {
type: String,
default: () => ''
},
loading: {
type: Boolean,
default: () => false
}
});
</script>
<style scoped lang="scss">
.ai-insight {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
margin: 0 32px 10px;
background: linear-gradient(124deg, #fdfaff 0%, #fbf9ff 1%, #f8f9ff 100%);
border-radius: 6px;
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);
&_icon {
width: 38px;
margin-left: 10px;
margin-top: 12px;
}
&_loading {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
top: 36px;
bottom: 36px;
&--img {
width: 96px;
}
&--text {
font-size: 12px;
color: #000000;
line-height: 17px;
}
}
&_result {
padding: 15px 28px;
font-size: 12px;
color: #333333;
line-height: 2;
}
}
.loading-height {
height: 150px;
}
</style>
<!-- add by zhangweiwei 20250331_ai AI 洞察 end -->

View File

@@ -1,5 +1,9 @@
<template> <template>
<div class="table-container"> <div class="table-container">
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<a-table <a-table
:align="'center'" :align="'center'"
:columns="columns" :columns="columns"
@@ -37,6 +41,7 @@ import { defineComponent, ref, watch, inject, computed } from 'vue';
import { getFileExtension } from '@/utils/file'; import { getFileExtension } from '@/utils/file';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { downloadFile } from '@/views/DataAnalyse/composables/downloadFile'; import { downloadFile } from '@/views/DataAnalyse/composables/downloadFile';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { downloadAllFile, downloadFileSheet, getDiagramAnalysis } from '@/api/data-analyse'; import { downloadAllFile, downloadFileSheet, getDiagramAnalysis } from '@/api/data-analyse';
@@ -57,7 +62,7 @@ export default defineComponent({
default: '' default: ''
} }
}, },
components: { ImagePreview }, components: { ImagePreview, AiInsightResult },
setup(props) { setup(props) {
const store = useStore(); const store = useStore();
const route = useRoute(); const route = useRoute();

View File

@@ -1,5 +1,9 @@
<template> <template>
<div class="table-container"> <div class="table-container">
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<a-table <a-table
:align="'center'" :align="'center'"
:columns="columns" :columns="columns"
@@ -21,6 +25,7 @@
<script setup> <script setup>
import { computed, ref, watch, inject } from 'vue'; import { computed, ref, watch, inject } from 'vue';
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
import { getDiagramAnalysis } from '@/api/data-analyse'; import { getDiagramAnalysis } from '@/api/data-analyse';
import { convertQueryToString } from '@/utils/httpFormat'; import { convertQueryToString } from '@/utils/httpFormat';

View File

@@ -8,6 +8,10 @@
/> />
<div class="chart-container"> <div class="chart-container">
<ChartAction @update:action="handleUpdateAction" :chartTypeOptions="chartTypeOptions" /> <ChartAction @update:action="handleUpdateAction" :chartTypeOptions="chartTypeOptions" />
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<div class="chart-wrapper"> <div class="chart-wrapper">
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
</div> </div>
@@ -39,6 +43,7 @@ import pieChart from '@/components/chart/PieChart';
import ChartAction from '@/views/DataAnalyse/diagram/components/ChartAction'; import ChartAction from '@/views/DataAnalyse/diagram/components/ChartAction';
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import useMultiChartOption from '@/views/DataAnalyse/diagram/composables/useMultiChartOption'; import useMultiChartOption from '@/views/DataAnalyse/diagram/composables/useMultiChartOption';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
import { generateTableCustomRender } from './renderTableGroup'; import { generateTableCustomRender } from './renderTableGroup';
function clearData(data) { function clearData(data) {
@@ -68,7 +73,8 @@ export default defineComponent({
}, },
components: { components: {
RenderTableTitle, RenderTableTitle,
ChartAction ChartAction,
AiInsightResult
}, },
setup(props) { setup(props) {
const chartInstance = inject('chartInstance'); const chartInstance = inject('chartInstance');
@@ -379,7 +385,8 @@ export default defineComponent({
height: 100%; height: 100%;
.chart-container { .chart-container {
height: 300px; min-height: 300px;
padding-bottom: 20px;
} }
} }

View File

@@ -2,6 +2,10 @@
<div class="container"> <div class="container">
<div class="chart-container"> <div class="chart-container">
<ChartAction @update:action="handleUpdateAction" :chartTypeOptions="chartTypeOptions" /> <ChartAction @update:action="handleUpdateAction" :chartTypeOptions="chartTypeOptions" />
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<div class="chart-wrapper"> <div class="chart-wrapper">
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
</div> </div>
@@ -33,6 +37,7 @@ import ChartAction from '@/views/DataAnalyse/diagram/components/ChartAction';
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo'; import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo';
import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOption'; import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOption';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { generateTableCustomRender } from './renderTableGroup'; import { generateTableCustomRender } from './renderTableGroup';
@@ -77,7 +82,8 @@ export default defineComponent({
components: { components: {
RenderTableTitle, RenderTableTitle,
ChartAction, ChartAction,
pieChart pieChart,
AiInsightResult
}, },
setup(props) { setup(props) {
const chartInstance = inject('chartInstance'); const chartInstance = inject('chartInstance');
@@ -211,7 +217,8 @@ export default defineComponent({
.container { .container {
height: 100%; height: 100%;
.chart-container { .chart-container {
height: 300px; min-height: 300px;
padding-bottom: 20px;
} }
.table-container { .table-container {

View File

@@ -31,6 +31,7 @@ import Header from '@/views/DataAnalyse/diagram/components/Layouts/Header';
import Main from '@/views/DataAnalyse/diagram/components/Layouts/Main'; import Main from '@/views/DataAnalyse/diagram/components/Layouts/Main';
import Footer from '@/views/DataAnalyse/diagram/components/Layouts/Footer'; import Footer from '@/views/DataAnalyse/diagram/components/Layouts/Footer';
import useQuestionInfo from '@/views/DataAnalyse/diagram/composables/useQuestionInfo'; import useQuestionInfo from '@/views/DataAnalyse/diagram/composables/useQuestionInfo';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
import spliceChart from '../../components/spliceChart'; import spliceChart from '../../components/spliceChart';
import generatorTable from './generatorTable'; import generatorTable from './generatorTable';
import { exportChart } from '@/api/data-analyse'; import { exportChart } from '@/api/data-analyse';

View File

@@ -2,6 +2,7 @@
<div class="container"> <div class="container">
<div class="chart-container"> <div class="chart-container">
<ChartAction @update:action="handleUpdateAction" :chartTypeOptions="chartTypeOptions" /> <ChartAction @update:action="handleUpdateAction" :chartTypeOptions="chartTypeOptions" />
<AiInsightResult v-if="data.question_conclusion" :result="data.question_conclusion" />
<div class="chart-wrapper"> <div class="chart-wrapper">
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
</div> </div>
@@ -31,6 +32,7 @@ import pieChart from '@/components/chart/PieChart';
import ChartAction from '@/views/DataAnalyse/diagram/components/ChartAction'; import ChartAction from '@/views/DataAnalyse/diagram/components/ChartAction';
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import useConstantOption from '@/views/DataAnalyse/diagram/composables/useConstantOption'; import useConstantOption from '@/views/DataAnalyse/diagram/composables/useConstantOption';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
function clearData(data) { function clearData(data) {
return data.filter((item) => item.avg !== '0.00'); return data.filter((item) => item.avg !== '0.00');
@@ -49,7 +51,8 @@ export default defineComponent({
}, },
components: { components: {
RenderTableTitle, RenderTableTitle,
ChartAction ChartAction,
AiInsightResult,
// pieChart, // pieChart,
}, },
setup(props, { emit }) { setup(props, { emit }) {
@@ -168,7 +171,8 @@ export default defineComponent({
.container { .container {
height: 100%; height: 100%;
.chart-container { .chart-container {
height: 300px; min-height: 300px;
padding-bottom: 20px;
} }
.table-container { .table-container {

View File

@@ -1,9 +1,11 @@
<template> <template>
<AiInsightResult v-if="data.question_conclusion" :result="data.question_conclusion" />
<div id="location-map" class="location-map" ref="locationMap"></div> <div id="location-map" class="location-map" ref="locationMap"></div>
</template> </template>
<script> <script>
import { defineComponent, ref, onMounted, watchEffect, computed } from 'vue'; import { defineComponent, ref, onMounted, watchEffect, computed } from 'vue';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
import _ from 'lodash'; import _ from 'lodash';
export default defineComponent({ export default defineComponent({
@@ -13,6 +15,9 @@ export default defineComponent({
default: () => {} default: () => {}
} }
}, },
components: {
AiInsightResult
},
setup(props) { setup(props) {
let count = 0; let count = 0;
const map = ref(null); const map = ref(null);

View File

@@ -3,6 +3,10 @@
<div style="height: 32px"> <div style="height: 32px">
<CascaderSelect :data="data.optionAll" :max="3" @onOptionIndexChange="getData"></CascaderSelect> <CascaderSelect :data="data.optionAll" :max="3" @onOptionIndexChange="getData"></CascaderSelect>
</div> </div>
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
<div class="table-container"> <div class="table-container">
<a-table <a-table
@@ -32,6 +36,7 @@ import { useRoute } from 'vue-router';
import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOption'; import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOption';
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo'; import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
function clearData(data) { function clearData(data) {
return data.filter((item) => item.number !== 0); return data.filter((item) => item.number !== 0);

View File

@@ -3,6 +3,10 @@
<div style="height: 32px"> <div style="height: 32px">
<CascaderSelect :data="university" :max="2" @onOptionIndexChange="getData"></CascaderSelect> <CascaderSelect :data="university" :max="2" @onOptionIndexChange="getData"></CascaderSelect>
</div> </div>
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
<div class="table-container"> <div class="table-container">
<a-table <a-table
@@ -33,6 +37,7 @@ import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOpti
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo'; import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo';
import university from '@/views/Answer/questions/json/university.json'; import university from '@/views/Answer/questions/json/university.json';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
function clearData(data) { function clearData(data) {
return data.filter((item) => item.number !== 0); return data.filter((item) => item.number !== 0);

View File

@@ -3,6 +3,10 @@
<div style="height: 32px"> <div style="height: 32px">
<CascaderSelect :data="major" :max="1" @onOptionIndexChange="getData"></CascaderSelect> <CascaderSelect :data="major" :max="1" @onOptionIndexChange="getData"></CascaderSelect>
</div> </div>
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
<div class="table-container"> <div class="table-container">
<a-table <a-table
@@ -33,6 +37,7 @@ import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOpti
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo'; import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo';
import major from '@/views/Answer/questions/json/major.json'; import major from '@/views/Answer/questions/json/major.json';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
function clearData(data) { function clearData(data) {
return data.filter((item) => item.number !== 0); return data.filter((item) => item.number !== 0);

View File

@@ -3,6 +3,10 @@
<div style="height: 32px"> <div style="height: 32px">
<CascaderSelect :data="cityes" :max="1" @onOptionIndexChange="getData"></CascaderSelect> <CascaderSelect :data="cityes" :max="1" @onOptionIndexChange="getData"></CascaderSelect>
</div> </div>
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
<div class="table-container"> <div class="table-container">
<a-table <a-table
@@ -33,6 +37,7 @@ import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOpti
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo'; import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo';
import cityes from '@/views/Answer/questions/json/cityes.json'; import cityes from '@/views/Answer/questions/json/cityes.json';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
function clearData(data) { function clearData(data) {
return data.filter((item) => item.number !== 0); return data.filter((item) => item.number !== 0);

View File

@@ -3,6 +3,10 @@
<div style="height: 32px"> <div style="height: 32px">
<CascaderSelect :data="cityes" :max="2" @onOptionIndexChange="getData"></CascaderSelect> <CascaderSelect :data="cityes" :max="2" @onOptionIndexChange="getData"></CascaderSelect>
</div> </div>
<AiInsightResult
v-if="data.question_conclusion"
:result="data.question_conclusion"
/>
<pieChart :chart-option="option" ref="chart" /> <pieChart :chart-option="option" ref="chart" />
<div class="table-container"> <div class="table-container">
<a-table <a-table
@@ -33,6 +37,7 @@ import useChartOption from '@/views/DataAnalyse/diagram/composables/useChartOpti
import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle'; import RenderTableTitle from '@/views/DataAnalyse/components/RenderTableTitle';
import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo'; import RenderTableTitleLo from '@/views/DataAnalyse/components/RenderTableTitleLo';
import cityes from '@/views/Answer/questions/json/cityes.json'; import cityes from '@/views/Answer/questions/json/cityes.json';
import AiInsightResult from '@/views/DataAnalyse/diagram/components/AiInsightResult';
function clearData(data) { function clearData(data) {
return data.filter((item) => item.number !== 0); return data.filter((item) => item.number !== 0);

View File

@@ -32,6 +32,13 @@
></newSearch> ></newSearch>
</div> </div>
<div v-if="diagramList.length > 0"> <div v-if="diagramList.length > 0">
<!-- add by zhangweiwei 20250331_ai AI 洞察 start -->
<ai-insight-result-overall
v-if="aiInsightResult || (typeof aiInsightLoading !== 'undefined')"
:result="aiInsightResult"
:loading="aiInsightLoading"
/>
<!-- add by zhangweiwei 20250331_ai AI 洞察 end -->
<div class="diagram-container"> <div class="diagram-container">
<BasicDiagramItem <BasicDiagramItem
@remove-chart="onRemove" @remove-chart="onRemove"
@@ -75,21 +82,25 @@
</template> </template>
<script setup> <script setup>
import 'element-plus/theme-chalk/el-loading.css'; import 'element-plus/theme-chalk/el-loading.css';
import { onMounted, ref, computed, provide, nextTick } from 'vue'; import { onMounted, ref, computed, provide, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import BasicDiagramItem from '@/views/DataAnalyse/diagram/components/diagram-item-new.vue'; import BasicDiagramItem from '@/views/DataAnalyse/diagram/components/diagram-item-new.vue';
import SubTitle from '../components/SubTitle.vue'; import SubTitle from '../components/SubTitle.vue';
import search from '../components/diagram/search.vue'; import search from '../components/diagram/search.vue';
import AiInsightResultOverall from '@views/DataAnalyse/diagram/components/AiInsightResultOverall.vue';
import FullScreenModal from '@views/DataAnalyse/diagram/components/FullScreenModal'; import FullScreenModal from '@views/DataAnalyse/diagram/components/FullScreenModal';
import newSearch from '@/views/DataAnalyse/components/diagram/newSearch'; import newSearch from '@/views/DataAnalyse/components/diagram/newSearch';
import { getDiagramAnalysis } from '@/api/data-analyse'; import { aiInsight, getDiagramAnalysis } from '@/api/data-analyse';
import { getAuthRoles } from '@/api/role'; import { getAuthRoles } from '@/api/role';
import { convertQueryToString } from '@/utils/httpFormat'; import { convertQueryToString } from '@/utils/httpFormat';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import useEmitter from '@/composables/useEmitter'; import useEmitter from '@/composables/useEmitter';
import { chartList } from '@/views/DataAnalyse/diagram/composables/useInstance'; import { chartList } from '@/views/DataAnalyse/diagram/composables/useInstance';
import newModal from '@/views/DataAnalyse/components/diagram/newModal'; import newModal from '@/views/DataAnalyse/components/diagram/newModal';
import taskManager from '@/utils/ai/ai-task-manager';
import { TASK_TYPE } from '@/utils/ai/ai-task';
const store = useStore(); const store = useStore();
const emitter = useEmitter(); const emitter = useEmitter();
@@ -107,6 +118,12 @@ provide('exportList', exportList);
const diagramItem = ref(null); const diagramItem = ref(null);
/** add by zhangweiwei 20250331_ai AI 洞察 start */
const aiInsightResult = ref('');
const aiInsightLoading = ref(undefined);
// const tasks = taskManager.tasks.value;
/** add by zhangweiwei 20250331_ai AI 洞察 end */
function getPermissionInfo() { function getPermissionInfo() {
getAuthRoles({ getAuthRoles({
flag: 2, flag: 2,
@@ -167,6 +184,7 @@ function getDiagramData() {
total.value = _total; total.value = _total;
per_page.value = res.meta.per_page; per_page.value = res.meta.per_page;
dataCount.value = res.other.answerCount; dataCount.value = res.other.answerCount;
aiInsightResult.value = res.other.overall_conclusion;
hasElementAfterRemove.value = _total === 0 ? true : false; hasElementAfterRemove.value = _total === 0 ? true : false;
let response_data = res.data let response_data = res.data
@@ -347,6 +365,18 @@ onMounted(async () => {
spinning.value = false; spinning.value = false;
} }
}); });
watch(
() => taskManager.tasks.value,
(newTasks) => {
console.log('任务列表发生变化:', newTasks);
aiInsightLoading.value = taskManager.findTask(TASK_TYPE.INSIGHT, sn)?.isPolling;
if (aiInsightLoading.value) {
aiInsightResult.value = '';
}
},
{ immediate: true, deep: true }
);
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -648,17 +648,14 @@ function aiMark(sn) {
aiSampleMark(sn, { aiSampleMark(sn, {
surveySn: sn surveySn: sn
}) })
.then((res) => { .then(() => {
showAiLoading.value = true; showAiLoading.value = true;
// 5s后关闭 // 10s后关闭
setTimeout(() => { setTimeout(() => {
showAiLoading.value = false; showAiLoading.value = false;
// getData();
// resetSelection();
}, 10000); }, 10000);
}) })
.catch((error) => { .catch((error) => {
console.error('----->', error);
if (error.data?.code === 10014) { if (error.data?.code === 10014) {
// 正在AI标记中 // 正在AI标记中
message.warn(error.data?.message); message.warn(error.data?.message);

View File

@@ -1,8 +1,9 @@
<!-- add by zhangweiwei 20250331_ai AI 质检 新增 质检结果组件 start --> <!-- add by zhangweiwei 20250331_ai AI 质检 新增 质检结果组件 start -->
<template> <template>
<div class="ai-inspection-result"> <div class="ai-inspection-result">
<img :src="require('@/assets/img/ai/ai-inspection-result-icon.png')" class="result-icon" /> <div>
<img class="result-icon" :src="require('@/assets/img/ai/ai-inspection-result-icon.png')" />
</div>
<div class="result-wrap"> <div class="result-wrap">
<div class="result-item" v-for="(advice, index) in result" :key="index"> <div class="result-item" v-for="(advice, index) in result" :key="index">
<span class="result-item__index" v-if="result.length > 1">{{ index + 1 }}</span> <span class="result-item__index" v-if="result.length > 1">{{ index + 1 }}</span>
@@ -35,7 +36,6 @@ const props = defineProps({
.result-icon { .result-icon {
width: 14px; width: 14px;
height: 16px; height: 16px;
margin-top: 4px;
} }
.result-wrap { .result-wrap {