feat: 分题统计前端开发 (#276)
This commit is contained in:
parent
1f1dd86f89
commit
cce508d17a
2
web/components.d.ts
vendored
2
web/components.d.ts
vendored
@ -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']
|
||||
|
@ -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",
|
||||
|
@ -8,3 +8,11 @@ export const getRecycleList = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getStatisticList = (data) => {
|
||||
return axios.get('/survey/dataStatistic/aggregationStatis', {
|
||||
params: {
|
||||
...data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
77
web/src/management/config/analysisConfig.js
Normal file
77
web/src/management/config/analysisConfig.js
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
}
|
57
web/src/management/config/chartConfig/bar.js
Normal file
57
web/src/management/config/chartConfig/bar.js
Normal file
@ -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} <br/>{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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
146
web/src/management/config/chartConfig/gauge.js
Normal file
146
web/src/management/config/chartConfig/gauge.js
Normal file
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
9
web/src/management/config/chartConfig/index.js
Normal file
9
web/src/management/config/chartConfig/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import pie from './pie'
|
||||
import bar from './bar'
|
||||
import gauge from './gauge'
|
||||
|
||||
export const getOption = {
|
||||
pie,
|
||||
bar,
|
||||
gauge
|
||||
}
|
57
web/src/management/config/chartConfig/pie.js
Normal file
57
web/src/management/config/chartConfig/pie.js
Normal file
@ -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} <br/>{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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
const menuItems = {
|
||||
export const menuItems = {
|
||||
text: {
|
||||
type: 'text',
|
||||
snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp',
|
||||
|
25
web/src/management/hooks/useCharts.js
Normal file
25
web/src/management/hooks/useCharts.js
Normal file
@ -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 }
|
||||
}
|
20
web/src/management/hooks/useResizeObserver.js
Normal file
20
web/src/management/hooks/useResizeObserver.js
Normal file
@ -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 }
|
||||
}
|
77
web/src/management/hooks/useStatisticsItemChart.js
Normal file
77
web/src/management/hooks/useStatisticsItemChart.js
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -1,139 +1,28 @@
|
||||
<template>
|
||||
<div class="analysis-page">
|
||||
<leftMenu class="left"></leftMenu>
|
||||
<div class="content-wrapper right">
|
||||
<template v-if="tableData.total">
|
||||
<h2 class="data-list">数据列表</h2>
|
||||
<div class="menus">
|
||||
<el-switch
|
||||
:model-value="isShowOriginData"
|
||||
active-text="是否展示原数据"
|
||||
@input="onIsShowOriginChange"
|
||||
<div class="right">
|
||||
<div class="analysis-tabs">
|
||||
<router-link
|
||||
v-for="item in analysisType"
|
||||
class="analysis-tabs__item"
|
||||
:key="item.value"
|
||||
:to="{ name: item.value }"
|
||||
>
|
||||
</el-switch>
|
||||
<i class="iconfont" :class="item.icon"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="tableData.total">
|
||||
<DataTable :main-table-loading="mainTableLoading" :table-data="tableData" />
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
popper-class="analysis-pagination"
|
||||
:total="tableData.total"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
</el-pagination>
|
||||
</template>
|
||||
<div v-else>
|
||||
<EmptyIndex :data="noDataConfig" />
|
||||
<div class="content-wrapper">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||
<script setup>
|
||||
import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||
import { getRecycleList } from '@/management/api/analysis'
|
||||
|
||||
import DataTable from './components/DataTable.vue'
|
||||
|
||||
export default {
|
||||
name: 'AnalysisPage',
|
||||
data() {
|
||||
return {
|
||||
mainTableLoading: false,
|
||||
tableData: {
|
||||
total: 0,
|
||||
listHead: [],
|
||||
listBody: []
|
||||
},
|
||||
noDataConfig: {
|
||||
title: '暂无数据',
|
||||
desc: '您的问卷当前还没有数据,快去回收问卷吧!',
|
||||
img: '/imgs/icons/analysis-empty.webp'
|
||||
},
|
||||
currentPage: 1,
|
||||
isShowOriginData: false,
|
||||
tmpIsShowOriginData: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
created() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
if (!this.$route.params.id) {
|
||||
ElMessage.error('没有传入问卷参数~')
|
||||
return
|
||||
}
|
||||
this.mainTableLoading = true
|
||||
try {
|
||||
const res = await getRecycleList({
|
||||
page: this.currentPage,
|
||||
surveyId: this.$route.params.id,
|
||||
isDesensitive: !this.tmpIsShowOriginData // 发起请求的时候,isShowOriginData还没改变,暂存了一个字段
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
const listHead = this.formatHead(res.data.listHead)
|
||||
this.tableData = { ...res.data, listHead }
|
||||
this.mainTableLoading = false
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('查询回收数据失败,请重试')
|
||||
}
|
||||
},
|
||||
handleCurrentChange(current) {
|
||||
if (this.mainTableLoading) {
|
||||
return
|
||||
}
|
||||
this.currentPage = current
|
||||
this.init()
|
||||
},
|
||||
formatHead(listHead = []) {
|
||||
const head = []
|
||||
|
||||
listHead.forEach((headItem) => {
|
||||
head.push({
|
||||
field: headItem.field,
|
||||
title: headItem.title
|
||||
})
|
||||
|
||||
if (headItem.othersCode?.length) {
|
||||
headItem.othersCode.forEach((item) => {
|
||||
head.push({
|
||||
field: item.code,
|
||||
title: `${headItem.title}-${item.option}`
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return head
|
||||
},
|
||||
async onIsShowOriginChange(data) {
|
||||
if (this.mainTableLoading) {
|
||||
return
|
||||
}
|
||||
// console.log(data)
|
||||
this.tmpIsShowOriginData = data
|
||||
await this.init()
|
||||
this.isShowOriginData = data
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
DataTable,
|
||||
EmptyIndex,
|
||||
LeftMenu
|
||||
}
|
||||
}
|
||||
import { analysisType } from '@/management/config/analysisConfig'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -152,29 +41,60 @@ export default {
|
||||
.right {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-left: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.menus {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 30px 40px 50px 40px;
|
||||
border-radius: 2px;
|
||||
background-color: #f6f7f9;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
|
||||
:deep(.el-pagination) {
|
||||
margin-top: 20px;
|
||||
min-width: 1160px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: #f6f7f9;
|
||||
|
||||
.analysis-tabs {
|
||||
flex: none;
|
||||
gap: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e7e9eb;
|
||||
|
||||
&__item {
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
color: #92949d;
|
||||
|
||||
.iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.data-list {
|
||||
margin-bottom: 20px;
|
||||
.router-link-active {
|
||||
color: $font-color-title;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(100% + 5px);
|
||||
height: 3px;
|
||||
background-color: $primary-color;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
padding: 24px 24px 24px 104px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -17,22 +17,23 @@
|
||||
minWidth="200"
|
||||
>
|
||||
<template #header="scope">
|
||||
<div class="table-row-cell">
|
||||
<span
|
||||
<div
|
||||
class="table-row-cell"
|
||||
@mouseover="onPopoverRefOver(scope, 'head')"
|
||||
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
|
||||
>
|
||||
<span>
|
||||
{{ scope.column.label.replace(/ /g, '') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<span
|
||||
<div
|
||||
class="table-row-cell"
|
||||
@mouseover="onPopoverRefOver(scope, 'content')"
|
||||
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
|
||||
>
|
||||
<span>
|
||||
{{ getContent(scope.row[scope.column.property]) }}
|
||||
</span>
|
||||
</div>
|
||||
@ -44,6 +45,7 @@
|
||||
popper-style="text-align: center;"
|
||||
:virtual-ref="popoverVirtualRef"
|
||||
placement="top"
|
||||
width="400"
|
||||
trigger="hover"
|
||||
virtual-triggering
|
||||
:content="popoverContent"
|
||||
@ -62,6 +64,10 @@ const props = defineProps({
|
||||
},
|
||||
mainTableLoading: {
|
||||
type: Boolean
|
||||
},
|
||||
tableMinHeight: {
|
||||
type: String,
|
||||
default: '620px'
|
||||
}
|
||||
})
|
||||
const popoverRefMap = ref({})
|
||||
@ -94,15 +100,18 @@ const onPopoverRefOver = (scope, type) => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
min-height: 620px;
|
||||
min-height: v-bind('tableMinHeight');
|
||||
background: #fff;
|
||||
padding: 10px 20px;
|
||||
|
||||
.table-border {
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
width: 100%;
|
||||
|
||||
.thead-cell .el-table__cell {
|
||||
.cell {
|
||||
height: 24px;
|
||||
@ -111,10 +120,16 @@ const onPopoverRefOver = (scope, type) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row-cell {
|
||||
white-space: nowrap; /* 禁止自动换行 */
|
||||
overflow: hidden; /* 超出部分隐藏 */
|
||||
text-overflow: ellipsis; /* 显示省略号 */
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
/* 禁止自动换行 */
|
||||
overflow: hidden;
|
||||
/* 超出部分隐藏 */
|
||||
text-overflow: ellipsis;
|
||||
/* 显示省略号 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
242
web/src/management/pages/analysis/components/StatisticsItem.vue
Normal file
242
web/src/management/pages/analysis/components/StatisticsItem.vue
Normal file
@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div class="separate-item">
|
||||
<div class="separate-item-title">
|
||||
<el-popover
|
||||
placement="top"
|
||||
width="400"
|
||||
trigger="hover"
|
||||
:disabled="!titlePoppverShow"
|
||||
:content="cleanRichText(StatisticsData.title)"
|
||||
>
|
||||
<template #reference>
|
||||
<p ref="titleRef" class="text" v-html="cleanRichText(StatisticsData.title)"></p>
|
||||
</template>
|
||||
</el-popover>
|
||||
<p v-if="questionTypeDesc" class="type">{{ questionTypeDesc }}</p>
|
||||
</div>
|
||||
<div class="separate-item-content">
|
||||
<div class="chart-wrapper">
|
||||
<div ref="chartRef" class="chart"></div>
|
||||
<div v-if="chartTypeList.length > 1" class="chart-type-list">
|
||||
<el-segmented v-model="chartType" :options="chartTypeList" size="small">
|
||||
<template #default="{ item }">
|
||||
<i class="iconfont" :class="`icon-${item}`"></i>
|
||||
</template>
|
||||
</el-segmented>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<data-table :table-data :table-min-height />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, toRefs, defineProps, computed, watch, onMounted, onUnmounted, ref } from 'vue'
|
||||
import _cloneDeep from 'lodash/cloneDeep'
|
||||
import {
|
||||
separateItemListHead,
|
||||
summaryType,
|
||||
summaryItemConfig
|
||||
} from '@/management/config/analysisConfig'
|
||||
import useCharts from '@/management/hooks/useCharts'
|
||||
import useStatisticsItemChart from '@/management/hooks/useStatisticsItemChart'
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
import { menuItems } from '@/management/config/questionMenuConfig'
|
||||
import DataTable from './DataTable.vue'
|
||||
import useResizeObserver from '@/management/hooks/useResizeObserver'
|
||||
|
||||
const props = defineProps({
|
||||
StatisticsData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const questionType = computed(() => {
|
||||
return props?.StatisticsData?.type
|
||||
})
|
||||
|
||||
const questionTypeDesc = computed(() => {
|
||||
return menuItems?.[questionType.value]?.title || ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const separateItemListBody = computed(() => {
|
||||
try {
|
||||
const aggregation = _cloneDeep(props?.StatisticsData?.data?.aggregation)
|
||||
const submitionCount = props?.StatisticsData?.data?.submitionCount
|
||||
const summaryList = summaryItemConfig[questionType.value]
|
||||
// 增加聚合信息
|
||||
if (summaryList?.length) {
|
||||
summaryList.forEach((item, index) => {
|
||||
const { type, text, field, max, min } = item
|
||||
if (text && field && type === summaryType.between) {
|
||||
aggregation.push({
|
||||
id: `summary_${index}`,
|
||||
text,
|
||||
count: aggregation.reduce((n, item) => {
|
||||
if (item[field] >= min && item[field] <= max) {
|
||||
return n + item.count
|
||||
}
|
||||
return n
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
aggregation?.map((item) => {
|
||||
const { id, count, text } = item
|
||||
const percent = submitionCount ? `${((count / submitionCount) * 100).toFixed(1)}%` : '0%'
|
||||
return {
|
||||
id,
|
||||
count,
|
||||
text,
|
||||
percent
|
||||
}
|
||||
}) || []
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const separateItemState = reactive({
|
||||
tableData: {
|
||||
total: 0,
|
||||
listHead: separateItemListHead,
|
||||
listBody: separateItemListBody
|
||||
},
|
||||
tableMinHeight: '0px'
|
||||
})
|
||||
|
||||
const { tableData, tableMinHeight } = toRefs(separateItemState)
|
||||
|
||||
const titlePoppverShow = ref(false)
|
||||
const titleRef = ref(null)
|
||||
|
||||
const titleResize = () => {
|
||||
if (titleRef.value?.scrollWidth > titleRef.value?.offsetWidth) {
|
||||
titlePoppverShow.value = true
|
||||
} else {
|
||||
titlePoppverShow.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const { chartRef, chartTypeList, chartType, chartData } = useStatisticsItemChart({
|
||||
questionType,
|
||||
data: props?.StatisticsData?.data
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 需要获取图表dom,所以得在mounted中执行
|
||||
const { changeType, resize: chartResize } = useCharts(
|
||||
chartRef.value,
|
||||
chartType.value,
|
||||
chartData.value
|
||||
)
|
||||
|
||||
const { destroy } = useResizeObserver(chartRef.value, () => {
|
||||
chartResize()
|
||||
titleResize()
|
||||
})
|
||||
|
||||
// 图型切换
|
||||
watch(chartType, () => {
|
||||
changeType(chartType.value, chartData.value)
|
||||
})
|
||||
|
||||
// 销毁resizeObserver
|
||||
onUnmounted(destroy)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.separate-item {
|
||||
padding: 32px 12px;
|
||||
border-bottom: 1px solid #efefef;
|
||||
|
||||
&:nth-last-of-type(1) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
max-width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.type {
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
color: white;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 7px 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 50px;
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
flex: auto;
|
||||
width: 50%;
|
||||
min-width: 300px;
|
||||
height: 320px;
|
||||
max-width: 1000px;
|
||||
box-shadow: 0 2px 8px -2px rgba(136, 136, 157, 0.2);
|
||||
border-radius: 2px;
|
||||
padding: 24px;
|
||||
|
||||
.chart-type-list {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
.iconfont {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: auto;
|
||||
width: 50%;
|
||||
min-width: 300px;
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1660px) {
|
||||
&-content {
|
||||
gap: 80px;
|
||||
|
||||
.chart-wrapper {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
142
web/src/management/pages/analysis/pages/DataTablePage.vue
Normal file
142
web/src/management/pages/analysis/pages/DataTablePage.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="data-table-page">
|
||||
<template v-if="tableData.total">
|
||||
<div class="menus">
|
||||
<el-switch
|
||||
:model-value="isShowOriginData"
|
||||
active-text="是否展示原数据"
|
||||
@input="onIsShowOriginChange"
|
||||
>
|
||||
</el-switch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="tableData.total">
|
||||
<DataTable :main-table-loading :table-data />
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
popper-class="analysis-pagination"
|
||||
:total="tableData.total"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
</el-pagination>
|
||||
</template>
|
||||
<div v-else>
|
||||
<EmptyIndex :data="noDataConfig" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||
import { getRecycleList } from '@/management/api/analysis'
|
||||
import { noDataConfig } from '@/management/config/analysisConfig'
|
||||
import DataTable from '../components/DataTable.vue'
|
||||
|
||||
const dataTableState = reactive({
|
||||
mainTableLoading: false,
|
||||
tableData: {
|
||||
total: 0,
|
||||
listHead: [],
|
||||
listBody: []
|
||||
},
|
||||
currentPage: 1,
|
||||
isShowOriginData: false,
|
||||
tmpIsShowOriginData: false
|
||||
})
|
||||
|
||||
const { mainTableLoading, tableData, isShowOriginData } = toRefs(dataTableState)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const formatHead = (listHead) => {
|
||||
const head = []
|
||||
|
||||
listHead.forEach((headItem) => {
|
||||
head.push({
|
||||
field: headItem.field,
|
||||
title: headItem.title
|
||||
})
|
||||
|
||||
if (headItem.othersCode?.length) {
|
||||
headItem.othersCode.forEach((item) => {
|
||||
head.push({
|
||||
field: item.code,
|
||||
title: `${headItem.title}-${item.option}`
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return head
|
||||
}
|
||||
|
||||
const onIsShowOriginChange = async (data) => {
|
||||
if (dataTableState.mainTableLoading) {
|
||||
return
|
||||
}
|
||||
dataTableState.tmpIsShowOriginData = data
|
||||
await init()
|
||||
dataTableState.isShowOriginData = data
|
||||
}
|
||||
|
||||
const handleCurrentChange = async (page) => {
|
||||
if (dataTableState.mainTableLoading) {
|
||||
return
|
||||
}
|
||||
dataTableState.currentPage = page
|
||||
await init()
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (!route.params.id) {
|
||||
ElMessage.error('没有传入问卷参数~')
|
||||
return
|
||||
}
|
||||
dataTableState.mainTableLoading = true
|
||||
try {
|
||||
const res = await getRecycleList({
|
||||
page: dataTableState.currentPage,
|
||||
surveyId: route.params.id,
|
||||
isDesensitive: !dataTableState.tmpIsShowOriginData // 发起请求的时候,isShowOriginData还没改变,暂存了一个字段
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
const listHead = formatHead(res.data.listHead)
|
||||
dataTableState.tableData = { ...res.data, listHead }
|
||||
dataTableState.mainTableLoading = false
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('查询回收数据失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-table-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menus {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-pagination) {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.data-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div v-if="data.length" class="separate-statis-page">
|
||||
<StatisticsItem v-for="StatisticsData in data" :key="StatisticsData.field" :StatisticsData />
|
||||
</div>
|
||||
<div v-else>
|
||||
<EmptyIndex :data="noDataConfig" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import { noDataConfig } from '@/management/config/analysisConfig'
|
||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||
import { getStatisticList } from '@/management/api/analysis'
|
||||
import StatisticsItem from '../components/StatisticsItem.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const initData = async () => {
|
||||
try {
|
||||
const res = await getStatisticList({
|
||||
surveyId: route.params.id
|
||||
})
|
||||
if (res.code === 200) {
|
||||
data.value = res?.data || []
|
||||
} else {
|
||||
ElMessage.error(res?.errmsg)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '查询回收数据失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(initData)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.separate-statis-page {
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
@ -139,7 +139,7 @@ import {
|
||||
noSearchDataConfig,
|
||||
selectOptionsDict,
|
||||
buttonOptionsDict
|
||||
} from '../config'
|
||||
} from '@/management/config/listConfig'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
@ -75,7 +75,7 @@ import { useStore } from 'vuex'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||
import { get, map } from 'lodash-es'
|
||||
import { spaceListConfig } from '../config'
|
||||
import { spaceListConfig } from '@/management/config/listConfig'
|
||||
import SpaceModify from './SpaceModify.vue'
|
||||
import { UserRole } from '@/management/utils/types/workSpace'
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { statusMaps } from '../config'
|
||||
import { statusMaps } from '@/management/config/listConfig'
|
||||
export default {
|
||||
name: 'StateModule',
|
||||
props: {
|
||||
|
@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { type as surveyType } from '../config'
|
||||
import { type as surveyType } from '@/management/config/listConfig'
|
||||
export default {
|
||||
name: 'TagModule',
|
||||
props: {
|
||||
|
@ -2,6 +2,7 @@ import { createRouter, createWebHistory, type RouteLocationNormalized, type Navi
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useStore, type Store } from 'vuex'
|
||||
import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
||||
import { analysisTypeMap } from '@/management/config/analysisConfig'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
@ -92,11 +93,34 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/survey/:id/analysis',
|
||||
name: 'analysisPage',
|
||||
redirect: {
|
||||
name: analysisTypeMap.dataTable
|
||||
},
|
||||
meta: {
|
||||
needLogin: true,
|
||||
permissions: [SurveyPermissions.DataManage]
|
||||
},
|
||||
component: () => import('../pages/analysis/AnalysisPage.vue')
|
||||
component: () => import('../pages/analysis/AnalysisPage.vue'),
|
||||
children: [
|
||||
{
|
||||
path: analysisTypeMap.dataTable,
|
||||
name: analysisTypeMap.dataTable,
|
||||
meta: {
|
||||
needLogin: true,
|
||||
premissions: [SurveyPermissions.DataManage]
|
||||
},
|
||||
component: () => import('../pages/analysis/pages/DataTablePage.vue')
|
||||
},
|
||||
{
|
||||
path: analysisTypeMap.separateStatistics,
|
||||
name: analysisTypeMap.separateStatistics,
|
||||
meta: {
|
||||
needLogin: true,
|
||||
premissions: [SurveyPermissions.DataManage]
|
||||
},
|
||||
component: () => import('../pages/analysis/pages/SeparateStatisticsPage.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/survey/:id/publish',
|
||||
|
@ -1,9 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'iconfont'; /* Project id 4263849 */
|
||||
font-family: 'iconfont';
|
||||
/* Project id 4263849 */
|
||||
src:
|
||||
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.woff2?t=1716556097756') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.woff?t=1716556097756') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.ttf?t=1716556097756') format('truetype');
|
||||
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff2?t=1717580126029') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff?t=1717580126029') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.ttf?t=1717580126029') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -125,15 +126,39 @@
|
||||
.icon-erweima:before {
|
||||
content: '\e6c0';
|
||||
}
|
||||
|
||||
.icon-yangshishezhi:before {
|
||||
content: '\e6e6';
|
||||
}
|
||||
|
||||
.icon-NPSpingfen::before {
|
||||
content: '\e6e7';
|
||||
}
|
||||
|
||||
.icon-wodekongjian::before {
|
||||
content: '\e6ee';
|
||||
}
|
||||
|
||||
.icon-tuanduikongjian::before {
|
||||
content: '\e6ec';
|
||||
}
|
||||
|
||||
.icon-shujuliebiao:before {
|
||||
content: '\e6f2';
|
||||
}
|
||||
|
||||
.icon-fentitongji:before {
|
||||
content: '\e6f3';
|
||||
}
|
||||
|
||||
.icon-bar:before {
|
||||
content: '\e600';
|
||||
}
|
||||
|
||||
.icon-pie:before {
|
||||
content: '\e606';
|
||||
}
|
||||
|
||||
.icon-gauge:before {
|
||||
content: '\e6db';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user