diff --git a/web/components.d.ts b/web/components.d.ts index c897a9e0..13eb4c58 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -17,6 +17,7 @@ declare module 'vue' { ElDialog: typeof import('element-plus/es')['ElDialog'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMenu: typeof import('element-plus/es')['ElMenu'] @@ -29,6 +30,7 @@ declare module 'vue' { ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] + ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelectV2: typeof import('element-plus/es')['ElSelectV2'] ElSlider: typeof import('element-plus/es')['ElSlider'] diff --git a/web/package.json b/web/package.json index 3cc9e456..114191e1 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "axios": "^1.4.0", "clipboard": "^2.0.11", "crypto-js": "^4.2.0", + "echarts": "^5.5.0", "element-plus": "^2.7.0", "lodash-es": "^4.17.21", "moment": "^2.29.4", diff --git a/web/src/management/api/analysis.js b/web/src/management/api/analysis.js index 8e55a48a..5f211572 100644 --- a/web/src/management/api/analysis.js +++ b/web/src/management/api/analysis.js @@ -8,3 +8,11 @@ export const getRecycleList = (data) => { } }) } + +export const getStatisticList = (data) => { + return axios.get('/survey/dataStatistic/aggregationStatis', { + params: { + ...data + } + }) +} diff --git a/web/src/management/config/analysisConfig.js b/web/src/management/config/analysisConfig.js new file mode 100644 index 00000000..c0957837 --- /dev/null +++ b/web/src/management/config/analysisConfig.js @@ -0,0 +1,77 @@ +import { menuItems } from './questionMenuConfig' + +export const noDataConfig = { + title: '暂无数据', + desc: '您的问卷当前还没有数据,快去回收问卷吧!', + img: '/imgs/icons/analysis-empty.webp' +} + +export const separateItemListHead = [ + { + title: '选项', + field: 'text' + }, + { + title: '数量', + field: 'count' + }, + { + title: '占比', + field: 'percent' + } +] + +// 图表名称需要和./chartConfig.js中保持一致 +export const questionChartsConfig = { + [menuItems['checkbox']['type']]: ['bar'], + [menuItems['radio-nps']['type']]: ['gauge', 'pie', 'bar'], + default: ['pie', 'bar'] +} + +export const analysisTypeMap = { + dataTable: 'dataTable', + separateStatistics: 'separateStatistics' +} + +export const analysisType = [ + { + value: analysisTypeMap.dataTable, + label: '数据列表', + icon: 'icon-shujuliebiao' + }, + { + value: analysisTypeMap.separateStatistics, + label: '分题统计', + icon: 'icon-fentitongji' + } +] + +export const summaryType = { + between: 'between' +} + +export const summaryItemConfig = { + 'radio-nps': [ + { + text: '推荐者', + field: 'id', + type: summaryType.between, + max: 10, + min: 9 + }, + { + text: '中立者', + field: 'id', + type: summaryType.between, + max: 8, + min: 7 + }, + { + text: '贬损者', + field: 'id', + type: summaryType.between, + max: 6, + min: 0 + } + ] +} diff --git a/web/src/management/config/chartConfig/bar.js b/web/src/management/config/chartConfig/bar.js new file mode 100644 index 00000000..32b6bb22 --- /dev/null +++ b/web/src/management/config/chartConfig/bar.js @@ -0,0 +1,57 @@ +/** + * @Description: 柱状图配置 + * @CreateDate: 2024-04-30 + */ +export default (data) => { + const xAxisData = data.map((item) => item.name) + return { + color: ['#55A8FD'], + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: '{a}
{b}: {c}' + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: [ + { + type: 'category', + data: xAxisData, + axisTick: { + alignWithLabel: true + }, + axisLabel: { + interval: 0, + formatter(value) { + return value + } + } + } + ], + yAxis: [ + { + type: 'value', + splitLine: { + lineStyle: { + type: 'dashed' + } + } + } + ], + series: [ + { + showAllSymbol: true, + name: '提交人数', + type: 'bar', + barMaxWidth: 50, + data + } + ] + } +} diff --git a/web/src/management/config/chartConfig/gauge.js b/web/src/management/config/chartConfig/gauge.js new file mode 100644 index 00000000..c3c2678f --- /dev/null +++ b/web/src/management/config/chartConfig/gauge.js @@ -0,0 +1,146 @@ +/** + * @Description: gauge(仪表盘) + * @CreateDate: 2024-04-30 + */ +export default (data) => { + return { + series: [ + { + type: 'gauge', + startAngle: 180, + endAngle: 0, + min: -100, + max: 100, + radius: '130%', + center: ['50%', '80%'], + splitNumber: 4, + z: 2, + axisLabel: { + show: false, + distance: 0, + color: '#AAB1C0', + fontSize: 12, + fontFamily: 'DaQi-Font' + }, + splitLine: { + show: false + }, + axisTick: { + show: false + }, + axisLine: { + lineStyle: { + width: 40, + color: [[1, '#e3efff']] + } + } + }, + { + type: 'gauge', + startAngle: 174, + endAngle: 5, + min: -100, + max: 100, + radius: '130%', + splitNumber: 4, + center: ['50%', '80%'], + z: 3, + axisLabel: { + distance: -5, + color: '#666', + rotate: 'tangential', + fontSize: 12, + fontFamily: 'DaQi-Font' + }, + splitLine: { + show: false + }, + axisTick: { + show: false + }, + axisLine: { + lineStyle: { + width: 40, + color: [[1, '#e3efff']] + } + } + }, + { + type: 'gauge', + startAngle: 178, + endAngle: 0, + min: -100, + max: 100, + radius: '109%', + z: 4, + center: ['50%', '80%'], + splitNumber: 4, + itemStyle: { + color: '#58D9F9' + }, + progress: { + show: true, + roundCap: true, + width: 15, + itemStyle: { + color: '#55A8FD', + shadowBlur: 10, + shadowColor: '#55A8FD' + } + }, + pointer: { + icon: 'triangle', + length: '10%', + width: 8, + offsetCenter: [0, '-80%'], + itemStyle: { + color: '#55A8FD' + } + }, + axisLine: { + lineStyle: { + width: 15, + color: [[1, '#d3e5fe']] + } + }, + axisTick: { + show: false + }, + splitLine: { + show: false + }, + axisLabel: { + show: false + }, + title: { + offsetCenter: [0, '-15%'], + fontSize: 18, + color: '#666' + }, + detail: { + fontSize: 46, + lineHeight: 40, + height: 40, + offsetCenter: [0, '-45%'], + valueAnimation: true, + color: '#55A8FD', + formatter: function (value) { + if (value) { + return value + '%' + } else if (value === 0) { + return value + } else { + return '--' + } + } + }, + data: [ + { + value: data, + name: 'NPS' + } + ] + } + ] + } +} diff --git a/web/src/management/config/chartConfig/index.js b/web/src/management/config/chartConfig/index.js new file mode 100644 index 00000000..6529b379 --- /dev/null +++ b/web/src/management/config/chartConfig/index.js @@ -0,0 +1,9 @@ +import pie from './pie' +import bar from './bar' +import gauge from './gauge' + +export const getOption = { + pie, + bar, + gauge +} diff --git a/web/src/management/config/chartConfig/pie.js b/web/src/management/config/chartConfig/pie.js new file mode 100644 index 00000000..fe5aa5b2 --- /dev/null +++ b/web/src/management/config/chartConfig/pie.js @@ -0,0 +1,57 @@ +const color = [ + '#55A8FD', + '#36CBCB', + '#FAD337', + '#A6D6FF', + '#A177DC', + '#F46C73', + '#FFBA62', + '#ACE474', + '#BEECD6', + '#AFD2FF' +] +/* + * @Description: 饼图配置 + * @CreateDate: 2024-04-30 + */ +export default (data) => { + return { + color, + tooltip: { + trigger: 'item', + formatter: '{a}
{b}: {c} ({d}%)' + }, + legend: { + orient: 'vertical', + right: 12, + top: 12, + tooltip: { + show: true + }, + formatter(name) { + return name.length > 17 ? name.substr(0, 17) + '...' : name + } + }, + series: [ + { + name: '提交人数', + type: 'pie', + radius: ['50%', '80%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: true, + formatter({ data }) { + const name = data?.name || '' + return name.length > 17 ? name.substr(0, 17) + '...' : name + } + }, + data + } + ] + } +} diff --git a/web/src/management/pages/list/config/index.js b/web/src/management/config/listConfig.js similarity index 100% rename from web/src/management/pages/list/config/index.js rename to web/src/management/config/listConfig.js diff --git a/web/src/management/config/questionMenuConfig.js b/web/src/management/config/questionMenuConfig.js index 2eaa27f0..66784bc5 100644 --- a/web/src/management/config/questionMenuConfig.js +++ b/web/src/management/config/questionMenuConfig.js @@ -1,4 +1,4 @@ -const menuItems = { +export const menuItems = { text: { type: 'text', snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp', diff --git a/web/src/management/hooks/useCharts.js b/web/src/management/hooks/useCharts.js new file mode 100644 index 00000000..e15583f7 --- /dev/null +++ b/web/src/management/hooks/useCharts.js @@ -0,0 +1,25 @@ +import * as echarts from 'echarts' +import { getOption } from '@/management/config/chartConfig' + +/** + * 绘制图表 + * @param {Object} el + * @param {String} type + * @param {Array} data + */ +export default (el, type, data) => { + const chart = echarts.init(el) + const option = getOption[type](data) + + chart.setOption(option, true) + + const resize = () => { + chart.resize() + } + + const changeType = (type, data) => { + chart.setOption(getOption[type](data), true) + } + + return { chart, resize, changeType } +} diff --git a/web/src/management/hooks/useResizeObserver.js b/web/src/management/hooks/useResizeObserver.js new file mode 100644 index 00000000..486962f2 --- /dev/null +++ b/web/src/management/hooks/useResizeObserver.js @@ -0,0 +1,20 @@ +// 引入防抖函数 +import _debounce from 'lodash/debounce' +/** + * @description: 监听元素尺寸变化 + * @param {*} el 元素dom + * @param {*} cb resize变化时执行的方法 + * @param {*} wait 防抖间隔 + * @return {*} + */ +export default (el, cb, wait = 200) => { + const resizeObserver = new ResizeObserver(_debounce(cb, wait)) + + resizeObserver.observe(el) + + const destroy = () => { + resizeObserver.disconnect(el) + } + + return { destroy, resizeObserver } +} diff --git a/web/src/management/hooks/useStatisticsItemChart.js b/web/src/management/hooks/useStatisticsItemChart.js new file mode 100644 index 00000000..cc7b18cf --- /dev/null +++ b/web/src/management/hooks/useStatisticsItemChart.js @@ -0,0 +1,77 @@ +import { ref, watchEffect } from 'vue' +import { cleanRichText } from '@/common/xss' +import { questionChartsConfig } from '../config/analysisConfig' + +// 饼图数据处理 +const pie = (data) => { + const aggregation = data?.aggregation + return ( + aggregation?.map?.((item) => { + const { id, count, text } = item + return { + id, + value: count, + name: cleanRichText(text) + } + }) || [] + ) +} +// 柱状图数据处理 +const bar = (data) => { + const aggregation = data?.aggregation + return ( + aggregation?.map?.((item) => { + const { id, count, text } = item + return { + id, + value: count, + name: cleanRichText(text) + } + }) || [] + ) +} +// 仪表盘数据处理 +const gauge = (data) => { + return parseFloat(data?.summary?.nps) || 0 +} + +const dataFormateConfig = { + pie, + bar, + gauge +} + +/** + * @description: 分题统计图表hook + * @param {*} chartType + * @param {*} data + * @return {*} chartRef 图表实例 chartTypeList 图表类型列表 chartType 图表类型 chartData 图表数据 + */ +export default ({ questionType, data }) => { + const chartRef = ref(null) + const chartTypeList = ref([]) + const chartType = ref('') + const chartData = ref({}) + + watchEffect(() => { + if (questionType.value) { + // 根据题型获取图表类型列表 + chartTypeList.value = questionChartsConfig[questionType.value] || questionChartsConfig.default + if (!chartType.value) { + // 默认选中第一项 + chartType.value = chartTypeList.value?.[0] + } + if (chartType.value) { + // 根据图表类型获取图表数据 + chartData.value = dataFormateConfig[chartType.value](data) + } + } + }) + + return { + chartRef, + chartTypeList, + chartType, + chartData + } +} diff --git a/web/src/management/pages/analysis/AnalysisPage.vue b/web/src/management/pages/analysis/AnalysisPage.vue index 0577b8c3..46eb24c2 100644 --- a/web/src/management/pages/analysis/AnalysisPage.vue +++ b/web/src/management/pages/analysis/AnalysisPage.vue @@ -1,139 +1,28 @@ - diff --git a/web/src/management/pages/analysis/components/DataTable.vue b/web/src/management/pages/analysis/components/DataTable.vue index 4ad07baf..5c0e047d 100644 --- a/web/src/management/pages/analysis/components/DataTable.vue +++ b/web/src/management/pages/analysis/components/DataTable.vue @@ -17,22 +17,23 @@ minWidth="200" >