<template>
|
<div class="unit-points">
|
<!-- 积分统计卡片 -->
|
<div class="stats-section">
|
<div class="stats-cards">
|
<div class="stats-card balance">
|
<div class="card-icon">
|
<svg-icon name="local-jifen" color="#007CEE"
|
size="50"></svg-icon>
|
</div>
|
<div class="card-content">
|
<div class="card-title">积分余额</div>
|
<div class="card-value">{{ formatNumber(stats.balance) }}</div>
|
</div>
|
</div>
|
<div class="stats-card earned">
|
<div class="card-icon">
|
<svg-icon name="local-jifenjian" color="#007CEE"
|
size="50"></svg-icon>
|
</div>
|
<div class="card-content">
|
<div class="card-title">累计获取</div>
|
<div class="card-value">{{ formatNumber(stats.totalEarned) }}</div>
|
</div>
|
</div>
|
<div class="stats-card consumed">
|
<div class="card-icon">
|
<svg-icon name="local-jifenjian" color="#007CEE"
|
size="50"></svg-icon>
|
</div>
|
<div class="card-content">
|
<div class="card-title">累计消耗</div>
|
<div class="card-value">{{ formatNumber(stats.totalConsumed) }}</div>
|
</div>
|
</div>
|
</div>
|
<div class="stats-background">
|
<div class="coins-bg"></div>
|
</div>
|
</div>
|
|
<!-- 单位积分总览 -->
|
<div class="overview-section">
|
<el-card shadow="never">
|
<template #header>
|
<div class="section-header">
|
<span class="section-title">单位积分总览</span>
|
</div>
|
</template>
|
|
<!-- 筛选条件 -->
|
<div class="filter-section">
|
<div class="filter-row">
|
<div class="filter-item">
|
<span class="filter-label">时间:</span>
|
<el-radio-group v-model="timeType" @change="handleTimeTypeChange">
|
<el-radio-button label="year">年</el-radio-button>
|
<el-radio-button label="month">月</el-radio-button>
|
<el-radio-button label="day">日</el-radio-button>
|
<el-radio-button label="custom">自定义</el-radio-button>
|
</el-radio-group>
|
<el-date-picker
|
v-if="timeType === 'year'"
|
v-model="queryParams.year"
|
type="year"
|
placeholder="选择年份"
|
format="YYYY"
|
value-format="YYYY"
|
style="margin-left: 8px;"
|
/>
|
<el-date-picker
|
v-else-if="timeType === 'month'"
|
v-model="monthValue"
|
type="month"
|
placeholder="选择月份"
|
format="YYYY-MM"
|
value-format="YYYY-MM"
|
style="margin-left: 8px;"
|
@change="handleMonthChange"
|
/>
|
<el-date-picker
|
v-else-if="timeType === 'day'"
|
v-model="queryParams.day"
|
type="date"
|
placeholder="选择日期"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
style="margin-left: 8px;"
|
/>
|
<el-date-picker
|
v-else-if="timeType === 'custom'"
|
v-model="dateRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
style="margin-left: 8px;"
|
@change="handleDateChange"
|
/>
|
</div>
|
<div class="filter-item">
|
<span class="filter-label">数据类目:</span>
|
<el-select v-model="queryParams.dataCategory" placeholder="全部" clearable>
|
<el-option label="全部" value="" />
|
<el-option
|
v-for="category in categoryList"
|
:key="category"
|
:label="getCategoryLabel(category)"
|
:value="category"
|
/>
|
</el-select>
|
</div>
|
<div class="filter-item">
|
<span class="filter-label">数据类型:</span>
|
<el-select v-model="queryParams.dataType" placeholder="全部" clearable>
|
<el-option label="全部" value="" />
|
<el-option label="获取" value="earned" />
|
<el-option label="消耗" value="consumed" />
|
<el-option label="转换" value="converted" />
|
</el-select>
|
</div>
|
</div>
|
<div class="filter-actions">
|
<el-button type="primary" @click="queryData">
|
<el-icon><Search /></el-icon>
|
查询
|
</el-button>
|
<el-button @click="resetQuery">
|
<el-icon><Refresh /></el-icon>
|
重置
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 积分统计详情 -->
|
<div class="stats-details">
|
<div class="stats-summary">
|
<div class="summary-item">
|
<span class="summary-label">获取积分:</span>
|
<span class="summary-value earned">{{ formatNumber(stats.totalEarned) }}</span>
|
<div class="breakdown-items">
|
<div
|
v-for="item in earnedDetails"
|
:key="item.category"
|
class="breakdown-item"
|
>
|
<span class="item-label">{{ getCategoryLabel(item.category) }}</span>
|
<span class="item-value">{{ formatNumber(item.points) }}</span>
|
<span class="item-percentage">({{ item.percentage.toFixed(1) }}%)</span>
|
</div>
|
</div>
|
</div>
|
<div class="summary-item">
|
<span class="summary-label">消耗积分:</span>
|
<span class="summary-value consumed">{{ formatNumber(stats.totalConsumed) }}</span>
|
<div class="breakdown-items">
|
<div
|
v-for="item in consumedDetails"
|
:key="item.category"
|
class="breakdown-item"
|
>
|
<span class="item-label">{{ getCategoryLabel(item.category) }}</span>
|
<span class="item-value">{{ formatNumber(item.points) }}</span>
|
<span class="item-percentage">({{ item.percentage.toFixed(1) }}%)</span>
|
</div>
|
</div>
|
</div>
|
<div class="summary-item">
|
<span class="summary-label">转化积分:</span>
|
<span class="summary-value converted">{{ formatNumber(stats.totalConverted) }}</span>
|
<div class="breakdown-items">
|
<div class="breakdown-item">
|
<span class="item-label">暂无数据</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
</div>
|
</el-card>
|
</div>
|
|
<!-- 积分流水 -->
|
<div class="flow-section">
|
<el-card shadow="never">
|
<template #header>
|
<div class="section-header">
|
<span class="section-title">积分流水</span>
|
</div>
|
</template>
|
|
<!-- 流水表格 -->
|
<div class="table-section">
|
<el-table :data="flowList" stripe style="width: 100%">
|
<el-table-column prop="id" label="序号" width="80" align="center" />
|
<el-table-column prop="dataCategory" label="数据类目" width="120">
|
<template #default="{ row }">
|
<span>{{ getCategoryLabel(row.dataCategory) }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="name" label="名称" min-width="300" show-overflow-tooltip />
|
<el-table-column prop="flowTime" label="时间" width="180" align="center" />
|
<el-table-column prop="points" label="积分" width="100" align="center">
|
<template #default="{ row }">
|
<span :class="row.points > 0 ? 'points-earned' : 'points-consumed'">
|
{{ row.points > 0 ? '+' : '' }}{{ row.points }}
|
</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-section">
|
<div class="pagination-info">
|
共{{ total }}条
|
</div>
|
<el-pagination
|
v-model:current-page="queryParams.pageNum"
|
v-model:page-size="queryParams.pageSize"
|
:page-sizes="[10, 20, 50, 100]"
|
:total="total"
|
layout="sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
</div>
|
</el-card>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { ref, reactive, onMounted, computed } from 'vue'
|
import { dayjs, ElMessage } from 'element-plus'
|
import {
|
Money,
|
Plus,
|
Minus,
|
Search,
|
Refresh
|
} from '@element-plus/icons-vue'
|
import pointsApi from '@/api/pointsApi'
|
import type { PointsStats, PointsFlow, PointsQueryParams, UnitPointsDetail } from '@/types/points'
|
|
// 积分统计
|
const stats = ref<PointsStats>({
|
balance: 2000,
|
totalEarned: 20000,
|
totalConsumed: 18000,
|
totalConverted: 0,
|
})
|
|
// 查询参数
|
const queryParams = reactive<PointsQueryParams>({
|
dataCategory: '',
|
dataType: '',
|
flowStartTime: '',
|
flowEndTime: '',
|
year: new Date().getFullYear().toString(),
|
month: '',
|
day: '',
|
pageNum: 1,
|
pageSize: 10,
|
})
|
|
// 时间类型
|
const timeType = ref('year')
|
|
// 月份选择器值
|
const monthValue = ref('')
|
|
// 日期范围
|
const dateRange = ref<[string, string] | null>(null)
|
|
// 流水列表
|
const flowList = ref<PointsFlow[]>([])
|
|
// 总数
|
const total = ref(0)
|
|
// 数据类目列表
|
const categoryList = ref<string[]>([])
|
|
// 获取积分详情
|
const earnedDetails = ref<UnitPointsDetail[]>([
|
{ category: 'resource_contribution', points: 10000, percentage: 50.0 },
|
{ category: 'resource_transaction', points: 8000, percentage: 40.0 },
|
{ category: 'resource_dissemination', points: 1900, percentage: 9.5 },
|
{ category: 'other', points: 100, percentage: 0.5 },
|
])
|
|
// 消耗积分详情
|
const consumedDetails = ref<UnitPointsDetail[]>([
|
{ category: 'resource_transaction', points: 17500, percentage: 97.2 },
|
{ category: 'resource_dissemination', points: 500, percentage: 2.8 },
|
])
|
|
// 格式化数字
|
const formatNumber = (num: number) => {
|
return num.toLocaleString()
|
}
|
|
// 获取分类标签
|
const getCategoryLabel = (category: string) => {
|
const categoryMap: Record<string, string> = {
|
resource_contribution: '资源贡献',
|
resource_transaction: '资源交易',
|
resource_dissemination: '资源传播',
|
user_participation: '用户参与',
|
points_conversion: '积分转换',
|
other: '其他',
|
}
|
return categoryMap[category] || category
|
}
|
|
// 获取单位积分统计
|
const getUnitPointsStats = async () => {
|
try {
|
const res = await pointsApi.getUnitPointsStats(queryParams)
|
if (res.code === 200 && res.data) {
|
stats.value = res.data.stats || stats.value
|
earnedDetails.value = res.data.earnedDetails || earnedDetails.value
|
consumedDetails.value = res.data.consumedDetails || consumedDetails.value
|
}
|
} catch (error) {
|
console.error('获取单位积分统计失败:', error)
|
}
|
}
|
|
// 获取数据类目列表
|
const getCategoryList = async () => {
|
try {
|
const res = await pointsApi.getPointsFlowCategories()
|
if (res.code === 200 && res.data) {
|
categoryList.value = res.data
|
}
|
} catch (error) {
|
console.error('获取数据类目失败:', error)
|
}
|
}
|
|
// 获取单位积分流水
|
const getUnitPointsFlow = async () => {
|
try {
|
const res = await pointsApi.getUnitPointsFlow(queryParams)
|
if (res.code === 200) {
|
flowList.value = res.data.list || []
|
total.value = res.data.total || 0
|
}
|
} catch (error) {
|
console.error('获取单位积分流水失败:', error)
|
}
|
}
|
|
// 查询数据
|
const queryData = () => {
|
queryParams.pageNum = 1
|
getUnitPointsStats()
|
getUnitPointsFlow()
|
}
|
|
// 重置查询
|
const resetQuery = () => {
|
queryParams.dataCategory = ''
|
queryParams.dataType = ''
|
queryParams.flowStartTime = ''
|
queryParams.flowEndTime = ''
|
queryParams.year = new Date().getFullYear().toString()
|
queryParams.month = ''
|
queryParams.day = ''
|
timeType.value = 'year'
|
monthValue.value = ''
|
dateRange.value = null
|
queryParams.pageNum = 1
|
queryData()
|
}
|
|
// 处理时间类型变化
|
const handleTimeTypeChange = (type: string | number | boolean | undefined) => {
|
// 清空其他时间字段
|
queryParams.flowStartTime = ''
|
queryParams.flowEndTime = ''
|
queryParams.month = ''
|
queryParams.day = ''
|
monthValue.value = ''
|
dateRange.value = null
|
}
|
|
// 处理月份变化
|
const handleMonthChange = (value: string) => {
|
if (value) {
|
const [year, month] = value.split('-')
|
queryParams.year = year
|
queryParams.month = month
|
} else {
|
queryParams.year = ''
|
queryParams.month = ''
|
}
|
}
|
|
// 处理日期变化
|
const handleDateChange = (dates: [string, string] | null) => {
|
if (dates) {
|
queryParams.flowStartTime = dates[0]
|
queryParams.flowEndTime = dates[1]
|
} else {
|
queryParams.flowStartTime = ''
|
queryParams.flowEndTime = ''
|
}
|
}
|
|
// 处理分页大小变化
|
const handleSizeChange = (size: number) => {
|
queryParams.pageSize = size
|
queryParams.pageNum = 1
|
getUnitPointsFlow()
|
}
|
|
// 处理当前页变化
|
const handleCurrentChange = (page: number) => {
|
queryParams.pageNum = page
|
getUnitPointsFlow()
|
}
|
|
onMounted(() => {
|
getUnitPointsStats()
|
getUnitPointsFlow()
|
getCategoryList()
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.unit-points {
|
padding: 20px;
|
background-color: #f5f5f5;
|
min-height: 100vh;
|
|
.stats-section {
|
position: relative;
|
margin-bottom: 20px;
|
padding: 24px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
border-radius: 12px;
|
overflow: hidden;
|
|
.stats-cards {
|
display: flex;
|
gap: 24px;
|
position: relative;
|
z-index: 2;
|
}
|
|
.stats-card {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.95);
|
border-radius: 8px;
|
backdrop-filter: blur(10px);
|
|
.card-icon {
|
width: 48px;
|
height: 48px;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
font-size: 24px;
|
color: white;
|
|
&.balance {
|
background: linear-gradient(135deg, #409eff, #66b1ff);
|
}
|
|
&.earned {
|
background: linear-gradient(135deg, #67c23a, #85ce61);
|
}
|
|
&.consumed {
|
background: linear-gradient(135deg, #f56c6c, #f78989);
|
}
|
}
|
|
.card-content {
|
flex: 1;
|
|
.card-title {
|
font-size: 14px;
|
color: #909399;
|
margin-bottom: 4px;
|
}
|
|
.card-value {
|
font-size: 24px;
|
font-weight: 600;
|
color: #303133;
|
}
|
}
|
}
|
|
.stats-background {
|
position: absolute;
|
top: 0;
|
right: 0;
|
width: 200px;
|
height: 100%;
|
opacity: 0.1;
|
|
.coins-bg {
|
width: 100%;
|
height: 100%;
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ffffff"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
|
background-repeat: repeat;
|
background-size: 24px 24px;
|
}
|
}
|
}
|
|
.overview-section {
|
margin-bottom: 20px;
|
|
.section-header {
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
}
|
|
.filter-section {
|
margin-bottom: 20px;
|
padding: 16px;
|
background: #fafafa;
|
border-radius: 6px;
|
display: flex;
|
flex-direction: row;
|
gap: 16px;
|
font-size: 12px;
|
align-items: center;
|
|
.filter-row {
|
display: flex;
|
gap: 12px;
|
margin-bottom: 0;
|
flex-wrap: nowrap;
|
align-items: center;
|
justify-content: flex-start;
|
flex: 0 0 80%;
|
min-width: 0;
|
}
|
|
.filter-item {
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
flex: 0 0 auto;
|
min-width: 0;
|
|
&:first-child {
|
flex: 0 0 50%;
|
}
|
|
&:nth-child(2) {
|
flex: 0 0 25%;
|
}
|
|
&:nth-child(3) {
|
flex: 0 0 25%;
|
}
|
|
.filter-label {
|
font-size: 12px;
|
color: #606266;
|
white-space: nowrap;
|
flex-shrink: 0;
|
}
|
|
.el-select,
|
.el-date-picker {
|
font-size: 12px;
|
flex: 1;
|
min-width: 0;
|
width: 100%;
|
}
|
|
.el-radio-group {
|
font-size: 12px;
|
display: flex;
|
flex-direction: row;
|
flex-wrap: nowrap;
|
width: auto;
|
|
:deep(.el-radio-button) {
|
margin-right: 0;
|
}
|
|
:deep(.el-radio-button__inner) {
|
border-radius: 0;
|
padding-left: 8px;
|
padding-right: 8px;
|
}
|
|
:deep(.el-radio-button:first-child .el-radio-button__inner) {
|
border-radius: 4px 0 0 4px;
|
}
|
|
:deep(.el-radio-button:last-child .el-radio-button__inner) {
|
border-radius: 0 4px 4px 0;
|
}
|
}
|
}
|
|
.filter-actions {
|
display: flex;
|
gap: 12px;
|
flex: 0 0 20%;
|
justify-content: flex-end;
|
margin-left: auto;
|
|
.el-button {
|
font-size: 12px;
|
}
|
}
|
}
|
|
.stats-details {
|
.stats-summary {
|
display: flex;
|
gap: 16px;
|
margin-bottom: 6px;
|
padding: 6px;
|
background: #f8f9fa;
|
border-radius: 6px;
|
align-items: flex-start;
|
|
.summary-item {
|
display: flex;
|
align-items: flex-start;
|
gap: 12px;
|
flex: 1;
|
border-right: 1px solid #e4e7ed;
|
padding-right: 16px;
|
|
&:last-child {
|
border-right: none;
|
padding-right: 0;
|
}
|
.summary-label {
|
font-size: 14px;
|
color: #606266;
|
width: 5em;
|
display: inline-block;
|
}
|
|
.summary-value {
|
font-size: 16px;
|
font-weight: 600;
|
|
&.earned {
|
color: #67c23a;
|
}
|
|
&.consumed {
|
color: #f56c6c;
|
}
|
|
&.converted {
|
color: #409eff;
|
}
|
}
|
.breakdown-items {
|
display: flex;
|
flex-direction: column;
|
gap: 3px;
|
margin-left: 12px;
|
}
|
|
.breakdown-item {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 1px 0;
|
|
.item-label {
|
width: 70px;
|
min-width: 70px;
|
font-size: 14px;
|
color: #606266;
|
white-space: nowrap;
|
}
|
|
.item-value {
|
font-size: 14px;
|
font-weight: 500;
|
color: #303133;
|
min-width: 60px;
|
text-align: right;
|
}
|
|
.item-percentage {
|
font-size: 12px;
|
color: #909399;
|
min-width: 40px;
|
text-align: right;
|
}
|
}
|
}
|
}
|
|
.stats-breakdown {
|
margin-bottom: 20px;
|
|
.breakdown-title {
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
margin-bottom: 12px;
|
padding-bottom: 8px;
|
border-bottom: 1px solid #ebeef5;
|
}
|
|
|
}
|
}
|
}
|
|
.flow-section {
|
.section-header {
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
}
|
|
.table-section {
|
.points-earned {
|
color: #67c23a;
|
font-weight: 500;
|
}
|
|
.points-consumed {
|
color: #f56c6c;
|
font-weight: 500;
|
}
|
}
|
|
.pagination-section {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-top: 20px;
|
padding-top: 16px;
|
border-top: 1px solid #ebeef5;
|
|
.pagination-info {
|
font-size: 14px;
|
color: #909399;
|
}
|
}
|
}
|
}
|
</style>
|