Skip to content

Commit

Permalink
增加聊天情况热力图
Browse files Browse the repository at this point in the history
  • Loading branch information
xaoyaoo committed Aug 7, 2024
1 parent 2550def commit c25312f
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 3 deletions.
227 changes: 227 additions & 0 deletions src/components/stats/DateChatHeatmapStats.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<script setup lang="ts">
import * as echarts from "echarts";
import {onMounted, ref, shallowRef} from "vue";
import {apiDateCount, apiTalkerCount} from "@/api/stat";
import {apiUserList} from "@/api/chat";
import {gen_show_name, type User} from "@/utils/common_utils";
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
import ColorSelect from "@/components/utils/ColorSelect.vue";
import NumberInput from "@/components/utils/NumberInput.vue";
import ChartInit from "@/components/stats/components/ChartInit.vue";
// https://echarts.apache.org/examples/en/editor.html
interface CountData {
sender_count: number
receiver_count: number
total_count: number
}
interface calendar_face {
top: number
left: number
orient: string
range: string
dayLabel: { margin: number }
}
interface series_face {
type: string
coordinateSystem: string
calendarIndex: number
data: any[]
}
const date_count_data = ref<any>({});
const datetime = ref([0, 0]);
const word = ref("");
const loading = ref(false);
const user_options = ref<User[]>([]);
const top_user = ref<{ [key: string]: User }>({});
const top_user_count = ref<{ [key: string]: CountData }>({});
const is_update = ref(false);
const chart_option = ref({
backgroundColor: "#ffffff",
title: {
left: 'center',
text: '聊天记录(不包括群聊)'
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
tooltip: {
position: 'top',
formatter: function (p: any) {
const format = echarts.time.format(p.data[0], '{yyyy}-{MM}-{dd}', false);
return format + ': ' + p.data[1];
}
},
visualMap: {
min: 0,
max: 500,
calculable: true,
orient: 'vertical',
right: '0',
top: 'center'
},
calendar: <calendar_face[]>[],
series: <series_face[]>[],
});
const get_date_count_data = async () => {
// {"2024-12-20":{ "sender_count": sender_count, "receiver_count": receiver_count, "total_count": total_count },....}
date_count_data.value = await apiDateCount(word.value, datetime.value[0] / 1000, datetime.value[1] / 1000);
// 根据key排序
date_count_data.value = Object.fromEntries(Object.entries(date_count_data.value).sort());
let min_date = Object.keys(date_count_data.value)[0];
let max_date = Object.keys(date_count_data.value)[Object.keys(date_count_data.value).length - 1];
let min_year = parseInt(min_date.split("-")[0]);
let max_year = parseInt(max_date.split("-")[0]);
chart_option.value.calendar = [];
chart_option.value.series = [];
for (let i = min_year; i < max_year + 1; i++) {
chart_option.value.calendar.push({
top: 100,
left: 50 + 200 * (i - min_year),
orient: 'vertical',
range: i.toString(),
dayLabel: {margin: 5}
});
chart_option.value.series.push({
type: 'heatmap', coordinateSystem: 'calendar', calendarIndex: i - min_year, data: []
});
}
// refreshData();
Object.keys(date_count_data.value).map(date => {
let year = parseInt(date.split("-")[0]);
let index = year - min_year;
chart_option.value.series[index].data.push([date, date_count_data.value[date].total_count]);
});
}
const get_top_user_count = async () => {
// {"wxid":{ "sender_count": sender_count, "receiver_count": receiver_count, "total_count": total_count },....}
const body_data = await apiTalkerCount();
top_user.value = await apiUserList("", Object.keys(body_data));
top_user_count.value = body_data;
// 根据total_count排序
top_user_count.value = Object.fromEntries(Object.entries(top_user_count.value).sort((a, b) => b[1].total_count - a[1].total_count));
}
// 刷新图表 START
const refreshChart = async (is_get_data: boolean = true) => {
if (is_get_data) {
await get_date_count_data();
}
// 渲染图表
is_update.value = !is_update.value;
}
// 刷新图表 END
onMounted(() => {
get_top_user_count();
refreshChart();
});
// 搜索联系人相关 START
const search_user = async (query: string) => {
try {
loading.value = true;
if (query === '') {
user_options.value = [];
return;
}
const body_data = await apiUserList(query);
loading.value = false;
user_options.value = Object.values(body_data);
} catch (error) {
console.error('Error fetching data:', error);
return [];
}
}
const set_top_user = async (wxid: string) => {
try {
word.value = wxid;
await refreshChart();
} catch (error) {
console.error('Error fetching data:', error);
return [];
}
}
// 搜索联系人相关 END
</script>

<template>
<div class="common-layout" style="background-color: #d2d2fa;height: 100%;width: 100%;">
<el-container style="height: 100%;width: 100%;">
<el-header :height="'80px'" style="width: 100%;">
<strong>时间(默认全部):</strong>
<DateTimeSelect @datetime="(val: any) => {datetime = val;}"/> &nbsp;
<el-select
v-model="word"
filterable
remote
reserve-keyword
placeholder="输入想查看的联系人"
remote-show-suffix
clearable
:remote-method="search_user"
:loading="loading"
style="width: 240px"
>
<el-option v-for="item in user_options" :key="item.wxid" :label="gen_show_name(item)" :value="item.wxid"/>
</el-select>&nbsp;
<el-button type="primary" @click="refreshChart">查看</el-button>
&nbsp
<strong>颜色设置:</strong>
bg:
<color-select
@updateColors="(val:any)=>{val?chart_option.backgroundColor=val:'';refreshChart(false)}"></color-select>
min:
<number-input :n="chart_option.visualMap.min" :step="100"
@updateNumber="(val:any)=>{val?chart_option.visualMap.min=val:'';refreshChart(false)}"></number-input>
max:
<number-input :n="chart_option.visualMap.max" :step="100"
@updateNumber="(val:any)=>{val?chart_option.visualMap.max=val:'';refreshChart(false)}"></number-input>
<br>
<strong>top10:</strong>
<template v-for="wxid in Object.keys(top_user_count)" :key="wxid">
<el-button type="primary" plain @click="set_top_user(wxid)" size="small">
{{ gen_show_name(top_user[wxid]) }} [{{ top_user_count[wxid]?.total_count }}({{
top_user_count[wxid]?.receiver_count
}}/{{ top_user_count[wxid]?.sender_count }})]
</el-button>
</template>
</el-header>

<el-main style="height: calc(100% - 100px);width: 100%;">
<chart-init :option="chart_option" :update="is_update" id="charts_main"/>
<chart-init :option="chart_option" :update="is_update" id="charts_main"/>
</el-main>
</el-container>
</div>
</template>

<style scoped>
</style>
7 changes: 6 additions & 1 deletion src/components/stats/DateChatStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,13 @@ const get_top_user_count = async () => {
const body_data = await apiTalkerCount();
top_user.value = await apiUserList("", Object.keys(body_data));
top_user_count.value = body_data;
// 根据total_count排序
top_user_count.value = Object.fromEntries(Object.entries(top_user_count.value).sort((a, b) => b[1].total_count - a[1].total_count));
}
// 刷新图表 START
const refreshChart = async (is_get_data: boolean = true) => {
Chart.value.clear();
update_chart_option();
if (is_get_data) {
await get_date_count_data();
Expand Down Expand Up @@ -236,7 +239,9 @@ const set_top_user = async (wxid: string) => {
<strong>top10:</strong>
<template v-for="wxid in Object.keys(top_user_count)" :key="wxid">
<el-button type="primary" plain @click="set_top_user(wxid)" size="small">
{{ gen_show_name(top_user[wxid]) }}({{ top_user_count[wxid]?.total_count }})
{{ gen_show_name(top_user[wxid]) }} [{{ top_user_count[wxid]?.total_count }}({{
top_user_count[wxid]?.receiver_count
}}/{{ top_user_count[wxid]?.sender_count }})]
</el-button>
</template>
</el-header>
Expand Down
38 changes: 38 additions & 0 deletions src/components/stats/components/ChartInit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import * as echarts from "echarts";
import {onMounted, ref, shallowRef, watch,} from "vue";
const props = defineProps<{
option: any,
update: boolean
}>();
const Chart = shallowRef<any>(null)
onMounted(() => {
const chartId = `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 5)}`
console.log('ChartInit onMounted', chartId)
Chart.value = echarts.init(document.getElementById('charts_main') as HTMLDivElement);
Chart.value.clear();
Chart.value.setOption(props.option);
})
watch(() => props.update, async (newVal, oldVal) => {
Chart.value.clear();
Chart.value.setOption(props.option);
});
</script>

<template>
<div id="charts_main" class="chart-div"></div>
</template>

<style scoped>
.chart-div {
width: 100%;
height: 100%;
}
</style>
25 changes: 25 additions & 0 deletions src/components/utils/NumberInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script setup lang="ts">
import {ref} from "vue";
const props = defineProps<{
n: Number,
step: Number
}>();
const value = ref(props.n);
const updateNumber = (val: bigint) => {
emit('updateNumber', val);
}
const emit = defineEmits(['updateNumber']);
</script>

<template>
<el-input-number v-model="value" @change="updateNumber" size="small" :step="step"
style="width: 100px;"></el-input-number>
</template>

<style scoped>
</style>
9 changes: 7 additions & 2 deletions src/views/StatisticsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import DateChatStats from "@/components/stats/DateChatStats.vue";
import {ref} from "vue";
import ContactStats from "@/components/stats/ContactStats.vue";
import DateChatHeatmapStats from "@/components/stats/DateChatHeatmapStats.vue";
const mene_selected = ref("date_chat_count");
const mene_selected = ref("date_chat_heatmap");
const MeneSelect = (val: string) => {
mene_selected.value = val;
}
Expand All @@ -26,7 +27,10 @@ const MeneSelect = (val: string) => {
@select="MeneSelect"
>
<el-menu-item index="date_chat_count">
<span>日聊天记录</span>
<span>日聊天数据</span>
</el-menu-item>
<el-menu-item index="date_chat_heatmap">
<span>聊天热力图</span>
</el-menu-item>
<el-menu-item index="contact_stats">
<span>联系人画像</span>
Expand All @@ -36,6 +40,7 @@ const MeneSelect = (val: string) => {

<el-main style="height: 100%;width: 100%;margin: 0;padding: 0;">
<date-chat-stats v-if="mene_selected=='date_chat_count'"/>
<date-chat-heatmap-stats v-if="mene_selected=='date_chat_heatmap'"/>
<contact-stats v-if="mene_selected=='contact_stats'"/>
</el-main>
</el-container>
Expand Down

0 comments on commit c25312f

Please sign in to comment.