feat: init
This commit is contained in:
280
src/views/visualization/data-analysis/components/chain-item.vue
Normal file
280
src/views/visualization/data-analysis/components/chain-item.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card :bordered="false" :style="cardStyle">
|
||||
<div class="content-wrap">
|
||||
<div class="content">
|
||||
<a-statistic :title="title" :value="renderData.count" :value-from="0" animation show-group-separator />
|
||||
<div class="desc">
|
||||
<a-typography-text type="secondary" class="label">
|
||||
{{ $t('dataAnalysis.card.yesterday') }}
|
||||
</a-typography-text>
|
||||
<a-typography-text type="danger">
|
||||
{{ renderData.growth }}
|
||||
<icon-arrow-rise />
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<Chart v-if="!loading" :option="chartOption" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PublicOpinionAnalysis, PublicOpinionAnalysisRes, queryPublicOpinionAnalysis } from '@/api/visualization'
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { CSSProperties, PropType, ref } from 'vue'
|
||||
|
||||
const barChartOptionsFactory = () => {
|
||||
const data = ref<any>([])
|
||||
// @ts-ignore
|
||||
const { chartOption } = useChartOption(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 10,
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
show: false,
|
||||
},
|
||||
yAxis: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
},
|
||||
series: {
|
||||
name: 'total',
|
||||
data,
|
||||
type: 'bar',
|
||||
barWidth: 7,
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
return {
|
||||
data,
|
||||
chartOption,
|
||||
}
|
||||
}
|
||||
|
||||
const lineChartOptionsFactory = () => {
|
||||
const data = ref<number[][]>([[], []])
|
||||
const { chartOption } = useChartOption(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 10,
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
show: false,
|
||||
},
|
||||
yAxis: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '2001',
|
||||
data: data.value[0],
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#165DFF',
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '2002',
|
||||
data: data.value[1],
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#6AA1FF',
|
||||
width: 3,
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
return {
|
||||
data,
|
||||
chartOption,
|
||||
}
|
||||
}
|
||||
|
||||
const pieChartOptionsFactory = () => {
|
||||
const data = ref<any>([])
|
||||
// @ts-ignore
|
||||
const { chartOption } = useChartOption(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
top: 'center',
|
||||
right: '0',
|
||||
orient: 'vertical',
|
||||
icon: 'circle',
|
||||
itemWidth: 6,
|
||||
itemHeight: 6,
|
||||
textStyle: {
|
||||
color: '#4E5969',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总计',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
data,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
return {
|
||||
data,
|
||||
chartOption,
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
quota: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
chartType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
cardStyle: {
|
||||
type: Object as PropType<CSSProperties>,
|
||||
default: () => {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const { chartOption: lineChartOption, data: lineData } = lineChartOptionsFactory()
|
||||
const { chartOption: barChartOption, data: barData } = barChartOptionsFactory()
|
||||
const { chartOption: pieChartOption, data: pieData } = pieChartOptionsFactory()
|
||||
const renderData = ref<PublicOpinionAnalysisRes>({
|
||||
count: 0,
|
||||
growth: 0,
|
||||
chartData: [],
|
||||
})
|
||||
const chartOption = ref({})
|
||||
const fetchData = async (params: PublicOpinionAnalysis) => {
|
||||
try {
|
||||
const { data } = await queryPublicOpinionAnalysis(params)
|
||||
renderData.value = data
|
||||
const { chartData } = data
|
||||
if (props.chartType === 'bar') {
|
||||
chartData.forEach((el, idx) => {
|
||||
barData.value.push({
|
||||
value: el.y,
|
||||
itemStyle: {
|
||||
color: idx % 2 ? '#2CAB40' : '#86DF6C',
|
||||
},
|
||||
})
|
||||
})
|
||||
chartOption.value = barChartOption.value
|
||||
} else if (props.chartType === 'line') {
|
||||
chartData.forEach((el) => {
|
||||
if (el.name === '2021') {
|
||||
lineData.value[0].push(el.y)
|
||||
} else {
|
||||
lineData.value[1].push(el.y)
|
||||
}
|
||||
})
|
||||
chartOption.value = lineChartOption.value
|
||||
} else {
|
||||
chartData.forEach((el) => {
|
||||
pieData.value.push(el)
|
||||
})
|
||||
chartOption.value = pieChartOption.value
|
||||
}
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData({ quota: props.quota })
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-card) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
:deep(.arco-card-body) {
|
||||
width: 100%;
|
||||
height: 134px;
|
||||
padding: 0;
|
||||
}
|
||||
.content-wrap {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:deep(.content) {
|
||||
float: left;
|
||||
width: 108px;
|
||||
height: 102px;
|
||||
}
|
||||
:deep(.arco-statistic) {
|
||||
.arco-statistic-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.arco-statistic-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
float: right;
|
||||
width: calc(100% - 108px);
|
||||
height: 90px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-right: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" :header-style="{ paddingBottom: '16px' }">
|
||||
<template #title>
|
||||
{{ $t('dataAnalysis.contentPeriodAnalysis') }}
|
||||
</template>
|
||||
<Chart style="width: 100%; height: 370px" :option="chartOption" />
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { queryContentPeriodAnalysis } from '@/api/visualization'
|
||||
import { ToolTipFormatterParams } from '@/types/echarts'
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
|
||||
const tooltipItemsHtmlString = (items: ToolTipFormatterParams[]) => {
|
||||
return items
|
||||
.map(
|
||||
(el) => `<div class="content-panel">
|
||||
<p>
|
||||
<span style="background-color: ${el.color}" class="tooltip-item-icon"></span>
|
||||
<span>${el.seriesName}</span>
|
||||
</p>
|
||||
<span class="tooltip-value">
|
||||
${el.value}%
|
||||
</span>
|
||||
</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const xAxis = ref<string[]>([])
|
||||
const textChartsData = ref<number[]>([])
|
||||
const imgChartsData = ref<number[]>([])
|
||||
const videoChartsData = ref<number[]>([])
|
||||
const { chartOption } = useChartOption((isDark) => {
|
||||
return {
|
||||
grid: {
|
||||
left: '40',
|
||||
right: 0,
|
||||
top: '20',
|
||||
bottom: '100',
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
color: '#4E5969',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxis.value,
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#3f3f3f' : '#A9AEB8',
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
alignWithLabel: true,
|
||||
lineStyle: {
|
||||
color: '#86909C',
|
||||
},
|
||||
interval(idx: number) {
|
||||
if (idx === 0) return false
|
||||
if (idx === xAxis.value.length - 1) return false
|
||||
return true
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#86909C',
|
||||
formatter(value: string, idx: number) {
|
||||
if (idx === 0) return ''
|
||||
if (idx === xAxis.value.length - 1) return ''
|
||||
return `${value}`
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#86909C',
|
||||
formatter: '{value}%',
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#3F3F3F' : '#E5E6EB',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
formatter(params) {
|
||||
const [firstElement] = params as ToolTipFormatterParams[]
|
||||
return `<div>
|
||||
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
|
||||
${tooltipItemsHtmlString(params as ToolTipFormatterParams[])}
|
||||
</div>`
|
||||
},
|
||||
className: 'echarts-tooltip-diy',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '纯文本',
|
||||
data: textChartsData.value,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
color: isDark ? '#3D72F6' : '#246EFF',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#E0E3FF',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '图文类',
|
||||
data: imgChartsData.value,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
color: isDark ? '#A079DC' : '#00B2FF',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#E2F2FF',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '视频类',
|
||||
data: videoChartsData.value,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
color: isDark ? '#6CAAF5' : '#81E2FF',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#D9F6FF',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
bottom: 40,
|
||||
type: 'slider',
|
||||
left: 40,
|
||||
right: 14,
|
||||
height: 14,
|
||||
borderColor: 'transparent',
|
||||
handleIcon:
|
||||
'image://http://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/1ee5a8c6142b2bcf47d2a9f084096447.svg~tplv-49unhts6dw-image.image',
|
||||
handleSize: '20',
|
||||
handleStyle: {
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
shadowBlur: 4,
|
||||
},
|
||||
brushSelect: false,
|
||||
backgroundColor: isDark ? '#313132' : '#F2F3F5',
|
||||
},
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
zoomOnMouseWheel: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data: chartData } = await queryContentPeriodAnalysis()
|
||||
xAxis.value = chartData.xAxis
|
||||
chartData.data.forEach((el) => {
|
||||
if (el.name === '纯文本') {
|
||||
textChartsData.value = el.value
|
||||
} else if (el.name === '图文类') {
|
||||
imgChartsData.value = el.value
|
||||
}
|
||||
videoChartsData.value = el.value
|
||||
})
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 230px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" :header-style="{ paddingBottom: '14px' }">
|
||||
<template #title>
|
||||
{{ $t('dataAnalysis.contentPublishRatio') }}
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-link>{{ $t('workplace.viewMore') }}</a-link>
|
||||
</template>
|
||||
<Chart style="width: 100%; height: 347px" :option="chartOption" />
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { ToolTipFormatterParams } from '@/types/echarts'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { queryContentPublish, ContentPublishRecord } from '@/api/visualization'
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
|
||||
const tooltipItemsHtmlString = (items: ToolTipFormatterParams[]) => {
|
||||
return items
|
||||
.map(
|
||||
(el) => `<div class="content-panel">
|
||||
<p>
|
||||
<span style="background-color: ${el.color}" class="tooltip-item-icon"></span>
|
||||
<span>
|
||||
${el.seriesName}
|
||||
</span>
|
||||
</p>
|
||||
<span class="tooltip-value">
|
||||
${Number(el.value).toLocaleString()}
|
||||
</span>
|
||||
</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const xAxis = ref<string[]>([])
|
||||
const textChartsData = ref<number[]>([])
|
||||
const imgChartsData = ref<number[]>([])
|
||||
const videoChartsData = ref<number[]>([])
|
||||
const { chartOption } = useChartOption((isDark) => {
|
||||
return {
|
||||
grid: {
|
||||
left: '4%',
|
||||
right: 0,
|
||||
top: '20',
|
||||
bottom: '60',
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
color: '#4E5969',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxis.value,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#3f3f3f' : '#A9AEB8',
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
alignWithLabel: true,
|
||||
lineStyle: {
|
||||
color: '#86909C',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#86909C',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#86909C',
|
||||
formatter(value: number, idx: number) {
|
||||
if (idx === 0) return `${value}`
|
||||
return `${value / 1000}k`
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#3F3F3F' : '#E5E6EB',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
formatter(params) {
|
||||
const [firstElement] = params as ToolTipFormatterParams[]
|
||||
return `<div>
|
||||
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
|
||||
${tooltipItemsHtmlString(params as ToolTipFormatterParams[])}
|
||||
</div>`
|
||||
},
|
||||
className: 'echarts-tooltip-diy',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '纯文本',
|
||||
data: textChartsData.value,
|
||||
stack: 'one',
|
||||
type: 'bar',
|
||||
barWidth: 16,
|
||||
color: isDark ? '#4A7FF7' : '#246EFF',
|
||||
},
|
||||
{
|
||||
name: '图文类',
|
||||
data: imgChartsData.value,
|
||||
stack: 'one',
|
||||
type: 'bar',
|
||||
color: isDark ? '#085FEF' : '#00B2FF',
|
||||
},
|
||||
{
|
||||
name: '视频类',
|
||||
data: videoChartsData.value,
|
||||
stack: 'one',
|
||||
type: 'bar',
|
||||
color: isDark ? '#01349F' : '#81E2FF',
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data: chartData } = await queryContentPublish()
|
||||
xAxis.value = chartData[0].x
|
||||
chartData.forEach((el: ContentPublishRecord) => {
|
||||
if (el.name === '纯文本') {
|
||||
textChartsData.value = el.y
|
||||
} else if (el.name === '图文类') {
|
||||
imgChartsData.value = el.y
|
||||
}
|
||||
videoChartsData.value = el.y
|
||||
})
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" :header-style="{ paddingBottom: '14px' }">
|
||||
<template #title>
|
||||
{{ $t('dataAnalysis.popularAuthor') }}
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-link>{{ $t('workplace.viewMore') }}</a-link>
|
||||
</template>
|
||||
<a-table :data="tableData.list" :pagination="false" :bordered="false" style="margin-bottom: 20px" :scroll="{ x: '100%', y: '350px' }">
|
||||
<template #columns>
|
||||
<a-table-column :title="$t('dataAnalysis.popularAuthor.column.ranking')" data-index="ranking"></a-table-column>
|
||||
<a-table-column :title="$t('dataAnalysis.popularAuthor.column.author')" data-index="author"></a-table-column>
|
||||
<a-table-column
|
||||
:title="$t('dataAnalysis.popularAuthor.column.content')"
|
||||
data-index="contentCount"
|
||||
:sortable="{
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
}"
|
||||
></a-table-column>
|
||||
<a-table-column
|
||||
:title="$t('dataAnalysis.popularAuthor.column.click')"
|
||||
data-index="clickCount"
|
||||
:sortable="{
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
}"
|
||||
></a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { queryPopularAuthor, PopularAuthorRes } from '@/api/visualization'
|
||||
|
||||
const { loading, setLoading } = useLoading()
|
||||
const tableData = ref<PopularAuthorRes>({ list: [] })
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { data } = await queryPopularAuthor()
|
||||
tableData.value = data
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.general-card {
|
||||
max-height: 425px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('dataAnalysis.title.publicOpinion')" :header-style="{ paddingBottom: '12px' }">
|
||||
<a-grid :cols="24" :col-gap="12" :row-gap="12">
|
||||
<a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
|
||||
<ChainItem
|
||||
:title="$t('dataAnalysis.card.title.allVisitors')"
|
||||
quota="visitors"
|
||||
chart-type="line"
|
||||
:card-style="{
|
||||
background: isDark ? 'linear-gradient(180deg, #284991 0%, #122B62 100%)' : 'linear-gradient(180deg, #f2f9fe 0%, #e6f4fe 100%)',
|
||||
}"
|
||||
/>
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
|
||||
<ChainItem
|
||||
:title="$t('dataAnalysis.card.title.contentPublished')"
|
||||
quota="published"
|
||||
chart-type="bar"
|
||||
:card-style="{
|
||||
background: isDark ? ' linear-gradient(180deg, #3D492E 0%, #263827 100%)' : 'linear-gradient(180deg, #F5FEF2 0%, #E6FEEE 100%)',
|
||||
}"
|
||||
/>
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
|
||||
<ChainItem
|
||||
:title="$t('dataAnalysis.card.title.totalComment')"
|
||||
quota="comment"
|
||||
chart-type="line"
|
||||
:card-style="{
|
||||
background: isDark ? 'linear-gradient(180deg, #294B94 0%, #0F275C 100%)' : 'linear-gradient(180deg, #f2f9fe 0%, #e6f4fe 100%)',
|
||||
}"
|
||||
/>
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 6, xxl: 6 }">
|
||||
<ChainItem
|
||||
:title="$t('dataAnalysis.card.title.totalShare')"
|
||||
quota="share"
|
||||
chart-type="pie"
|
||||
:card-style="{
|
||||
background: isDark ? 'linear-gradient(180deg, #312565 0%, #201936 100%)' : 'linear-gradient(180deg, #F7F7FF 0%, #ECECFF 100%)',
|
||||
}"
|
||||
/>
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useThemes from '@/hooks/themes'
|
||||
import ChainItem from './chain-item.vue'
|
||||
|
||||
const { isDark } = useThemes()
|
||||
</script>
|
||||
59
src/views/visualization/data-analysis/index.vue
Normal file
59
src/views/visualization/data-analysis/index.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.visualization', 'menu.visualization.dataAnalysis']" />
|
||||
<a-space direction="vertical" :size="12" fill>
|
||||
<a-space direction="vertical" :size="16" fill>
|
||||
<div class="space-unit">
|
||||
<PublicOpinion />
|
||||
</div>
|
||||
<div>
|
||||
<a-grid :cols="24" :col-gap="16" :row-gap="16">
|
||||
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 16, xxl: 16 }">
|
||||
<ContentPublishRatio />
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 8, xxl: 8 }">
|
||||
<PopularAuthor />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
<div>
|
||||
<ContentPeriodAnalysis />
|
||||
</div>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PublicOpinion from './components/public-opinion.vue'
|
||||
import ContentPeriodAnalysis from './components/content-period-analysis.vue'
|
||||
import ContentPublishRatio from './components/content-publish-ratio.vue'
|
||||
import PopularAuthor from './components/popular-author.vue'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataAnalysis',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.space-unit {
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-fix {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14;
|
||||
}
|
||||
:deep(.section-title) {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
16
src/views/visualization/data-analysis/locale/en-US.ts
Normal file
16
src/views/visualization/data-analysis/locale/en-US.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
'menu.visualization.dataAnalysis': 'Analysis',
|
||||
'dataAnalysis.title.publicOpinion': 'Public Opinion Analysis',
|
||||
'dataAnalysis.card.title.allVisitors': 'All Visitors',
|
||||
'dataAnalysis.card.title.contentPublished': 'Content Published',
|
||||
'dataAnalysis.card.title.totalComment': 'Total Comment',
|
||||
'dataAnalysis.card.title.totalShare': 'Total Share',
|
||||
'dataAnalysis.card.yesterday': 'Yesterday',
|
||||
'dataAnalysis.contentPublishRatio': 'Content Publishing Ratio',
|
||||
'dataAnalysis.popularAuthor': 'Popular Author',
|
||||
'dataAnalysis.popularAuthor.column.ranking': 'ranking',
|
||||
'dataAnalysis.popularAuthor.column.author': 'author',
|
||||
'dataAnalysis.popularAuthor.column.content': 'Content Number',
|
||||
'dataAnalysis.popularAuthor.column.click': 'Click Number',
|
||||
'dataAnalysis.contentPeriodAnalysis': 'Content Period Analysis',
|
||||
}
|
||||
16
src/views/visualization/data-analysis/locale/zh-CN.ts
Normal file
16
src/views/visualization/data-analysis/locale/zh-CN.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
'menu.visualization.dataAnalysis': '分析页',
|
||||
'dataAnalysis.title.publicOpinion': '舆情分析',
|
||||
'dataAnalysis.card.title.allVisitors': '访问总人次',
|
||||
'dataAnalysis.card.title.contentPublished': '内容发布量',
|
||||
'dataAnalysis.card.title.totalComment': '评论总量',
|
||||
'dataAnalysis.card.title.totalShare': '分享总量',
|
||||
'dataAnalysis.card.yesterday': '较昨日',
|
||||
'dataAnalysis.contentPublishRatio': '内容发布比例',
|
||||
'dataAnalysis.popularAuthor': '热门作者榜单',
|
||||
'dataAnalysis.popularAuthor.column.ranking': '排名',
|
||||
'dataAnalysis.popularAuthor.column.author': '作者',
|
||||
'dataAnalysis.popularAuthor.column.content': '内容量',
|
||||
'dataAnalysis.popularAuthor.column.click': '点击量',
|
||||
'dataAnalysis.contentPeriodAnalysis': '内容时段分析',
|
||||
}
|
||||
97
src/views/visualization/data-analysis/mock.ts
Normal file
97
src/views/visualization/data-analysis/mock.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
import { PostData } from '@/types/global'
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
Mock.mock(new RegExp('/api/public-opinion-analysis'), (params: PostData) => {
|
||||
const { quota = 'visitors' } = JSON.parse(params.body)
|
||||
if (['visitors', 'comment'].includes(quota)) {
|
||||
const year = new Date().getFullYear()
|
||||
const getLineData = (name: number) => {
|
||||
return new Array(12).fill(0).map((_item, index) => ({
|
||||
x: `${index + 1}月`,
|
||||
y: Mock.Random.natural(0, 100),
|
||||
name: String(name),
|
||||
}))
|
||||
}
|
||||
return successResponseWrap({
|
||||
count: 5670,
|
||||
growth: 206.32,
|
||||
chartData: [...getLineData(year), ...getLineData(year - 1)],
|
||||
})
|
||||
}
|
||||
if (['published'].includes(quota)) {
|
||||
const year = new Date().getFullYear()
|
||||
const getLineData = (name: number) => {
|
||||
return new Array(12).fill(0).map((_item, index) => ({
|
||||
x: `${index + 1}日`,
|
||||
y: Mock.Random.natural(20, 100),
|
||||
name: String(name),
|
||||
}))
|
||||
}
|
||||
return successResponseWrap({
|
||||
count: 5670,
|
||||
growth: 206.32,
|
||||
chartData: [...getLineData(year)],
|
||||
})
|
||||
}
|
||||
return successResponseWrap({
|
||||
count: 5670,
|
||||
growth: 206.32,
|
||||
chartData: [
|
||||
// itemStyle for demo
|
||||
{ name: '文本类', value: 25, itemStyle: { color: '#8D4EDA' } },
|
||||
{ name: '图文类', value: 35, itemStyle: { color: '#165DFF' } },
|
||||
{ name: '视频类', value: 40, itemStyle: { color: '#00B2FF' } },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
Mock.mock(new RegExp('/api/content-period-analysis'), () => {
|
||||
const getLineData = (name: string) => {
|
||||
return {
|
||||
name,
|
||||
value: new Array(12).fill(0).map(() => Mock.Random.natural(30, 90)),
|
||||
}
|
||||
}
|
||||
return successResponseWrap({
|
||||
xAxis: new Array(12).fill(0).map((_item, index) => `${index * 2}:00`),
|
||||
data: [getLineData('纯文本'), getLineData('图文类'), getLineData('视频类')],
|
||||
})
|
||||
})
|
||||
|
||||
Mock.mock(new RegExp('/api/content-publish'), () => {
|
||||
const generateLineData = (name: string) => {
|
||||
const result = {
|
||||
name,
|
||||
x: [] as string[],
|
||||
y: [] as number[],
|
||||
}
|
||||
new Array(12).fill(0).forEach((_item, index) => {
|
||||
result.x.push(`${index * 2}:00`)
|
||||
result.y.push(Mock.Random.natural(1000, 3000))
|
||||
})
|
||||
return result
|
||||
}
|
||||
return successResponseWrap([generateLineData('纯文本'), generateLineData('图文类'), generateLineData('视频类')])
|
||||
})
|
||||
|
||||
Mock.mock(new RegExp('/api/popular-author/list'), () => {
|
||||
const generateData = () => {
|
||||
const list = new Array(7).fill(0).map((_item, index) => ({
|
||||
ranking: index + 1,
|
||||
author: Mock.mock('@ctitle(5)'),
|
||||
contentCount: Mock.mock(/[0-9]{4}/),
|
||||
clickCount: Mock.mock(/[0-9]{4}/),
|
||||
}))
|
||||
return {
|
||||
list,
|
||||
}
|
||||
}
|
||||
return successResponseWrap({
|
||||
...generateData(),
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" :title="title" :header-style="{ paddingBottom: '12px' }">
|
||||
<div class="content">
|
||||
<a-statistic :value="count" :show-group-separator="true" :value-from="0" animation />
|
||||
<a-typography-text class="percent-text" :type="isUp ? 'danger' : 'success'">
|
||||
{{ growth }}%
|
||||
<icon-arrow-rise v-if="isUp" />
|
||||
<icon-arrow-fall v-else />
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<Chart :option="chartOption" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { queryDataChainGrowth, DataChainGrowth } from '@/api/visualization'
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
quota: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
chartType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const count = ref(0)
|
||||
const growth = ref(100)
|
||||
const isUp = computed(() => growth.value > 50)
|
||||
const chartData = ref<any>([])
|
||||
const { chartOption } = useChartOption(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
show: false,
|
||||
},
|
||||
yAxis: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
formatter: '{c}',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: chartData.value,
|
||||
...(props.chartType === 'bar'
|
||||
? {
|
||||
type: 'bar',
|
||||
barWidth: 7,
|
||||
barGap: '0',
|
||||
}
|
||||
: {
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#4080FF',
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const fetchData = async (params: DataChainGrowth) => {
|
||||
try {
|
||||
const { data } = await queryDataChainGrowth(params)
|
||||
const { chartData: resChartData } = data
|
||||
count.value = data.count
|
||||
growth.value = data.growth
|
||||
resChartData.data.value.forEach((el, idx) => {
|
||||
if (props.chartType === 'bar') {
|
||||
chartData.value.push({
|
||||
value: el,
|
||||
itemStyle: {
|
||||
color: idx % 2 ? '#468DFF' : '#86DF6C',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
chartData.value.push(el)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData({ quota: props.quota })
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.general-card {
|
||||
min-height: 204px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.percent-text {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.unit {
|
||||
padding-left: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-right: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" :title="$t('multiDAnalysis.card.title.contentPublishingSource')">
|
||||
<Chart style="width: 100%; height: 300px" :option="chartOption" />
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading'
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
|
||||
const { chartOption } = useChartOption((isDark) => {
|
||||
const graphicElementStyle = {
|
||||
textAlign: 'center',
|
||||
fill: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969',
|
||||
fontSize: 14,
|
||||
lineWidth: 10,
|
||||
fontWeight: 'bold',
|
||||
}
|
||||
return {
|
||||
legend: {
|
||||
left: 'center',
|
||||
data: ['UGC原创', '国外网站', '转载文章', '行业报告', '其他'],
|
||||
bottom: 0,
|
||||
icon: 'circle',
|
||||
itemWidth: 8,
|
||||
textStyle: {
|
||||
color: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969',
|
||||
},
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'item',
|
||||
},
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: '9.6%',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '纯文本',
|
||||
...graphicElementStyle,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '图文类',
|
||||
...graphicElementStyle,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: '86.6%',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '视频类',
|
||||
...graphicElementStyle,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['11%', '50%'],
|
||||
label: {
|
||||
formatter: '{d}% ',
|
||||
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969',
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: isDark ? '#000' : '#fff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: [148564],
|
||||
name: 'UGC原创',
|
||||
itemStyle: {
|
||||
color: '#249EFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [334271],
|
||||
name: '国外网站',
|
||||
itemStyle: {
|
||||
color: '#846BCE',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '转载文章',
|
||||
itemStyle: {
|
||||
color: '#21CCFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '行业报告',
|
||||
itemStyle: {
|
||||
color: '#0E42D2',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '其他',
|
||||
itemStyle: {
|
||||
color: '#86DF6C',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
label: {
|
||||
formatter: '{d}% ',
|
||||
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969',
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: isDark ? '#000' : '#fff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: [148564],
|
||||
name: 'UGC原创',
|
||||
itemStyle: {
|
||||
color: '#249EFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [334271],
|
||||
name: '国外网站',
|
||||
itemStyle: {
|
||||
color: '#846BCE',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '转载文章',
|
||||
itemStyle: {
|
||||
color: '#21CCFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '行业报告',
|
||||
itemStyle: {
|
||||
color: '#0E42D2',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '其他',
|
||||
itemStyle: {
|
||||
color: '#86DF6C',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['88%', '50%'],
|
||||
label: {
|
||||
formatter: '{d}% ',
|
||||
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969',
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: isDark ? '#000' : '#fff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: [148564],
|
||||
name: 'UGC原创',
|
||||
itemStyle: {
|
||||
color: '#249EFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [334271],
|
||||
name: '国外网站',
|
||||
itemStyle: {
|
||||
color: '#846BCE',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '转载文章',
|
||||
itemStyle: {
|
||||
color: '#21CCFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '行业报告',
|
||||
itemStyle: {
|
||||
color: '#0E42D2',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [445694],
|
||||
name: '其他',
|
||||
itemStyle: {
|
||||
color: '#86DF6C',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const { loading } = useLoading(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('multiDAnalysis.card.title.contentTypeDistribution')" :header-style="{ paddingBottom: 0 }">
|
||||
<Chart style="height: 222px" :option="chartOption" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
|
||||
const { chartOption } = useChartOption((isDark) => {
|
||||
return {
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 20,
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
top: 'center',
|
||||
right: '0',
|
||||
orient: 'vertical',
|
||||
icon: 'circle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 20,
|
||||
textStyle: {
|
||||
color: isDark ? '#ffffff' : '#4E5969',
|
||||
},
|
||||
},
|
||||
radar: {
|
||||
center: ['40%', '50%'],
|
||||
radius: 80,
|
||||
indicator: [
|
||||
{ name: '国际', max: 6500 },
|
||||
{ name: '财经', max: 22000 },
|
||||
{ name: '科技', max: 30000 },
|
||||
{ name: '其他', max: 38000 },
|
||||
{ name: '体育', max: 52000 },
|
||||
{ name: '娱乐', max: 25000 },
|
||||
],
|
||||
axisName: {
|
||||
color: isDark ? '#ffffff' : '#1D2129',
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#484849' : '#E5E6EB',
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#484849' : '#E5E6EB',
|
||||
},
|
||||
},
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
color: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: [4850, 19000, 19000, 29500, 35200, 20000],
|
||||
name: '纯文本',
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: isDark ? '#6CAAF5' : '#249EFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [2250, 17000, 21000, 23500, 42950, 22000],
|
||||
name: '图文类',
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: isDark ? '#A079DC' : '#313CA9',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: [5850, 11000, 26000, 27500, 46950, 18000],
|
||||
name: '视频类',
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: isDark ? '#3D72F6' : '#21CCFF',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<ChainItem :title="$t('multiDAnalysis.card.title.retentionTrends')" quota="retentionTrends" chart-type="line" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<ChainItem :title="$t('multiDAnalysis.card.title.userRetention')" quota="userRetention" chart-type="bar" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<ChainItem :title="$t('multiDAnalysis.card.title.contentConsumptionTrends')" quota="contentConsumptionTrends" chart-type="line" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<ChainItem :title="$t('multiDAnalysis.card.title.contentConsumption')" quota="contentConsumption" chart-type="bar" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ChainItem from './chain-item.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" :title="$t('multiDAnalysis.card.title.dataOverview')">
|
||||
<a-row justify="space-between">
|
||||
<a-col v-for="(item, idx) in renderData" :key="idx" :span="6">
|
||||
<a-statistic :title="item.title" :value="item.value" show-group-separator :value-from="0" animation>
|
||||
<template #prefix>
|
||||
<span class="statistic-prefix" :style="{ background: item.prefix.background }">
|
||||
<component :is="item.prefix.icon" :style="{ color: item.prefix.iconColor }" />
|
||||
</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<Chart style="height: 328px; margin-top: 20px" :option="chartOption" />
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryDataOverview } from '@/api/visualization'
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import useThemes from '@/hooks/themes'
|
||||
import { ToolTipFormatterParams } from '@/types/echarts'
|
||||
import { LineSeriesOption } from 'echarts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const tooltipItemsHtmlString = (items: ToolTipFormatterParams[]) => {
|
||||
return items
|
||||
.map(
|
||||
(el) => `<div class="content-panel">
|
||||
<p>
|
||||
<span style="background-color: ${el.color}" class="tooltip-item-icon"></span><span>${el.seriesName}</span>
|
||||
</p>
|
||||
<span class="tooltip-value">${el.value?.toLocaleString()}</span>
|
||||
</div>`
|
||||
)
|
||||
.reverse()
|
||||
.join('')
|
||||
}
|
||||
|
||||
const generateSeries = (name: string, lineColor: string, itemBorderColor: string, data: number[]): LineSeriesOption => {
|
||||
return {
|
||||
name,
|
||||
data,
|
||||
stack: 'Total',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
itemStyle: {
|
||||
color: lineColor,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
color: lineColor,
|
||||
borderWidth: 2,
|
||||
borderColor: itemBorderColor,
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: lineColor,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: lineColor,
|
||||
},
|
||||
}
|
||||
}
|
||||
const { t } = useI18n()
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const { isDark } = useThemes()
|
||||
const renderData = computed(() => [
|
||||
{
|
||||
title: t('multiDAnalysis.dataOverview.contentProduction'),
|
||||
value: 1902,
|
||||
prefix: {
|
||||
icon: 'icon-edit',
|
||||
background: isDark.value ? '#593E2F' : '#FFE4BA',
|
||||
iconColor: isDark.value ? '#F29A43' : '#F77234',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('multiDAnalysis.dataOverview.contentClick'),
|
||||
value: 2445,
|
||||
prefix: {
|
||||
icon: 'icon-thumb-up',
|
||||
background: isDark.value ? '#3D5A62' : '#E8FFFB',
|
||||
iconColor: isDark.value ? '#6ED1CE' : '#33D1C9',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('multiDAnalysis.dataOverview.contentExposure'),
|
||||
value: 3034,
|
||||
prefix: {
|
||||
icon: 'icon-heart',
|
||||
background: isDark.value ? '#354276' : '#E8F3FF',
|
||||
iconColor: isDark.value ? '#4A7FF7' : '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('multiDAnalysis.dataOverview.activeUsers'),
|
||||
value: 1275,
|
||||
prefix: {
|
||||
icon: 'icon-user',
|
||||
background: isDark.value ? '#3F385E' : '#F5E8FF',
|
||||
iconColor: isDark.value ? '#8558D3' : '#722ED1',
|
||||
},
|
||||
},
|
||||
])
|
||||
const xAxis = ref<string[]>([])
|
||||
const contentProductionData = ref<number[]>([])
|
||||
const contentClickData = ref<number[]>([])
|
||||
const contentExposureData = ref<number[]>([])
|
||||
const activeUsersData = ref<number[]>([])
|
||||
const { chartOption } = useChartOption((dark) => {
|
||||
return {
|
||||
grid: {
|
||||
left: '2.6%',
|
||||
right: '4',
|
||||
top: '40',
|
||||
bottom: '40',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
offset: 2,
|
||||
data: xAxis.value,
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: '#4E5969',
|
||||
formatter(value: string, idx: number) {
|
||||
if (idx === 0) return ''
|
||||
if (idx === xAxis.value.length - 1) return ''
|
||||
return `${value}`
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisPointer: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#23ADFF',
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
formatter(value: number, idx: number) {
|
||||
if (idx === 0) return String(value)
|
||||
return `${value / 1000}k`
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: dark ? '#2E2E30' : '#F2F3F5',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter(params) {
|
||||
const [firstElement] = params as ToolTipFormatterParams[]
|
||||
return `<div>
|
||||
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
|
||||
${tooltipItemsHtmlString(params as ToolTipFormatterParams[])}
|
||||
</div>`
|
||||
},
|
||||
className: 'echarts-tooltip-diy',
|
||||
},
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: '2.6%',
|
||||
bottom: '18',
|
||||
style: {
|
||||
text: '12.10',
|
||||
textAlign: 'center',
|
||||
fill: '#4E5969',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
right: '0',
|
||||
bottom: '18',
|
||||
style: {
|
||||
text: '12.17',
|
||||
textAlign: 'center',
|
||||
fill: '#4E5969',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
series: [
|
||||
generateSeries('内容生产量', '#722ED1', '#F5E8FF', contentProductionData.value),
|
||||
generateSeries('内容点击量', '#F77234', '#FFE4BA', contentClickData.value),
|
||||
generateSeries('内容曝光量', '#33D1C9', '#E8FFFB', contentExposureData.value),
|
||||
generateSeries('活跃用户数', '#3469FF', '#E8F3FF', activeUsersData.value),
|
||||
],
|
||||
}
|
||||
})
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data } = await queryDataOverview()
|
||||
xAxis.value = data.xAxis
|
||||
data.data.forEach((el) => {
|
||||
if (el.name === '内容生产量') {
|
||||
contentProductionData.value = el.value
|
||||
} else if (el.name === '内容点击量') {
|
||||
contentClickData.value = el.value
|
||||
} else if (el.name === '内容曝光量') {
|
||||
contentExposureData.value = el.value
|
||||
}
|
||||
activeUsersData.value = el.value
|
||||
})
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-statistic) {
|
||||
.arco-statistic-title {
|
||||
color: rgb(var(--gray-10));
|
||||
font-weight: bold;
|
||||
}
|
||||
.arco-statistic-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.statistic-prefix {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
color: var(--color-white);
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('multiDAnalysis.card.title.userActions')">
|
||||
<Chart height="122px" :option="chartOption" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useChartOption from '@/hooks/chart-option'
|
||||
|
||||
const { chartOption } = useChartOption((isDark) => {
|
||||
return {
|
||||
grid: {
|
||||
left: 44,
|
||||
right: 20,
|
||||
top: 0,
|
||||
bottom: 20,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
show: true,
|
||||
formatter(value: number, idx: number) {
|
||||
if (idx === 0) return String(value)
|
||||
return `${Number(value) / 1000}k`
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#484849' : '#E5E8EF',
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: ['点赞量', '评论量', '分享量'],
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: '#4E5969',
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
length: 2,
|
||||
lineStyle: {
|
||||
color: '#A9AEB8',
|
||||
},
|
||||
alignWithLabel: true,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: isDark ? '#484849' : '#A9AEB8',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [1033, 1244, 1520],
|
||||
type: 'bar',
|
||||
barWidth: 7,
|
||||
itemStyle: {
|
||||
color: '#4086FF',
|
||||
borderRadius: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.visualization', 'menu.visualization.multiDimensionDataAnalysis']" />
|
||||
<a-space direction="vertical" :size="16" fill>
|
||||
<a-grid :cols="24" :col-gap="16" :row-gap="16">
|
||||
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 18, xl: 18, xxl: 18 }">
|
||||
<DataOverview />
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 6, xl: 6, xxl: 6 }">
|
||||
<UserActions style="margin-bottom: 16px" />
|
||||
<ContentTypeDistribution />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
<DataChainGrowth />
|
||||
<ContentPublishingSource />
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DataOverview from './components/data-overview.vue'
|
||||
import DataChainGrowth from './components/data-chain-growth.vue'
|
||||
import UserActions from './components/user-actions.vue'
|
||||
import ContentTypeDistribution from './components/content-type-distribution.vue'
|
||||
import ContentPublishingSource from './components/content-publishing-source.vue'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MultiDimensionDataAnalysis',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
:deep(.section-title) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.chart-wrap) {
|
||||
height: 264px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
'menu.visualization.multiDimensionDataAnalysis': 'Multi-D Analysis',
|
||||
'multiDAnalysis.card.title.dataOverview': 'Overview',
|
||||
'multiDAnalysis.dataOverview.contentProduction': 'Content Production',
|
||||
'multiDAnalysis.dataOverview.contentClick': 'Content Click',
|
||||
'multiDAnalysis.dataOverview.contentExposure': 'Content Exposure',
|
||||
'multiDAnalysis.dataOverview.activeUsers': 'Active Users',
|
||||
'multiDAnalysis.card.title.userActions': 'User Actions',
|
||||
'multiDAnalysis.card.title.contentTypeDistribution': 'Content Type Distribution',
|
||||
'multiDAnalysis.card.title.retentionTrends': 'Retention Trends',
|
||||
'multiDAnalysis.card.title.userRetention': 'User Retention',
|
||||
'multiDAnalysis.card.title.contentConsumptionTrends': 'Content Consumption Trends',
|
||||
'multiDAnalysis.card.title.contentConsumption': 'Content Consumption',
|
||||
'multiDAnalysis.card.title.contentPublishingSource': 'Content Publishing Source',
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
'menu.visualization.multiDimensionDataAnalysis': '多维数据分析',
|
||||
'multiDAnalysis.card.title.dataOverview': '数据总览',
|
||||
'multiDAnalysis.dataOverview.contentProduction': '内容生产量',
|
||||
'multiDAnalysis.dataOverview.contentClick': '内容点击量',
|
||||
'multiDAnalysis.dataOverview.contentExposure': '内容曝光量',
|
||||
'multiDAnalysis.dataOverview.activeUsers': '活跃用户数',
|
||||
'multiDAnalysis.card.title.userActions': '今日转评赞统计',
|
||||
'multiDAnalysis.card.title.contentTypeDistribution': '内容题材分布',
|
||||
'multiDAnalysis.card.title.retentionTrends': '用户留存趋势',
|
||||
'multiDAnalysis.card.title.userRetention': '用户留存量',
|
||||
'multiDAnalysis.card.title.contentConsumptionTrends': '内容消费趋势',
|
||||
'multiDAnalysis.card.title.contentConsumption': '内容消费量',
|
||||
'multiDAnalysis.card.title.contentPublishingSource': '内容发布来源',
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
import { PostData } from '@/types/global'
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
Mock.mock(new RegExp('/api/data-chain-growth'), (params: PostData) => {
|
||||
const { quota } = JSON.parse(params.body)
|
||||
const getLineData = () => {
|
||||
return {
|
||||
xAxis: new Array(12).fill(0).map((_item, index) => `${index + 1}日`),
|
||||
data: {
|
||||
name: quota,
|
||||
value: new Array(12).fill(0).map(() => Mock.Random.natural(1000, 3000)),
|
||||
},
|
||||
}
|
||||
}
|
||||
return successResponseWrap({
|
||||
count: Mock.Random.natural(1000, 3000),
|
||||
growth: Mock.Random.float(20, 100, 2, 2),
|
||||
chartData: getLineData(),
|
||||
})
|
||||
})
|
||||
// v2
|
||||
Mock.mock(new RegExp('/api/data-overview'), () => {
|
||||
const generateLineData = (name: string) => {
|
||||
return {
|
||||
name,
|
||||
count: Mock.Random.natural(20, 2000),
|
||||
value: new Array(8).fill(0).map(() => Mock.Random.natural(800, 4000)),
|
||||
}
|
||||
}
|
||||
const xAxis = new Array(8).fill(0).map((_item, index) => {
|
||||
return `12.1${index}`
|
||||
})
|
||||
return successResponseWrap({
|
||||
xAxis,
|
||||
data: [
|
||||
generateLineData('内容生产量'),
|
||||
generateLineData('内容点击量'),
|
||||
generateLineData('内容曝光量'),
|
||||
generateLineData('活跃用户数'),
|
||||
],
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user