You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
475 lines
13 KiB
475 lines
13 KiB
<template>
|
|
<div class="user-activity-stats-container">
|
|
<!-- 用户活跃度趋势图 -->
|
|
<div class="content-card">
|
|
<div class="card-header">
|
|
<el-icon class="header-icon"><TrendCharts /></el-icon>
|
|
<span class="header-title">用户活跃度趋势图</span>
|
|
</div>
|
|
|
|
<div class="card-filter-row">
|
|
<div class="filter-left">
|
|
<span class="filter-label">时间段查询</span>
|
|
<el-date-picker
|
|
v-model="chartDateRange"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始时间"
|
|
end-placeholder="结束时间"
|
|
size="default"
|
|
@change="handleChartDateChange"
|
|
/>
|
|
</div>
|
|
<div class="filter-right">
|
|
<el-button-group>
|
|
<el-button :type="chartMode === 'day' ? 'danger' : 'default'" @click="handleModeChange('day')">每日</el-button>
|
|
<el-button :type="chartMode === 'week' ? 'danger' : 'default'" @click="handleModeChange('week')">近七日</el-button>
|
|
<el-button :type="chartMode === 'month' ? 'danger' : 'default'" @click="handleModeChange('month')">近三十日</el-button>
|
|
</el-button-group>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-container" ref="chartRef"></div>
|
|
</div>
|
|
|
|
<!-- DeepChart活跃用户明细 -->
|
|
<div class="content-card">
|
|
<div class="card-header">
|
|
<el-icon class="header-icon"><DataLine /></el-icon>
|
|
<span class="header-title">DeepChart活跃用户明细</span>
|
|
</div>
|
|
|
|
<div class="table-filter-row">
|
|
<el-input v-model="tableAccount" placeholder="请输入账号" style="width: 150px" />
|
|
<el-select v-model="tableRegion" placeholder="请选择所属地区" style="width: 150px">
|
|
<el-option label="全部" value="all" />
|
|
<el-option
|
|
v-for="item in regionOptions"
|
|
:key="item.value"
|
|
:label="item.label"
|
|
:value="item.value"
|
|
/>
|
|
</el-select>
|
|
|
|
<el-date-picker
|
|
v-model="tableDateRange"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始时间"
|
|
end-placeholder="结束时间"
|
|
size="default"
|
|
style="width: 240px"
|
|
/>
|
|
|
|
<el-button type="primary" class="search-btn-small" @click="handleTableSearch">搜索</el-button>
|
|
<el-button type="primary" class="reset-btn-small" @click="handleTableReset">重置</el-button>
|
|
<el-button type="danger" class="export-btn-small">数据导出</el-button>
|
|
<el-button type="danger" class="export-list-btn">查看导出列表</el-button>
|
|
</div>
|
|
|
|
<el-table :data="tableData" style="width: 100%" :header-cell-style="headerCellStyle">
|
|
<el-table-column prop="index" label="序号" width="80" align="center" />
|
|
<el-table-column prop="account" label="账号" min-width="120" />
|
|
<el-table-column prop="name" label="姓名" min-width="120" />
|
|
<el-table-column prop="region" label="地区" width="100" />
|
|
<el-table-column prop="loginCount" label="登录次数" width="100" align="center" />
|
|
<el-table-column prop="totalDuration" label="总停留时长" min-width="150" align="center" />
|
|
<el-table-column prop="avgDuration" label="平均停留时长" min-width="150" align="center" />
|
|
<el-table-column prop="deepMate" label="DeepMate" min-width="100" align="center" />
|
|
<el-table-column prop="deepExplore" label="深度探索" min-width="100" align="center" />
|
|
<el-table-column prop="marketInfo" label="行情" min-width="100" align="center" />
|
|
<el-table-column prop="updateTime" label="更新日期" min-width="150" align="center" />
|
|
</el-table>
|
|
|
|
<div class="pagination-container">
|
|
<div class="total-count">共{{ total }}条</div>
|
|
<el-pagination
|
|
background
|
|
layout="sizes, prev, pager, next, jumper"
|
|
:total="total"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
v-model:current-page="currentPage"
|
|
v-model:page-size="pageSize"
|
|
@current-change="handlePageChange"
|
|
@size-change="handleSizeChange"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, nextTick, watch } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import * as echarts from 'echarts';
|
|
import { getUserDeepChartTrend, getDeepChartActiveUserList, getRegionsList } from '../../api/platformData';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
// 图表筛选
|
|
const chartDateRange = ref('');
|
|
const chartMode = ref(route.query.mode || 'day'); // day, week, month
|
|
const chartRef = ref(null);
|
|
let chartInstance = null;
|
|
|
|
// 初始化 URL 参数
|
|
const initQueryParams = () => {
|
|
const { mode, startTime, endTime } = route.query;
|
|
|
|
if (mode) {
|
|
chartMode.value = mode;
|
|
}
|
|
|
|
if (startTime && endTime) {
|
|
chartDateRange.value = [new Date(startTime), new Date(endTime)];
|
|
}
|
|
};
|
|
|
|
initQueryParams();
|
|
|
|
const updateUrlParams = () => {
|
|
const query = { ...route.query };
|
|
query.mode = chartMode.value;
|
|
|
|
if (chartDateRange.value && chartDateRange.value.length === 2) {
|
|
query.startTime = formatDate(chartDateRange.value[0]);
|
|
query.endTime = formatDate(chartDateRange.value[1]);
|
|
} else {
|
|
delete query.startTime;
|
|
delete query.endTime;
|
|
}
|
|
|
|
router.replace({ query });
|
|
};
|
|
|
|
const handleChartDateChange = () => {
|
|
updateUrlParams();
|
|
fetchChartData();
|
|
};
|
|
|
|
const handleModeChange = (mode) => {
|
|
chartMode.value = mode;
|
|
updateUrlParams();
|
|
fetchChartData();
|
|
};
|
|
|
|
// 格式化日期
|
|
const formatDate = (date) => {
|
|
if (!date) return '';
|
|
const d = new Date(date);
|
|
const pad = (n) => n < 10 ? '0' + n : n;
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
};
|
|
|
|
const fetchChartData = async () => {
|
|
const params = {
|
|
mode: chartMode.value
|
|
};
|
|
|
|
if (chartDateRange.value && chartDateRange.value.length === 2) {
|
|
params.startTime = formatDate(chartDateRange.value[0]);
|
|
params.endTime = formatDate(chartDateRange.value[1]);
|
|
}
|
|
|
|
try {
|
|
const res = await getUserDeepChartTrend(params);
|
|
console.log("获取DeepChart趋势数据响应:", res);
|
|
const data = res.data || res; // 兼容处理
|
|
|
|
if (Array.isArray(data)) {
|
|
updateChart(data);
|
|
}
|
|
} catch (e) {
|
|
console.error('获取DeepChart趋势数据失败:', e);
|
|
}
|
|
};
|
|
|
|
const updateChart = (data) => {
|
|
if (!chartRef.value) return;
|
|
|
|
if (!chartInstance) {
|
|
chartInstance = echarts.init(chartRef.value);
|
|
}
|
|
|
|
const dates = data.map(item => item.time_point);
|
|
const activeUsers = data.map(item => item.active_users);
|
|
const useCounts = data.map(item => item.use_count);
|
|
|
|
const option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: { type: 'cross' }
|
|
},
|
|
legend: {
|
|
data: ['DeepChart活跃人数', 'DeepChart使用次数'],
|
|
top: 'top',
|
|
left: 'center',
|
|
itemWidth: 35,
|
|
itemHeight: 18,
|
|
textStyle: {
|
|
fontSize: 14
|
|
}
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: dates
|
|
},
|
|
yAxis: [
|
|
{
|
|
type: 'value',
|
|
min: 0,
|
|
position: 'left',
|
|
axisLabel: { formatter: '{value}' }
|
|
},
|
|
{
|
|
type: 'value',
|
|
min: 0,
|
|
position: 'right',
|
|
axisLabel: { formatter: '{value}' }
|
|
}
|
|
],
|
|
series: [
|
|
{
|
|
name: 'DeepChart活跃人数',
|
|
type: 'line',
|
|
yAxisIndex: 0,
|
|
data: activeUsers,
|
|
itemStyle: { color: '#e74c3c' },
|
|
symbol: 'circle',
|
|
symbolSize: 8,
|
|
lineStyle: { width: 3 }
|
|
},
|
|
{
|
|
name: 'DeepChart使用次数',
|
|
type: 'line',
|
|
yAxisIndex: 1,
|
|
data: useCounts,
|
|
itemStyle: { color: '#2ecc71' },
|
|
symbol: 'circle',
|
|
symbolSize: 8,
|
|
lineStyle: { width: 3 }
|
|
}
|
|
]
|
|
};
|
|
|
|
chartInstance.setOption(option);
|
|
};
|
|
const tableAccount = ref('');
|
|
const tableRegion = ref('');
|
|
const tableDateRange = ref('');
|
|
const currentPage = ref(1);
|
|
const pageSize = ref(10);
|
|
const total = ref(0);
|
|
const regionOptions = ref([]);
|
|
|
|
// 获取地区列表
|
|
const fetchRegionOptions = async () => {
|
|
try {
|
|
const res = await getRegionsList();
|
|
console.log("获取地区列表响应:", res);
|
|
const data = res.data || res;
|
|
if (data && data.list) {
|
|
regionOptions.value = data.list.map(region => ({ label: region, value: region }));
|
|
}
|
|
} catch (e) {
|
|
console.error('获取地区列表失败:', e);
|
|
}
|
|
};
|
|
|
|
// 表格数据
|
|
const tableData = ref([]);
|
|
|
|
const handleTableSearch = () => {
|
|
currentPage.value = 1;
|
|
fetchTableData();
|
|
};
|
|
|
|
const handleTableReset = () => {
|
|
tableAccount.value = '';
|
|
tableRegion.value = '';
|
|
tableDateRange.value = '';
|
|
currentPage.value = 1;
|
|
fetchTableData();
|
|
};
|
|
|
|
const handlePageChange = (page) => {
|
|
currentPage.value = page;
|
|
fetchTableData();
|
|
};
|
|
|
|
const handleSizeChange = (size) => {
|
|
pageSize.value = size;
|
|
currentPage.value = 1;
|
|
fetchTableData();
|
|
};
|
|
|
|
const fetchTableData = async () => {
|
|
const params = {
|
|
page: currentPage.value,
|
|
page_size: pageSize.value
|
|
};
|
|
|
|
if (tableAccount.value) params.jwcode = tableAccount.value;
|
|
if (tableRegion.value && tableRegion.value !== 'all') params.region = tableRegion.value;
|
|
|
|
if (tableDateRange.value && tableDateRange.value.length === 2) {
|
|
params.startTime = formatDate(tableDateRange.value[0]);
|
|
params.endTime = formatDate(tableDateRange.value[1]);
|
|
}
|
|
|
|
try {
|
|
const res = await getDeepChartActiveUserList(params);
|
|
console.log("获取DeepChart活跃用户明细响应:", res);
|
|
const data = res.data || res; // 兼容处理
|
|
|
|
if (data && data.list) {
|
|
tableData.value = data.list.map((item, index) => ({
|
|
index: (currentPage.value - 1) * pageSize.value + index + 1,
|
|
account: item.jwcode,
|
|
name: item.username,
|
|
region: item.region,
|
|
loginCount: item.login_count,
|
|
totalDuration: formatDuration(item.stay_time),
|
|
avgDuration: formatDuration(item.avg_stay_time),
|
|
deepMate: item.deepmate_count,
|
|
deepExplore: item.dive_seek_count,
|
|
marketInfo: item.market_info_count, // 行情登录次数
|
|
updateTime: item.update_time // 使用 update_time
|
|
}));
|
|
total.value = data.total || 0;
|
|
} else {
|
|
tableData.value = [];
|
|
total.value = 0;
|
|
}
|
|
} catch (e) {
|
|
console.error('获取DeepChart活跃用户明细失败:', e);
|
|
}
|
|
};
|
|
|
|
// 格式化时长 (秒 -> 时分秒)
|
|
const formatDuration = (seconds) => {
|
|
if (!seconds) return '0秒';
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
let str = '';
|
|
if (h > 0) str += `${h}小时`;
|
|
if (m > 0) str += `${m}分钟`;
|
|
if (s > 0) str += `${s}秒`;
|
|
return str || '0秒';
|
|
};
|
|
|
|
const headerCellStyle = {
|
|
background: '#f5f7fa',
|
|
color: '#333',
|
|
fontWeight: 'bold'
|
|
};
|
|
|
|
const initChart = () => {
|
|
if (chartRef.value) {
|
|
// 初始化图表实例
|
|
if (!chartInstance) {
|
|
chartInstance = echarts.init(chartRef.value);
|
|
}
|
|
// 初始加载数据
|
|
fetchChartData();
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
initChart();
|
|
fetchTableData();
|
|
fetchRegionOptions();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-activity-stats-container {
|
|
padding: 20px;
|
|
background-color: #fee6e6;
|
|
min-height: calc(100vh - 40px);
|
|
}
|
|
|
|
/* Content Cards */
|
|
.content-card {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #f0f0f0;
|
|
}
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.header-icon {
|
|
color: #409eff;
|
|
font-size: 20px;
|
|
}
|
|
.header-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
/* Chart Section */
|
|
.card-filter-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
background: #fafafa;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
}
|
|
.filter-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.filter-label {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
.chart-container {
|
|
width: 100%;
|
|
height: 400px;
|
|
}
|
|
|
|
/* Table Section */
|
|
.table-filter-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.search-btn-small { background-color: #409eff; width: 70px; }
|
|
.reset-btn-small { background-color: #409eff; border-color: #409eff; width: 70px; }
|
|
.export-btn-small { background-color: #ff7875; border-color: #ff7875; width: 90px; }
|
|
.export-list-btn { background-color: #ff7875; border-color: #ff7875; width: 110px; }
|
|
|
|
/* Pagination */
|
|
.pagination-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin-top: 20px;
|
|
}
|
|
.total-count {
|
|
font-size: 14px;
|
|
color: #606266;
|
|
margin-right: 20px;
|
|
}
|
|
</style>
|