feat: 分题统计前端开发 (#276)

This commit is contained in:
hiStephen 2024-06-21 16:33:20 +08:00 committed by GitHub
parent 1f1dd86f89
commit cce508d17a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1067 additions and 171 deletions

2
web/components.d.ts vendored
View File

@ -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']

View File

@ -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",

View File

@ -8,3 +8,11 @@ export const getRecycleList = (data) => {
}
})
}
export const getStatisticList = (data) => {
return axios.get('/survey/dataStatistic/aggregationStatis', {
params: {
...data
}
})
}

View 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
}
]
}

View 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
}
]
}
}

View 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'
}
]
}
]
}
}

View File

@ -0,0 +1,9 @@
import pie from './pie'
import bar from './bar'
import gauge from './gauge'
export const getOption = {
pie,
bar,
gauge
}

View 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
}
]
}
}

View File

@ -1,4 +1,4 @@
const menuItems = {
export const menuItems = {
text: {
type: 'text',
snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp',

View 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 }
}

View 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 }
}

View 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
}
}

View File

@ -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>

View File

@ -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(/&nbsp;/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>

View 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(() => {
// dommounted
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>

View 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>

View File

@ -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>

View File

@ -139,7 +139,7 @@ import {
noSearchDataConfig,
selectOptionsDict,
buttonOptionsDict
} from '../config'
} from '@/management/config/listConfig'
const store = useStore()
const router = useRouter()

View File

@ -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'

View File

@ -6,7 +6,7 @@
</template>
<script>
import { statusMaps } from '../config'
import { statusMaps } from '@/management/config/listConfig'
export default {
name: 'StateModule',
props: {

View File

@ -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: {

View File

@ -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',

View File

@ -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';
}