feat: 优化问卷分析图表展示功能

- 修复饼图组件显示问题,取消注释使图表正常显示
- 优化数据处理逻辑,支持问题索引1和2的数据处理
- 移除不必要的响应式包装,直接使用JSON对象提高性能
- 清理未使用的导入和函数,如showToast、surveys等
- 添加对空选项数组的条件判断,避免渲染空数据
- 移除控制台日志输出,提高代码整洁度
- 更新IDE图标主题为material-icon-theme
- 优化图表配置结构,简化代码
This commit is contained in:
Huangzhe
2025-05-15 17:55:57 +08:00
parent 00cc42d565
commit 304a404eaf
10 changed files with 347 additions and 378 deletions

154
.vscode/settings.json vendored
View File

@@ -1,77 +1,77 @@
{ {
"explorer.confirmDelete": false, "explorer.confirmDelete": false,
"editor.fontSize": 16, "editor.fontSize": 16,
"workbench.editorAssociations": { "workbench.editorAssociations": {
"*.ipynb": "jupyter.notebook.ipynb" "*.ipynb": "jupyter.notebook.ipynb"
}, },
"window.zoomLevel": 1, "window.zoomLevel": 1,
"workbench.iconTheme": "vscode-icons", "workbench.iconTheme": "material-icon-theme",
"prettier.enable": true, "prettier.enable": true,
"editor.formatOnSave": false, "editor.formatOnSave": false,
"javascript.format.insertSpaceBeforeFunctionParenthesis": true, "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
"typescript.format.insertSpaceBeforeFunctionParenthesis": true, "typescript.format.insertSpaceBeforeFunctionParenthesis": true,
"prettier.singleQuote": true, "prettier.singleQuote": true,
"emmet.syntaxProfiles": { "emmet.syntaxProfiles": {
"vue-html": "html", "vue-html": "html",
"vue": "html" "vue": "html"
}, },
"files.associations": { "files.associations": {
"*.html": "html", "*.html": "html",
"*.vue": "vue", "*.vue": "vue",
"*.ejs": "html", "*.ejs": "html",
"*.js": "javascript" "*.js": "javascript"
}, },
"vsicons.dontShowNewVersionMessage": true, "vsicons.dontShowNewVersionMessage": true,
"autoimport.showNotifications": true, "autoimport.showNotifications": true,
"path-intellisense.mappings": { "path-intellisense.mappings": {
"@": "${workspaceRoot}/src", "@": "${workspaceRoot}/src",
"/": "${workspaceRoot}/" "/": "${workspaceRoot}/"
}, },
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"css.validate": false, //用来校验CSS文件中的语法错误和潜在的问题 "css.validate": false, //用来校验CSS文件中的语法错误和潜在的问题
"less.validate": false, //用来校验LESS文件中的语法错误和潜在的问题 "less.validate": false, //用来校验LESS文件中的语法错误和潜在的问题
"scss.validate": false, //用来校验SCSS文件中的语法错误和潜在的问题 "scss.validate": false, //用来校验SCSS文件中的语法错误和潜在的问题
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
// 用于在保存文件时自动执行代码操作 // 用于在保存文件时自动执行代码操作
"source.fixAll.eslint": "explicit", // 自动执行ESlint "source.fixAll.eslint": "explicit", // 自动执行ESlint
"source.fixAll.stylelint": "explicit" // 自动执行stylelint "source.fixAll.stylelint": "explicit" // 自动执行stylelint
}, },
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
"javascriptreact", "javascriptreact",
"typescript", "typescript",
"typescriptreact", "typescriptreact",
"html", "html",
"markdown", "markdown",
"yaml", "yaml",
"toml", "toml",
"xml", "xml",
"gql", "gql",
"graphql", "graphql",
"astro" "astro"
], ],
"eslint.nodePath": "./node_modules/@yl/yili-fe-lint-config/node_modules", // 指定ESLint可执行文件路径 "eslint.nodePath": "./node_modules/@yl/yili-fe-lint-config/node_modules", // 指定ESLint可执行文件路径
"eslint.options": { "eslint.options": {
// 用于配置 // 用于配置
"overrideConfigFile": "./node_modules/@yl/yili-fe-lint-config/eslintrc.vue3.js" //该选项指定了 ESLint 应使用的配置文件路径。此项设置会覆盖所有其他位置查找的 ESLint 配置文件。 "overrideConfigFile": "./node_modules/@yl/yili-fe-lint-config/eslintrc.vue3.js" //该选项指定了 ESLint 应使用的配置文件路径。此项设置会覆盖所有其他位置查找的 ESLint 配置文件。
}, },
"stylelint.configBasedir": "./node_modules/@yl/yili-fe-lint-config/", //该选项用于定义 Stylelint 配置文件所基于的基础目录。当您的配置文件中使用 extends、plugins 或其他引用时,这个基础目录将作为解析路径的起点 "stylelint.configBasedir": "./node_modules/@yl/yili-fe-lint-config/", //该选项用于定义 Stylelint 配置文件所基于的基础目录。当您的配置文件中使用 extends、plugins 或其他引用时,这个基础目录将作为解析路径的起点
"stylelint.configFile": "./node_modules/@yl/yili-fe-lint-config/stylelintrc.js", // 该选项指定了 stylelint 应使用的配置文件路径。此项设置会覆盖所有其他位置查找的 stylelint 配置文件 "stylelint.configFile": "./node_modules/@yl/yili-fe-lint-config/stylelintrc.js", // 该选项指定了 stylelint 应使用的配置文件路径。此项设置会覆盖所有其他位置查找的 stylelint 配置文件
"stylelint.customSyntax": "postcss-scss", // 配置stylelint使用的预处理器 "stylelint.customSyntax": "postcss-scss", // 配置stylelint使用的预处理器
"stylelint.stylelintPath": "./node_modules/@yl/yili-fe-lint-config/node_modules/stylelint", // 指定stylelint安装路径 "stylelint.stylelintPath": "./node_modules/@yl/yili-fe-lint-config/node_modules/stylelint", // 指定stylelint安装路径
"stylelint.validate": [ "stylelint.validate": [
"html", "html",
"css", "css",
"scss", "scss",
"less", "less",
"vue" "vue"
], ],
"typescript.tsdk": "node_modules\\typescript\\lib" "typescript.tsdk": "node_modules\\typescript\\lib"
} }

