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.

655 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="interaction-stats">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">
<i class="el-icon-s-data"></i>
交互统计
</h2>
</div>
<!-- 筛选区域 -->
<div class="filter-section">
<el-form :model="filter" label-position="top">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="时间范围" prop="timeRange">
<el-select v-model="filter.timeRange" placeholder="请选择时间范围" style="width: 100%;">
<el-option label="最近7天" :value="7"></el-option>
<el-option label="最近30天" :value="30"></el-option>
<el-option label="最近90天" :value="90"></el-option>
<el-option label="最近一年" :value="365"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="供需类型" prop="supplyType">
<el-select v-model="filter.supplyType" placeholder="请选择供需类型" clearable style="width: 100%;">
<el-option label="全部" value=""></el-option>
<el-option label="供应" value="supply"></el-option>
<el-option label="需求" value="demand"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="操作">
<el-button type="primary" @click="updateStats" :loading="loading">
{{ loading ? '...' : '' }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<!-- 时间段统计 -->
<div class="stats-overview">
<div class="stat-card blue">
<div class="stat-header">
<div class="stat-title">供需发布数</div>
<div class="stat-icon icon-blue">
<i class="el-icon-document-add"></i>
</div>
</div>
<div class="stat-value">{{ chartData.totalCount || 0 }}</div>
<div class="stat-change" :class="getChangeClass(chartData.publishChange)">
<i :class="getChangeIcon(chartData.publishChange)"></i>
<span>{{ formatChange(chartData.publishChange) }} 较上期</span>
</div>
</div>
<div class="stat-card green">
<div class="stat-header">
<div class="stat-title">私信数量</div>
<div class="stat-icon icon-green">
<i class="el-icon-message"></i>
</div>
</div>
<div class="stat-value">{{ chartData.messageCount || 0 }}</div>
<div class="stat-change" :class="getChangeClass(chartData.messageChange)">
<i :class="getChangeIcon(chartData.messageChange)"></i>
<span>{{ formatChange(chartData.messageChange) }} 较上期</span>
</div>
</div>
<div class="stat-card orange">
<div class="stat-header">
<div class="stat-title">交互次数</div>
<div class="stat-icon icon-orange">
<i class="el-icon-chat-dot-round"></i>
</div>
</div>
<div class="stat-value">{{ chartData.interactionCount || 0 }}</div>
<div class="stat-change" :class="getChangeClass(chartData.interactionChange)">
<i :class="getChangeIcon(chartData.interactionChange)"></i>
<span>{{ formatChange(chartData.interactionChange) }} 较上期</span>
</div>
</div>
</div>
<!-- 供需交互明细 -->
<div class="chart-section">
<div class="section-title">
供需交互明细
</div>
<div class="data-table">
<el-table :data="interactionList" style="width: 100%" v-loading="tableLoading">
<el-table-column label="供需信息" min-width="200">
<template slot-scope="scope">
<div>
<div style="font-weight: 600; font-size: 14px; margin-bottom: 5px;">{{ scope.row.title || '-' }}</div>
<div style="font-size: 12px; color: #666; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" :title="scope.row.content || '-'">{{ scope.row.content || '-' }}</div>
<el-tag :type="getTypeTagType(scope.row.type)" size="small">
{{ getTypeDisplayValue(scope.row.type) }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="发布者" min-width="150">
<template slot-scope="scope">
<div class="user-info">
<div class="user-avatar">{{ scope.row.publisher ? scope.row.publisher.charAt(0) : '-' }}</div>
<div>
<div style="font-weight: 600; font-size: 14px;">{{ scope.row.publisher || '-' }}</div>
<div style="font-size: 12px; color: #666;">{{ scope.row.publisherInfo || '-' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="发布时间" min-width="120">
<template slot-scope="scope">
<div style="font-size: 12px;">{{ scope.row.created_at || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="交互记录" min-width="200">
<template slot-scope="scope">
<div>
<div class="interaction-detail">
<div v-if="scope.row.messages && scope.row.messages.length > 0">
<div v-for="(message, index) in scope.row.messages" :key="index" style="margin-bottom: 8px;">
<span style="font-size: 12px;">{{ message.to_user.name || '-' }}({{ message.to_user.year || '-' }}届) · {{ formatDateTime(message.created_at) || '-' }}</span>
</div>
</div>
<div v-else style="color: #999; font-size: 12px;">暂无交互记录</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" min-width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<div class="pagination-info">
<span class="text-muted"> {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, totalCount) }} {{ totalCount }} </span>
</div>
<el-pagination
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-size="pageSize"
:total="totalCount"
layout="prev, pager, next"
background>
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import { supplyDemandChart } from '@/api/student'
export default {
name: 'InteractionStats',
data() {
return {
filter: {
timeRange: 30,
supplyType: ''
},
currentPage: 1,
pageSize: 10,
totalCount: 0,
loading: false,
tableLoading: false,
chartData: {
totalCount: 0,
messageCount: 0,
interactionCount: 0,
publishChange: 0,
messageChange: 0,
interactionChange: 0
},
interactionList: []
}
},
mounted() {
this.fetchChartData()
},
methods: {
// 获取图表数据
async fetchChartData() {
try {
this.loading = true
// 根据timeRange计算开始和结束日期
const endDate = new Date()
const startDate = new Date()
startDate.setDate(endDate.getDate() - this.filter.timeRange)
const params = {
start_date: startDate.toISOString().split('T')[0], // YYYY-MM-DD格式
end_date: endDate.toISOString().split('T')[0], // YYYY-MM-DD格式
page_size: this.pageSize,
page: this.currentPage
}
const type = this.getTypeValue(this.filter.supplyType)
if (type !== undefined) {
params.type = type
}
const response = await supplyDemandChart(params)
if (response) {
this.chartData = {
totalCount: response.supply_demand_count || 0,
messageCount: response.message_count || 0,
interactionCount: response.interaction_count || 0,
publishChange: response.supply_demand_growth_rate?.rate || 0,
messageChange: response.message_growth_rate?.rate || 0,
interactionChange: response.interaction_growth_rate?.rate || 0
}
// 更新交互记录列表
if (response.list && response.list.data) {
this.interactionList = response.list.data
}
console.log(this.interactionList)
// 更新分页数据
if (response.list) {
this.totalCount = response.list.total || 0
this.currentPage = response.list.current_page || 1
this.pageSize = response.list.per_page || 10
} else {
// 如果没有list结构使用原有的总数
this.totalCount = response.supply_demand_count || 0
}
}
} catch (error) {
console.error('获取图表数据失败:', error)
this.$message.error('获取统计数据失败,请重试')
} finally {
this.loading = false
}
},
// 更新统计数据
async updateStats() {
await this.fetchChartData()
this.$message.success('统计数据已更新')
},
// 处理分页变化
async handleCurrentChange(page) {
this.currentPage = page
this.tableLoading = true
try {
// 这里可以调用分页API获取指定页面的数据
// 暂时使用现有数据,实际项目中应该调用分页接口
await this.fetchChartData()
this.$message.info(`已加载第 ${page} 页数据`)
} catch (error) {
console.error('加载分页数据失败:', error)
this.$message.error('加载数据失败')
} finally {
this.tableLoading = false
}
},
// 获取状态类型
getStatusType(status) {
const statusMap = {
0: 'warning', // 待审核
1: 'success', // 通过
2: 'danger', // 拒绝
3: 'info', // 退回修改
4: 'info' // 永久隐藏
}
return statusMap[status] || 'info'
},
// 获取状态文本
getStatusText(status) {
const statusMap = {
0: '待审核',
1: '通过',
2: '拒绝',
3: '退回修改',
4: '永久隐藏'
}
return statusMap[status] || '未知状态'
},
// 获取变化样式类
getChangeClass(change) {
if (change > 0) return 'change-up'
if (change < 0) return 'change-down'
return 'change-neutral'
},
// 获取变化图标
getChangeIcon(change) {
if (change > 0) return 'el-icon-arrow-up'
if (change < 0) return 'el-icon-arrow-down'
return 'el-icon-minus'
},
// 格式化变化数据
formatChange(change) {
if (change > 0) return `+${change.toFixed(1)}%`
if (change < 0) return `${change.toFixed(1)}%`
return '0.0%'
},
// 获取类型值映射
getTypeValue(supplyType) {
if (supplyType === 'supply') return 1
if (supplyType === 'demand') return 2
return undefined // 全部时不传type参数
},
// 获取类型显示值
getTypeDisplayValue(type) {
if (type === 'demand' || type === 2) return '需求'
if (type === 'supply' || type === 1) return '供应'
return '未知'
},
// 获取类型标签样式
getTypeTagType(type) {
if (type === 'demand' || type === 2) return 'warning'
if (type === 'supply' || type === 1) return 'success'
return 'info'
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return '-'
try {
// 如果是时间戳转换为Date对象
let date
if (typeof dateTime === 'number') {
date = new Date(dateTime * 1000) // 假设是秒级时间戳
} else if (typeof dateTime === 'string') {
date = new Date(dateTime)
} else {
date = dateTime
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
console.warn('Invalid date:', dateTime)
return '-'
}
// 格式化日期
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (error) {
console.error('格式化日期时间失败:', error, dateTime)
return '-'
}
}
},
// 监听筛选条件变化
watch: {
filter: {
handler() {
this.currentPage = 1
this.fetchChartData()
},
deep: true
}
}
}
</script>
<style lang="scss" scoped>
.interaction-stats {
padding: 20px;
background-color: #f8f9fa;
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.page-title {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
display: flex;
align-items: center;
margin: 0;
i {
margin-right: 10px;
color: #409EFF;
}
}
.filter-section {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border-left: 4px solid;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
&.blue {
border-left-color: #409EFF;
}
&.green {
border-left-color: #67C23A;
}
&.orange {
border-left-color: #E6A23C;
}
}
.stat-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
}
.stat-title {
font-size: 14px;
color: #7f8c8d;
font-weight: 500;
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
&.icon-blue { background: #409EFF; }
&.icon-green { background: #67C23A; }
&.icon-orange { background: #E6A23C; }
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.stat-change {
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
&.change-up {
color: #67C23A;
}
&.change-down {
color: #F56C6C;
}
}
.chart-section {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.data-table {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
// 表格头部渐变背景
::v-deep .el-table__header-wrapper {
.el-table__header {
th {
background: linear-gradient(135deg, #409EFF 0%, rgba(64, 158, 255, 0.8) 100%);
color: white;
border: none;
}
}
}
// 表格自适应宽度
::v-deep .el-table {
width: 100% !important;
.el-table__body-wrapper {
overflow-x: auto;
}
.el-table__header-wrapper {
overflow-x: auto;
}
}
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #409EFF 0%, rgba(64, 158, 255, 0.7) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: 600;
}
.interaction-detail {
max-height: 150px;
overflow-y: auto;
}
.interaction-detail::-webkit-scrollbar {
width: 4px;
}
.interaction-detail::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2px;
}
.interaction-detail::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 2px;
}
.interaction-detail::-webkit-scrollbar-thumb:hover {
background: #999;
}
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px;
border-top: 1px solid #e9ecef;
}
.pagination-info {
font-size: 14px;
}
.text-muted {
color: #909399;
}
// 自定义标签颜色
::v-deep .el-tag {
&.el-tag--danger {
background-color: #E6A23C;
border-color: #E6A23C;
color: white;
}
&.el-tag--success {
background-color: #67C23A;
border-color: #67C23A;
color: white;
}
&.el-tag--info {
background-color: #909399;
border-color: #909399;
color: white;
}
}
// 分页按钮样式
::v-deep .el-pagination {
.el-pager li {
&.active {
background-color: #409EFF;
color: white;
}
}
}
</style>