feat: 问卷分析页面功能实现
- 修复路径别名问题 - 添加问卷类型映射工具 - 实现饼图数据格式化功能 - 优化图表组件传参方式 - 重构分析信息展示组件
This commit is contained in:
7
components.d.ts
vendored
7
components.d.ts
vendored
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
Contenteditable: typeof import('./src/components/contenteditable.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
@@ -22,6 +23,7 @@ declare module 'vue' {
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
Index: typeof import('./src/components/Analysis/Index.vue')['default']
|
||||
MarketItem: typeof import('./src/components/MarketItem/MarketItem.vue')['default']
|
||||
@@ -34,6 +36,7 @@ declare module 'vue' {
|
||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||
VanCheckbox: typeof import('vant/es')['Checkbox']
|
||||
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
|
||||
VanCol: typeof import('vant/es')['Col']
|
||||
VanDivider: typeof import('vant/es')['Divider']
|
||||
VanField: typeof import('vant/es')['Field']
|
||||
VanIcon: typeof import('vant/es')['Icon']
|
||||
@@ -44,6 +47,7 @@ declare module 'vue' {
|
||||
VanPopup: typeof import('vant/es')['Popup']
|
||||
VanRadio: typeof import('vant/es')['Radio']
|
||||
VanRadioGroup: typeof import('vant/es')['RadioGroup']
|
||||
VanRow: typeof import('vant/es')['Row']
|
||||
VanSearch: typeof import('vant/es')['Search']
|
||||
VanSpace: typeof import('vant/es')['Space']
|
||||
VanStepper: typeof import('vant/es')['Stepper']
|
||||
@@ -60,4 +64,7 @@ declare module 'vue' {
|
||||
YLPicker: typeof import('./src/components/YLPicker.vue')['default']
|
||||
YLSelect: typeof import('./src/components/YLSelect.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,15 @@ export function analysisInsights (sn: string, params?: ParticalParams) {
|
||||
params
|
||||
});
|
||||
}
|
||||
/**
|
||||
* ai 洞察检查分析是否完成
|
||||
* @param sn 循环题组sn
|
||||
* @param data {1|2} 1=AI样本标记;2=AI洞察
|
||||
*/
|
||||
export function checkAnalysisSatus(sn: string, data?: 1 | 2) {
|
||||
return request({
|
||||
url: `/console/surveys/${sn}/status`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function getSurveyInfo(sn) {
|
||||
* @param data {import('@/api/types/analysis.js').AnalysisParam}
|
||||
* @returns {Promise<axios.AxiosResponse<any>> | *}
|
||||
*/
|
||||
export function surveyAnalysis(sn, data) {
|
||||
export function surveyAnalysis(sn, data = {}) {
|
||||
return request({
|
||||
url: `/console/surveys/${sn}/chart_analysis`,
|
||||
method: 'post',
|
||||
|
||||
@@ -3,94 +3,23 @@ import { ref, useTemplateRef } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useSetPieChart } from '@/hooks/chart/usePieChart';
|
||||
import { surveys } from '@/components/Analysis/hooks/useSurvey';
|
||||
import { questionTypeMap } from '@/utils/question/typeMapping';
|
||||
|
||||
// const info = defineModel<any>('info', { required: true });
|
||||
// series 信息
|
||||
const series = defineModel<any>('series', { required: true });
|
||||
// 饼图 dom 结构
|
||||
const pieChart = useTemplateRef<HTMLSpanElement>('pieChart');
|
||||
|
||||
useSetPieChart(pieChart, [1], { title: false, ledge: false });
|
||||
|
||||
// 任务数据
|
||||
const taskData = ref({
|
||||
title: '问卷A',
|
||||
progress: 100,
|
||||
deadline: '2025-03-31至04-01',
|
||||
creator: '张三',
|
||||
creationMethod: '移动端',
|
||||
creationTime: '2025-03-04',
|
||||
responseCount: 10,
|
||||
responseRate: '10%',
|
||||
submissionRate: '10%',
|
||||
status: '草稿中'
|
||||
});
|
||||
|
||||
// 图表数据
|
||||
const chartData = ref([
|
||||
{
|
||||
name: '选项1',
|
||||
value: 66.77,
|
||||
color: '#F56C6C'
|
||||
},
|
||||
{
|
||||
name: '选项2',
|
||||
value: 33.33,
|
||||
color: '#F7BA2A'
|
||||
},
|
||||
{
|
||||
name: '选项3',
|
||||
value: 0,
|
||||
color: '#67C23A'
|
||||
},
|
||||
{
|
||||
name: '选项4',
|
||||
value: 0,
|
||||
color: '#409EFF'
|
||||
}
|
||||
]);
|
||||
|
||||
// 导航按钮点击事件
|
||||
const handlePrev = () => {
|
||||
showToast('点击了上一个问题');
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
showToast('点击了下一个问题');
|
||||
};
|
||||
|
||||
useSetPieChart(pieChart, series.value, { title: false, ledge: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<!-- 卡片内容 -->
|
||||
<van-space direction="vertical" fill style="width: 100%; padding-top: 16px">
|
||||
<p>1. 能描述您为自己或家人购买这款产品A的可能性吗?(单选)</p>
|
||||
|
||||
<!-- 图表部分 -->
|
||||
<div style="display: flex; justify-content: center; margin: 16px 0">
|
||||
<span ref="pieChart" style="width: 100%; height: 300px"></span>
|
||||
</div>
|
||||
|
||||
<!-- 选项列表 -->
|
||||
<van-cell-group inset>
|
||||
<van-cell v-for="(item, index) in chartData" :key="index">
|
||||
<template #title>
|
||||
<van-tag :color="item.value > 0 ? item.color : '#dcdfe6'" plain>
|
||||
{{ item.name }}
|
||||
</van-tag>
|
||||
</template>
|
||||
<template #value>
|
||||
<span>{{ item.value.toFixed(2) }}%</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</van-space>
|
||||
<!-- 导航按钮 左右按钮-->
|
||||
<!-- <div style="position: fixed; left: 16px; top: 50%; transform: translateY(-50%)">-->
|
||||
<!-- <van-button round icon="arrow-left" @click="handlePrev" />-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div style="position: fixed; right: 16px; top: 50%; transform: translateY(-50%)">-->
|
||||
<!-- <van-button round icon="arrow" @click="handleNext" />-->
|
||||
<!-- </div>-->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,27 +9,27 @@ const pieChart = ref();
|
||||
/**
|
||||
* 定义数据集
|
||||
*/
|
||||
const series = ref<dataOption[]>([
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [
|
||||
{ value: 1048, name: 'Search Engine' },
|
||||
{ value: 735, name: 'Direct' },
|
||||
{ value: 580, name: 'Email' },
|
||||
{ value: 484, name: 'Union Ads' },
|
||||
{ value: 300, name: 'Video Ads' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
// const series = ref<dataOption[]>([
|
||||
// {
|
||||
// name: 'Access From',
|
||||
// type: 'pie',
|
||||
// radius: '50%',
|
||||
// data: [
|
||||
// { value: 1048, name: 'Search Engine' },
|
||||
// { value: 735, name: 'Direct' },
|
||||
// { value: 580, name: 'Email' },
|
||||
// { value: 484, name: 'Union Ads' },
|
||||
// { value: 300, name: 'Video Ads' }
|
||||
// ],
|
||||
// emphasis: {
|
||||
// itemStyle: {
|
||||
// shadowBlur: 10,
|
||||
// shadowOffsetX: 0,
|
||||
// shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ]);
|
||||
|
||||
/**
|
||||
* 饼图的 option
|
||||
@@ -38,18 +38,18 @@ const option = ref(pieOption);
|
||||
|
||||
function useSetPieChart(
|
||||
dom: Readonly<ShallowRef<HTMLSpanElement | null>>,
|
||||
data: any[],
|
||||
series: any,
|
||||
opts: optsType = {}
|
||||
): void {
|
||||
for (let item in opts) {
|
||||
if (!series) return;
|
||||
for (const item in opts) {
|
||||
if (item === 'legend') !opts[item] && deleteLegend();
|
||||
else if (item === 'title') !opts[item] && deleteTitle();
|
||||
}
|
||||
|
||||
// 检测边界范围 dom 和 data 是否存在
|
||||
onMounted(() => {
|
||||
// console.log(dom);
|
||||
if (!dom || data.length === 0) return;
|
||||
if (!dom || series.data.length === 0) return;
|
||||
// 在 dom 挂载之后,显示饼图
|
||||
pieChart.value = chart.init(dom.value);
|
||||
pieChart.value.setOption(option.value, opts);
|
||||
|
||||
7
src/utils/question/typeMapping.ts
Normal file
7
src/utils/question/typeMapping.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
const map = new Map<number, string>();
|
||||
|
||||
map.set(1, '单选题');
|
||||
map.set(5, '数值打分题');
|
||||
map.set(9, '矩阵单选');
|
||||
|
||||
export { map as questionTypeMap };
|
||||
@@ -11,7 +11,13 @@ import { showDialog, showFailToast, showSuccessToast } from 'vant';
|
||||
import { finish } from '@/api/survey';
|
||||
import { copySurveys } from '@/api/home';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { form, fetchSurveys, deleteItem, saveTemplate, currentSurvey } from '@/views/Survey/hooks/useSurveyData';
|
||||
import {
|
||||
form,
|
||||
fetchSurveys,
|
||||
deleteItem,
|
||||
saveTemplate,
|
||||
currentSurvey
|
||||
} from '@/views/Survey/hooks/useSurveyData';
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
@@ -119,7 +125,6 @@ function copyItem (item: SurveyItem) {
|
||||
// on cancel
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -136,14 +141,25 @@ function copyItem (item: SurveyItem) {
|
||||
{{ `${survey.start_time} 至 ${survey.end_time ?? '无限期'}` }}
|
||||
</el-text>
|
||||
|
||||
<el-button v-if="isAnalysis" type="primary" size="small" @click="emit('post-analysis')">AI 洞察</el-button>
|
||||
<el-button
|
||||
v-if="isAnalysis"
|
||||
style="margin-left: 30px"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="emit('post-analysis')"
|
||||
>
|
||||
AI 洞察
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="survey_item_info_status">
|
||||
<el-space spacer="|">
|
||||
<!--报名签到-->
|
||||
<div class="flex align-center">
|
||||
<img :src="setImg(survey.scene_code_info as number)" alt="Content Icon"
|
||||
style="width: 15px; height: 15px" />
|
||||
<img
|
||||
:src="setImg(survey.scene_code_info as number)"
|
||||
alt="Content Icon"
|
||||
style="width: 15px; height: 15px"
|
||||
/>
|
||||
<el-text size="small">{{ survey.owner }}</el-text>
|
||||
</div>
|
||||
<!-- 问卷来源 -->
|
||||
@@ -186,9 +202,7 @@ function copyItem (item: SurveyItem) {
|
||||
<!-- action 功能位置 -->
|
||||
<div v-if="!isAnalysis" class="survey_item_action">
|
||||
<div>
|
||||
<el-button :disabled="survey.source === 0" @click="editItem(survey)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button :disabled="survey.source === 0" @click="editItem(survey)"> 编辑 </el-button>
|
||||
<!-- <el-button-->
|
||||
<!-- style="border: 1px solid #71b73c"-->
|
||||
<!-- @click="toPreview(survey)"-->
|
||||
@@ -205,19 +219,22 @@ function copyItem (item: SurveyItem) {
|
||||
</el-text>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-dropdown v-if="survey.source === 1" placement="top-end" trigger="click" active-color="#71b73c">
|
||||
<el-dropdown
|
||||
v-if="survey.source === 1"
|
||||
placement="top-end"
|
||||
trigger="click"
|
||||
active-color="#71b73c"
|
||||
>
|
||||
<van-icon class-prefix="mobilefont" name="gengduo" size="0.7rem"></van-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu active-color="#71b73c" :close-on-click-overlay="false" :close-on-click-outside="false">
|
||||
<el-dropdown-item @click="copyItem(survey)">
|
||||
复制
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteItem(survey)">
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="saveTemplate(survey)">
|
||||
保存为模板
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-menu
|
||||
active-color="#71b73c"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-click-outside="false"
|
||||
>
|
||||
<el-dropdown-item @click="copyItem(survey)"> 复制 </el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteItem(survey)"> 删除 </el-dropdown-item>
|
||||
<el-dropdown-item @click="saveTemplate(survey)"> 保存为模板 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -226,8 +243,8 @@ function copyItem (item: SurveyItem) {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/base";
|
||||
@import "@/assets/css/main";
|
||||
@import '@/assets/css/base';
|
||||
@import '@/assets/css/main';
|
||||
|
||||
.survey_item_info {
|
||||
.survey_item_info_status {
|
||||
|
||||
@@ -4,16 +4,21 @@ import SurveyItem from '@/views/Survey/components/SurveyItem.vue';
|
||||
import LogicInfo from '@/views/Survey/views/Analysis/components/LogicInfo/Index.vue';
|
||||
import AiInsightResults from '@/views/Survey/views/Analysis/components/AiInsightResults/Index.vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFetchAnalysis, postAnalysis, aiInsightsConfig } from '@/views/Survey/views/Analysis/hooks/useAnalysis';
|
||||
import {
|
||||
useFetchAnalysis,
|
||||
postAnalysis,
|
||||
aiInsightsConfig
|
||||
} from '@/views/Survey/views/Analysis/hooks/useAnalysis';
|
||||
import Wait from '@/views/Survey/views/Analysis/components/Wait/Index.vue';
|
||||
import AnalysisInfo from '@/views/Survey/views/Analysis/components/AnalysisInfo/Index.vue';
|
||||
|
||||
const route = useRoute();
|
||||
/**
|
||||
* 如果当前问卷的数据不存在,重新获取数据
|
||||
*/
|
||||
if (!currentSurvey.value) fetchSingleSurvey(<string>route.query.sn);
|
||||
if (!currentSurvey.value) fetchSingleSurvey(route.query.sn as string);
|
||||
|
||||
useFetchAnalysis(<string>route.query.sn);
|
||||
useFetchAnalysis(route.query.sn as string);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,11 +26,14 @@ useFetchAnalysis(<string>route.query.sn);
|
||||
<!-- 问卷详情部分 -->
|
||||
<van-cell class="survey-item">
|
||||
<template #extra>
|
||||
<survey-item @post-analysis="postAnalysis(route.query.sn as string)" :is-analysis="true"
|
||||
:survey="<SurveyItem>currentSurvey" />
|
||||
<survey-item
|
||||
:is-analysis="true"
|
||||
:survey="currentSurvey as SurveyItem"
|
||||
@post-analysis="postAnalysis(route.query.sn as string)"
|
||||
/>
|
||||
|
||||
<!-- 弹窗组件 -->
|
||||
<el-dialog align-center width="70%" v-model="aiInsightsConfig.visible">
|
||||
<el-dialog v-model="aiInsightsConfig.visible" align-center width="70%">
|
||||
<wait />
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -37,19 +45,24 @@ useFetchAnalysis(<string>route.query.sn);
|
||||
</template>
|
||||
</van-cell>
|
||||
|
||||
<van-cell class="ai-insight">
|
||||
<van-cell v-if="false" class="ai-insight">
|
||||
<template #extra>
|
||||
<!-- ai 洞察部分内容 -->
|
||||
<AiInsightResults />
|
||||
</template>
|
||||
</van-cell>
|
||||
|
||||
<van-cell class="analysis">
|
||||
<template #extra>
|
||||
<!-- 图表分析部分 -->
|
||||
<analysis-info />
|
||||
</template>
|
||||
</van-cell>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/assets/css/theme";
|
||||
@use '@/assets/css/theme';
|
||||
|
||||
.survey-container {
|
||||
padding: theme.$gap;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<section v-for="analysis in questionAnalysis" :key="analysis.stem">
|
||||
<el-tag>{{ questionTypeMap.get(analysis.question_type as number) }}</el-tag>
|
||||
{{ analysis.stem }}
|
||||
|
||||
<chart-msg :series="formatData(analysis)" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { questionAnalysis } from '../../hooks/useAnalysis';
|
||||
import { questionTypeMap } from '@/utils/question/typeMapping';
|
||||
import ChartMsg from '@/components/Analysis/Index.vue';
|
||||
import { formatData, series } from './hooks/pieSeries';
|
||||
|
||||
console.log(`question analysis info`, questionAnalysis.value);
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const series = ref({
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: []
|
||||
});
|
||||
|
||||
export function formatData(data: any) {
|
||||
console.log(data);
|
||||
const { option } = data;
|
||||
|
||||
series.value.data = option.map((item) => {
|
||||
return {
|
||||
value: item.number,
|
||||
name: item.title
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`series data`, series.value);
|
||||
|
||||
return series.value;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { surveyAnalysis } from '@/api/survey';
|
||||
import { ref } from 'vue';
|
||||
import { analysisInsights } from '@/api/anslysis/aiInsight';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { analysisInsights, checkAnalysisSatus } from '@/api/anslysis/aiInsight';
|
||||
|
||||
type AiInsightType = {
|
||||
visible: boolean,
|
||||
timer: NodeJS.Timeout | undefined
|
||||
}
|
||||
visible: boolean;
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
};
|
||||
// ai 智能洞察
|
||||
export const aiInsightsConfig = ref<AiInsightType>({
|
||||
visible: false,
|
||||
timer: void 0
|
||||
timer: undefined
|
||||
});
|
||||
|
||||
const questionAnalysis = ref();
|
||||
@@ -20,23 +20,28 @@ const params: AnalysisParam = {
|
||||
};
|
||||
|
||||
export function postAnalysis(sn: string) {
|
||||
console.log(sn);
|
||||
aiInsightsConfig.value.visible = true;
|
||||
// analysisInsights(sn);
|
||||
analysisInsights(sn);
|
||||
if (!aiInsightsConfig.value.timer) {
|
||||
// 每 1 s 检查一次是否分析完成
|
||||
aiInsightsConfig.value.timer = setInterval(() => {
|
||||
checkAnalysis(sn);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 当组件取消挂载时, 取消 interval 定时
|
||||
clearInterval(aiInsightsConfig.value.timer);
|
||||
});
|
||||
}
|
||||
|
||||
export function checkAnalysis (sn: string) {
|
||||
console.log(sn);
|
||||
export async function checkAnalysis(sn: string) {
|
||||
const res = checkAnalysisSatus(sn, 2);
|
||||
console.log(res);
|
||||
}
|
||||
|
||||
async function useFetchAnalysis(sn: string) {
|
||||
const res = await surveyAnalysis(sn, params);
|
||||
const res = await surveyAnalysis(sn);
|
||||
questionAnalysis.value = res.data.data;
|
||||
console.log(`question analysis`, questionAnalysis.value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user