View File

@@ -1,9 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, useTemplateRef } from 'vue'; import { ref, useTemplateRef } from 'vue';
import { showToast } from 'vant';
import { useSetPieChart } from '@/hooks/chart/usePieChart'; import { useSetPieChart } from '@/hooks/chart/usePieChart';
import { surveys } from '@/components/Analysis/hooks/useSurvey';
import { questionTypeMap } from '@/utils/question/typeMapping';
// series 信息 // series 信息
const series = defineModel<any>('series', { required: true }); const series = defineModel<any>('series', { required: true });
@@ -19,7 +16,7 @@ useSetPieChart(pieChart, series, { title: false, legend: false });
<div <div
style="display: flex; height: 300px; width: 300px; justify-content: center; margin: 16px 0" style="display: flex; height: 300px; width: 300px; justify-content: center; margin: 16px 0"
> >
<!-- <span ref="pieChart" style="width: 100%; height: 300px"></span> --> <span ref="pieChart" style="width: 100%; height: 300px"></span>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -1,5 +1,3 @@
import { ref } from 'vue';
const option = { const option = {
// title: { // title: {
// text: 'Referer of a Website', // text: 'Referer of a Website',
@@ -36,13 +34,4 @@ const option = {
] ]
}; };
export const pieOption = ref<Partial<typeof option>>(option); export const pieOption = option;
// 删除左侧的预览图
export function deleteLegend() {
delete pieOption.value.legend;
}
export function deleteTitle() {
delete pieOption.value.title;
}

View File

@@ -1,60 +1,36 @@
import { onMounted, ref, type ShallowRef, watch } from 'vue'; import { onMounted, ref, type ShallowRef, watch } from 'vue';
import type { ECOption } from '@/utils/echarts'; import type { ECOption } from '@/utils/echarts';
import { chart } from '@/utils/echarts'; import { chart } from '@/utils/echarts';
import { deleteLegend, deleteTitle } from './data/pie'; import { pieOption } from './data/pie';
type dataOption = Partial<ECOption['data']>; type dataOption = Partial<ECOption['data']>;
/** /**
* 饼图的 option * 饼图的 option
*/ */
// const option = ref(pieOption);
function useSetPieChart( function useSetPieChart(
dom: Readonly<ShallowRef<HTMLSpanElement | null>>, dom: Readonly<ShallowRef<HTMLSpanElement | null>>,
series: any, series: any,
opts: optsType = {} opts: optsType = {}
): void { ): void {
// 图表实例 // 图表实例
let pieChart: any; let chartInstance: any;
// 检测边界范围 dom 和 data 是否存在
onMounted(() => { onMounted(() => {
if (!dom.value) { // 检测边界范围 dom 和 series 是否存在
console.error('饼图DOM元素不存在'); if (!dom.value && !series) return;
return;
}
// 在 dom 挂载之后,显示饼图 // 在 dom 挂载之后,显示饼图
pieChart = chart.init(dom.value); chartInstance = chart.init(dom.value);
pieOption.series = JSON.parse(JSON.stringify(series.value));
if (series.value) { // 设置图表选项
// 创建完整的配置对象 chartInstance.setOption(pieOption, opts);
const fullOption = {
series: [
{
type: 'pie',
radius: '50%',
data: series.value.data || []
}
]
};
// 设置图表选项
pieChart.setOption(fullOption, opts);
// 强制重绘以确保显示
setTimeout(() => {
pieChart.resize();
}, 100);
} else {
console.error('饼图数据不存在');
}
}); });
// 如果 data 变动重新生成图表w // 如果 data 变动重新生成图表w
watch(series, (value) => { watch(series, (value) => {
pieChart.value.setOption(value, opts); chartInstance.setOption(value, opts);
}); });
} }

View File

@@ -1,7 +1,9 @@
const map = new Map<number, string>(); const map = new Map<number, string>();
map.set(1, '单选题'); map.set(1, '单选题');
map.set(5, '数值打分题'); map.set(2, "多选题")
map.set(9, '矩阵单选'); map.set(3, "图片上传题")
map.set(5, '数值打分题');
export { map as questionTypeMap }; map.set(9, '矩阵单选');
export { map as questionTypeMap };

View File

@@ -1,105 +1,112 @@
<script setup> <script setup>
import LastSurvey from './components/LastSurvey/Index.vue'; import LastSurvey from './components/LastSurvey/Index.vue';
import Market from './components/Market/Index.vue'; import Market from './components/Market/Index.vue';
import CreateSurvey from './components/CreateSurvey/Index.vue'; import CreateSurvey from './components/CreateSurvey/Index.vue';
import NewSurvey from './components/NewSurvey/index.vue'; import NewSurvey from './components/NewSurvey/index.vue';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import utils from '@/assets/js/common'; import utils from '@/assets/js/common';
import { getUserInfo } from '@/api/common/index.js'; import { getUserInfo } from '@/api/common/index.js';
import { showFailToast } from 'vant'; import { showFailToast } from 'vant';
import appBridge from '@/assets/js/appBridge'; import appBridge from '@/assets/js/appBridge';
import ImageSlider from './components/ImageSlider/Index.vue'; import ImageSlider from './components/ImageSlider/Index.vue';
import MineTask from '@/components/Analysis/Index.vue'; import SearchBar from '@/components/Search/Index.vue';
import Navigation from '@/components/Navigation/Index.vue'; import Navigation from '@/components/Navigation/Index.vue';
import router from '@/router';
const contentShow = ref(false);
const contentShow = ref(false);
onMounted(async () => {
if (appBridge.isInReactNative()) { onMounted(async () => {
const appToken = utils.getSessionStorage('xToken'); if (appBridge.isInReactNative()) {
getUserInfo(appToken) const appToken = utils.getSessionStorage('xToken');
.then((res) => { getUserInfo(appToken)
if (res.data) { .then((res) => {
contentShow.value = true; if (res.data) {
const token = res.data.data.token; contentShow.value = true;
localStorage.setItem('plantToken', token); const token = res.data.data.token;
utils.setSessionStorage('userInfo', res.data.data); localStorage.setItem('plantToken', token);
} else { utils.setSessionStorage('userInfo', res.data.data);
contentShow.value = false; } else {
showFailToast( contentShow.value = false;
error.response.data?.message || error.data?.message || error.message || '服务器错误' showFailToast(
); error.response.data?.message || error.data?.message || error.message || '服务器错误'
} );
}) }
.catch((error) => { })
contentShow.value = false; .catch((error) => {
showFailToast(error?.response?.data?.message || error?.message || '服务器错误'); contentShow.value = false;
}); showFailToast(error?.response?.data?.message || error?.message || '服务器错误');
} else { });
utils.setSessionStorage('xToken', 'f74ba36d7fc3468480648dedba5672ff'); } else {
contentShow.value = true; utils.setSessionStorage('xToken', 'f74ba36d7fc3468480648dedba5672ff');
} contentShow.value = true;
}); }
</script> });
<template> function handleSearchClick() {
<div v-if="contentShow" class="container-home"> router.push({ name: 'search' });
<div class="container-body"> }
<!-- 首页轮播图 --> </script>
<image-slider />
<create-survey :createdNewPage="false" /> <template>
<!-- 最新问卷 --> <div v-if="contentShow" class="container-home">
<!--<last-survey/>--> <div class="container-body">
<!-- 模板市场 --> <!-- 搜索栏 -->
<!-- <Market/> --> <search-bar @click="handleSearchClick" />
<!--底部新建问卷--> <!-- 首页轮播图 -->
<NewSurvey /> <image-slider />
<create-survey :createdNewPage="false" />
<!-- <mine-task /> --> <!-- 最新问卷 -->
<!--<last-survey/>-->
<navigation /> <!-- 模板市场 -->
</div> <!-- <Market/> -->
</div> <!--底部新建问卷-->
</template> <NewSurvey />
<style scoped lang="scss"> <!-- <mine-task /> -->
.container-home {
overflow: hidden; <navigation />
width: 100%; </div>
</div>
//background: #f2f2f2; </template>
//position: relative;
<style scoped lang="scss">
.home-pen { .container-home {
//height: 200px; overflow: hidden;
position: absolute; width: 100%;
top: -10px;
left: 0; //background: #f2f2f2;
z-index: 8; //position: relative;
//width: 100%; .home-pen {
background: #fff; //height: 200px;
position: absolute;
//background: linear-gradient(180deg, rgba(242, 242, 242, 0) 0%, #ebffe9 100%); top: -10px;
img { left: 0;
width: 100%; z-index: 8;
//height: 200px; //width: 100%;
} background: #fff;
}
} //background: linear-gradient(180deg, rgba(242, 242, 242, 0) 0%, #ebffe9 100%);
img {
.container-body { width: 100%;
padding: 0 10px 80px;
//height: 200px;
& > :first-child { }
& > div { }
display: flex; }
flex-direction: column;
width: 50px; .container-body {
height: 50px; padding: 0 10px 80px;
margin: 10px;
} & > :first-child {
} & > div {
} display: flex;
</style> flex-direction: column;
width: 50px;
height: 50px;
margin: 10px;
}
}
}
</style>

View File

@@ -1,117 +1,109 @@
import { import { getSurveysPage, deleteSurveys, saveTemplates } from '@/api/home';
getSurveysPage, deleteSurveys, import { ref } from 'vue';
saveTemplates import { showDialog, showConfirmDialog, showFailToast, showToast } from 'vant';
} from '@/api/home'; import { getSurveysDetail } from '@/api/design';
import { ref } from 'vue';
import { const form = ref({
showDialog, page: 0,
showConfirmDialog, pageSize: 10,
showFailToast, project_name: ''
showToast });
} from 'vant';
import { getSurveysDetail } from '@/api/design'; const searchValue = ref('');
const survey = ref<SurveyItem[]>([]);
const form = ref({ const total = ref(0);
page: 0, const loading = ref(false);
pageSize: 10, const finished = ref(false);
project_name: '' const currentSurvey = ref<SurveyItem>();
});
async function fetchSingleSurvey(sn: string) {
const searchValue = ref(''); const res = await getSurveysDetail(sn);
const survey = ref<SurveyItem[]>([]); // console.log(res);
const total = ref(0); if (res.data.code === 0) {
const loading = ref(false); currentSurvey.value = res.data.data;
const finished = ref(false); }
const currentSurvey = ref<SurveyItem>(); }
async function fetchSingleSurvey(sn: string) { async function fetchSurveys() {
const res = await getSurveysDetail(sn); const params = {
// console.log(res); page: form.value.page,
if (res.data.code === 0) { per_page: form.value.pageSize,
currentSurvey.value = res.data.data; group_id: 0,
} project_name: searchValue.value
} };
const res = await getSurveysPage(params);
async function fetchSurveys() { if (res.data.code === 0) {
const params = { survey.value = survey.value.concat(res.data.data);
page: form.value.page, total.value = res.data.meta.total;
per_page: form.value.pageSize, survey.value.forEach((item) => {
group_id: 0, const sceneName = JSON.parse(JSON.stringify(item.scene_name));
project_name: searchValue.value
}; const nameList = sceneName ? sceneName.split('-') : [];
const res = await getSurveysPage(params); if (nameList.length > 0) {
if (res.data.code === 0) { item.scene_name = nameList[1] ? nameList[1] : nameList[0];
survey.value = survey.value.concat(res.data.data); }
total.value = res.data.meta.total;
survey.value.forEach((item) => { const timeList = item.created_at.split(' ');
const sceneName = JSON.parse(JSON.stringify(item.scene_name)); if (nameList.length) {
const nameList = sceneName.split('-'); item.created_at = timeList[0];
if (nameList.length > 0) { }
item.scene_name = nameList[1] ? nameList[1] : nameList[0]; });
} loading.value = false;
// 数据全部加载完成
const timeList = item.created_at.split(' '); if (survey.value.length >= total.value) {
if (nameList.length) { finished.value = true;
item.created_at = timeList[0]; }
} } else {
}); // Toast()
loading.value = false; }
// 数据全部加载完成 }
if (survey.value.length >= total.value) {
finished.value = true; function deleteItem(item: SurveyItem) {
} showDialog({
} else { title: `确认删除问卷${item.project_name} ?`,
// Toast() showCancelButton: true,
} confirmButtonColor: '#03B03C'
} })
.then(async () => {
function deleteItem(item: SurveyItem) { const res = await deleteSurveys(item.sn);
showDialog({ if (res.data.message) {
title: `确认删除问卷${item.project_name} ?`, showToast(res.data.message);
showCancelButton: true, } else {
confirmButtonColor: '#03B03C' showToast('删除成功!');
}) }
.then(async () => { form.value.page = 1;
const res = await deleteSurveys(item.sn); survey.value = [];
if (res.data.message) { await fetchSurveys();
showToast(res.data.message); })
} else { .catch(() => {
showToast('删除成功!'); // on cancel
} });
form.value.page = 1; }
survey.value = [];
await fetchSurveys(); // 保存为模板
}) async function saveTemplate(item: SurveyItem) {
.catch(() => { const data = JSON.parse(JSON.stringify(item));
// on cancel const res = await saveTemplates(item.sn, data);
}); if (res.data.code === 200 || res.data.code === 201) {
}; showConfirmDialog({
message: '模板保存成功,请前往模板市场查看!',
// 保存为模板 showCancelButton: false
async function saveTemplate(item: SurveyItem) { });
const data = JSON.parse(JSON.stringify(item)); } else {
const res = await saveTemplates(item.sn, data); showFailToast(res.data);
if (res.data.code === 200 || res.data.code === 201) { }
showConfirmDialog({ }
message: '模板保存成功,请前往模板市场查看!',
showCancelButton: false export {
}); form,
} else { fetchSurveys,
showFailToast(res.data); loading,
} finished,
} survey,
total,
export { searchValue,
form, deleteItem,
fetchSurveys, saveTemplate,
loading, currentSurvey,
finished, fetchSingleSurvey
survey, };
total,
searchValue,
deleteItem,
saveTemplate,
currentSurvey,
fetchSingleSurvey
}
;

View File

@@ -17,6 +17,8 @@ const route = useRoute();
* 如果当前问卷的数据不存在,重新获取数据 * 如果当前问卷的数据不存在,重新获取数据
*/ */
if (!currentSurvey.value) fetchSingleSurvey(route.query.sn as string); if (!currentSurvey.value) fetchSingleSurvey(route.query.sn as string);
// 重置 message 信息
aiInsightsConfig.value.message = '';
useFetchAnalysis(route.query.sn as string); useFetchAnalysis(route.query.sn as string);
</script> </script>
@@ -45,7 +47,7 @@ useFetchAnalysis(route.query.sn as string);
</template> </template>
</van-cell> </van-cell>
<van-cell v-if="aiInsightsConfig.message.length > 0" class="ai-insight"> <van-cell v-if="aiInsightsConfig.message.length > 0" class="ai-insight" :key="route">
<template #extra> <template #extra>
<!-- ai 洞察部分内容 --> <!-- ai 洞察部分内容 -->
<div v-html="aiInsightsConfig.message" /> <div v-html="aiInsightsConfig.message" />

View File

@@ -1,19 +1,23 @@
<template> <template>
<div> <div>
<section v-for="analysis in questionAnalysis" :key="analysis.stem"> <section
<el-tag>{{ questionTypeMap.get(analysis.question_type as number) }}</el-tag> v-for="analysis in questionAnalysis"
{{ analysis.stem }} :key="analysis.stem"
v-if="analysis?.option.length"
<chart-msg :series="formatData(analysis)" /> >
</section> <el-tag>{{ questionTypeMap.get(analysis.question_type as number) }}</el-tag>
</div> {{ analysis.stem }}
</template>
<chart-msg :series="formatData(analysis)" />
<script setup lang="ts">
import { questionAnalysis } from '../../hooks/useAnalysis'; {{ analysis }}
import { questionTypeMap } from '@/utils/question/typeMapping'; </section>
import ChartMsg from '@/components/Analysis/Index.vue'; </div>
import { formatData, series } from './hooks/pieSeries'; </template>
console.log(`question analysis info`, questionAnalysis.value); <script setup lang="ts">
</script> import { questionAnalysis } from '../../hooks/useAnalysis';
import { questionTypeMap } from '@/utils/question/typeMapping';
import ChartMsg from '@/components/Analysis/Index.vue';
import { formatData } from './hooks/pieSeries';
</script>

View File

@@ -23,12 +23,12 @@ export const series = ref({
}); });
export function formatData(data: any) { export function formatData(data: any) {
const _series = ref(JSON.parse(JSON.stringify(series.value))); const _series = JSON.parse(JSON.stringify(series.value));
// 当内容为单选的时候处理方式 // 当内容为单选的时候处理方式
if (data.question_index === 2) { if (data.question_index === 2 || data.question_index === 1) {
const { option } = data; const { option } = data;
_series.value.data = option.map((item: any) => { _series.data = option.map((item: any) => {
return { return {
value: item.number, value: item.number,
name: item.title name: item.title