This commit is contained in:
那些花儿
2025-10-17 14:18:52 +08:00
parent b729639d01
commit 501e64022b
9 changed files with 1123 additions and 5 deletions
@@ -0,0 +1,422 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.dashboard-container{
height: calc(100vh - 64px - 20px);
display: flex;
flex-direction: column;
}
.stat-dashboard {
padding: 20px 0;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 0 auto;
}
.stat-card {
background: #ffffff;
padding: 24px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 16px;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-4px);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 32px;
font-weight: bold;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
font-weight: 500;
}
@media (max-width: 768px) {
.stat-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
padding: 0 15px;
}
.stat-card {
padding: 20px;
}
.stat-number {
font-size: 28px;
}
.stat-icon {
width: 50px;
height: 50px;
font-size: 20px;
}
}
@media (max-width: 480px) {
.stat-grid {
grid-template-columns: 1fr;
padding: 0;
}
.stat-card {
padding: 16px;
}
.stat-number {
font-size: 24px;
}
}
.content-section {
display: flex;
gap: 20px;
flex: 1;
}
.left-section {
flex: 1;
min-width: 0;
}
.right-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
.section-card, .chart-card {
background: white;
padding: 20px;
border: 1px solid #f0f0f0;
height: 100%;
display: flex;
flex-direction: column;
}
.chart-card {
flex: 1;
min-height: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
}
.chart-container {
flex: 1;
/*min-height: 200px;*/
}
/* 响应式布局 */
@media (max-width: 1024px) {
.content-section {
flex-direction: column;
height: auto;
}
.left-section, .right-section {
flex: none;
}
.section-card {
height: 400px;
}
.chart-card {
height: 300px;
}
}
@media (max-width: 768px) {
.content-section {
gap: 15px;
margin-top: 15px;
}
.section-card, .chart-card {
padding: 15px;
border-radius: 8px;
}
.section-title {
font-size: 14px;
margin-bottom: 12px;
}
.chart-container {
min-height: 180px;
}
}
</style>
<div id="app" v-cloak>
<div class="dashboard-container">
<!-- 统计数据展示区域 -->
<div class="stat-dashboard">
<div class="stat-grid">
<div class="stat-card">
<div class="stat-icon medical-icon">
<i class="fa fa-heartbeat"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ statCount.subsidyCount || 0 }}</div>
<div class="stat-label">医疗补助数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon new-member-icon">
<i class="fa fa-user-plus"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ statCount.newCount || 0 }}</div>
<div class="stat-label">本年新增会员数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon total-member-icon">
<i class="fa fa-users"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ statCount.count || 0 }}</div>
<div class="stat-label">基金会员总数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon money-icon">
<i class="fa fa-yen"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ (statCount.totalSubsidyCost || 0).toLocaleString() }}</div>
<div class="stat-label">医疗补助总金额</div>
</div>
</div>
</div>
</div>
<!-- 下方内容区域 -->
<div class="content-section">
<!-- 左侧表格区域 -->
<div class="left-section">
<div class="section-card">
<div class="section-title">各分工会会员数量</div>
<el-table :data="unionCount" style="width: 100%" height="100%">
<el-table-column prop="name" label="分工会名称" min-width="200"></el-table-column>
<el-table-column prop="count" label="会员数量" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.count > 0 ? 'success' : 'info'">
{{ scope.row.count }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 右侧图表区域 -->
<div class="right-section">
<!-- 右上:在职状态图表 -->
<div class="chart-card">
<div class="section-title">在职状态分布</div>
<div id="userStateChart" class="chart-container"></div>
</div>
<!-- 右下:人员类型柱状图 -->
<div class="chart-card">
<div class="section-title">人员类型统计</div>
<div id="personTypeChart" class="chart-container"></div>
</div>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
statCount: {
// 医疗补助数
"subsidyCount": 12,
// 新增会员数
"newCount": 3,
//基金会员总数
"count": 3,
// 医疗补助金额
"totalSubsidyCost": 146401
},
unionCount: [],
userStateCount: [],
personTypeCount: [],
userStateChart: null,
personTypeChart: null
}
},
methods: {
// 统计数量
getStatCount() {
this.$axios.post('/platform/fund/dashBoard/statCount').then(res => {
if (res.code === 0) {
this.statCount = res.data
}
})
},
// 各分工会基金会员数
getStatUnionCount() {
this.$axios.post('/platform/fund/dashBoard/statUnionCount').then(res => {
if (res.code === 0) {
this.unionCount = res.data
}
})
},
// 各在职状态基金会员数
getUserStateCount() {
this.$axios.post('/platform/fund/dashBoard/userStateCount').then(res => {
if (res.code === 0) {
this.userStateCount = res.data
this.renderUserStateChart()
}
})
},
// 各人员类型基金会员数
getPersonTypeCount() {
this.$axios.post('/platform/fund/dashBoard/personTypeCount').then(res => {
if (res.code === 0) {
this.personTypeCount = res.data
this.renderPersonTypeChart()
}
})
},
// 渲染在职状态饼图
renderUserStateChart() {
if (this.userStateChart) {
this.userStateChart.destroy()
}
const data = []
if (this.userStateCount && this.userStateCount.length > 0) {
const stateData = this.userStateCount[0]
if (stateData.onDutyCount > 0) {
data.push({ type: '在职', value: stateData.onDutyCount })
}
if (stateData.retireCount > 0) {
data.push({ type: '退休', value: stateData.retireCount })
}
}
this.userStateChart = new G2Plot.Pie('userStateChart', {
data: data,
angleField: 'value',
colorField: 'type',
radius: 0.8,
label: {
type: 'outer',
content: '{name} {percentage}'
},
interactions: [{ type: 'element-active' }],
color: ['#5B8FF9', '#5AD8A6']
})
this.userStateChart.render()
},
// 渲染人员类型柱状图
renderPersonTypeChart() {
if (this.personTypeChart) {
this.personTypeChart.destroy()
}
this.personTypeChart = new G2Plot.Column('personTypeChart', {
data: this.personTypeCount,
xField: 'personType',
yField: 'userCount',
label: {
position: 'middle',
style: {
fill: '#FFFFFF',
opacity: 0.6,
},
},
xAxis: {
label: {
autoHide: true,
autoRotate: false,
},
},
meta: {
personType: {
alias: '人员类型',
},
userCount: {
alias: '人数',
},
},
color: '#5B8FF9'
})
this.personTypeChart.render()
}
},
created() {
this.getStatCount()
this.getStatUnionCount()
this.getUserStateCount()
this.getPersonTypeCount()
},
mounted() {
// 初始化图表
this.$nextTick(() => {
this.renderUserStateChart()
this.renderPersonTypeChart()
})
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,550 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.analysis-page {
background: #f6f6f6;
min-height: calc(100vh - 64px);
padding: 20px;
}
.stats-row {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
padding: 30px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: transform 0.3s ease;
cursor: pointer;
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card.card-blue {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-card.card-green {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.card-orange {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card.card-purple {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.stat-number {
font-size: 36px;
font-weight: bold;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.stat-label {
font-size: 16px;
opacity: 0.9;
font-weight: 500;
}
.chart-card {
margin-bottom: 20px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.growth-tag {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
}
.chart-container {
height: 350px;
width: 100%;
}
.comparison-table {
margin-top: 20px;
}
.el-table .growth-cell {
font-weight: bold;
}
.growth-positive {
color: #67c23a;
}
.growth-negative {
color: #f56c6c;
}
.growth-zero {
color: #909399;
}
/* 响应式设计 */
@media (max-width: 768px) {
.analysis-page {
padding: 10px;
}
.stat-card {
padding: 20px 15px;
height: 100px;
}
.stat-number {
font-size: 28px;
}
.stat-label {
font-size: 14px;
}
.chart-container {
height: 280px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
@media (max-width: 480px) {
.stat-card {
padding: 15px 10px;
height: 90px;
}
.stat-number {
font-size: 24px;
}
.stat-label {
font-size: 12px;
}
.chart-container {
height: 250px;
}
}
/* 加载状态样式 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.loading-text {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
/* 错误状态样式 */
.error-container {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
color: #f56c6c;
}
.error-text {
margin-left: 10px;
font-size: 14px;
}
</style>
<div id="app" v-cloak>
<div class="analysis-page">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<i class="el-icon-loading"></i>
<span class="loading-text">数据加载中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<i class="el-icon-warning"></i>
<span class="error-text">{{ error }}</span>
</div>
<!-- 主要内容 -->
<div v-else>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card card-blue">
<div class="stat-number">{{ totalStats.totalPeople | number }}</div>
<div class="stat-label">总补助人数</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card card-green">
<div class="stat-number">{{ (totalStats.totalAmount / 10000) | currency }}</div>
<div class="stat-label">总补助金额(万元)</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card card-orange">
<div class="stat-number">{{ totalStats.avgAmount | currency }}</div>
<div class="stat-label">平均补助金额(元)</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card card-purple">
<div class="stat-number">{{ totalStats.yearCount }}</div>
<div class="stat-label">统计年度数</div>
</div>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20">
<el-col :xs="24" :lg="12">
<el-card class="chart-card" shadow="hover">
<div class="chart-header">
<span class="chart-title">年度补助人数趋势</span>
<el-tag v-if="growthRates.people !== null"
:type="getGrowthTagType(growthRates.people)"
class="growth-tag">
{{ growthRates.people > 0 ? '+' : '' }}{{ growthRates.people }}%
</el-tag>
</div>
<div ref="peopleChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :xs="24" :lg="12">
<el-card class="chart-card" shadow="hover">
<div class="chart-header">
<span class="chart-title">年度补助金额趋势</span>
<el-tag v-if="growthRates.amount !== null"
:type="getGrowthTagType(growthRates.amount)"
class="growth-tag">
{{ growthRates.amount > 0 ? '+' : '' }}{{ growthRates.amount }}%
</el-tag>
</div>
<div ref="amountChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 对比表格 -->
<el-card class="comparison-table" shadow="hover">
<div slot="header">
<span class="chart-title">年度对比分析</span>
</div>
<el-table :data="yearlyComparison" stripe style="width: 100%">
<el-table-column prop="year" label="年度" width="100" align="center"></el-table-column>
<el-table-column prop="people" label="补助人数" align="center">
<template slot-scope="scope">
{{ scope.row.people | number }}
</template>
</el-table-column>
<el-table-column label="人数增长率" align="center" width="120">
<template slot-scope="scope">
<span v-if="scope.row.peopleGrowthRate !== null"
:class="['growth-cell', getGrowthClass(scope.row.peopleGrowthRate)]">
{{ scope.row.peopleGrowthRate > 0 ? '+' : '' }}{{ scope.row.peopleGrowthRate }}%
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="补助金额(万元)" align="center">
<template slot-scope="scope">
{{ (scope.row.amount / 10000) | currency }}
</template>
</el-table-column>
<el-table-column label="金额增长率" align="center" width="120">
<template slot-scope="scope">
<span v-if="scope.row.amountGrowthRate !== null"
:class="['growth-cell', getGrowthClass(scope.row.amountGrowthRate)]">
{{ scope.row.amountGrowthRate > 0 ? '+' : '' }}{{ scope.row.amountGrowthRate }}%
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="人均补助(元)" align="center">
<template slot-scope="scope">
{{ scope.row.avgAmount | currency }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
loading: true,
error: null,
yearlyData: [],
totalStats: {
totalPeople: 0,
totalAmount: 0,
avgAmount: 0,
yearCount: 0
},
growthRates: {
people: null,
amount: null
},
peopleChart: null,
amountChart: null
}
},
computed: {
yearlyComparison() {
return this.yearlyData.map((item, index) => {
const prevItem = index > 0 ? this.yearlyData[index - 1] : null;
return {
year: item.year,
people: item.people,
amount: item.amount,
avgAmount: item.people > 0 ? Math.round(item.amount / item.people) : 0,
peopleGrowthRate: prevItem ?
Math.round(((item.people - prevItem.people) / prevItem.people) * 100) : null,
amountGrowthRate: prevItem ?
Math.round(((item.amount - prevItem.amount) / prevItem.amount) * 100) : null
};
});
}
},
filters: {
number(value) {
if (!value && value !== 0) return '0';
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
currency(value) {
if (!value && value !== 0) return '0.00';
return parseFloat(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
},
mounted() {
this.loadData();
},
methods: {
async loadData() {
try {
this.loading = true;
this.error = null;
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟数据
this.yearlyData = [
{ year: 2020, people: 1250, amount: 3750000 },
{ year: 2021, people: 1380, amount: 4140000 },
{ year: 2022, people: 1520, amount: 4560000 },
{ year: 2023, people: 1680, amount: 5040000 },
{ year: 2024, people: 1850, amount: 5550000 }
];
this.calculateTotalStats();
this.calculateGrowthRates();
// 等待DOM更新后渲染图表
this.$nextTick(() => {
this.renderCharts();
});
} catch (error) {
this.error = '数据加载失败:' + error.message;
} finally {
this.loading = false;
}
},
calculateTotalStats() {
this.totalStats.totalPeople = this.yearlyData.reduce((sum, item) => sum + item.people, 0);
this.totalStats.totalAmount = this.yearlyData.reduce((sum, item) => sum + item.amount, 0);
this.totalStats.avgAmount = this.totalStats.totalPeople > 0 ?
Math.round(this.totalStats.totalAmount / this.totalStats.totalPeople) : 0;
this.totalStats.yearCount = this.yearlyData.length;
},
calculateGrowthRates() {
if (this.yearlyData.length >= 2) {
const firstYear = this.yearlyData[0];
const lastYear = this.yearlyData[this.yearlyData.length - 1];
this.growthRates.people = Math.round(
((lastYear.people - firstYear.people) / firstYear.people) * 100
);
this.growthRates.amount = Math.round(
((lastYear.amount - firstYear.amount) / firstYear.amount) * 100
);
}
},
renderCharts() {
// 确保DOM元素存在后再渲染图表
if (this.$refs.peopleChart && this.$refs.amountChart) {
this.renderPeopleChart();
this.renderAmountChart();
}
},
renderPeopleChart() {
// 销毁已存在的图表
if (this.peopleChart) {
this.peopleChart.destroy();
}
const data = this.yearlyData.map(item => ({
year: item.year.toString(),
value: item.people
}));
this.peopleChart = new G2Plot.Line(this.$refs.peopleChart, {
data,
xField: 'year',
yField: 'value',
point: {
size: 5,
shape: 'diamond',
style: {
fill: 'white',
stroke: '#5B8FF9',
lineWidth: 2,
},
},
tooltip: {
showMarkers: false,
formatter: (datum) => {
return { name: '补助人数', value: datum.value + ' 人' };
},
},
color: '#5B8FF9',
lineStyle: {
lineWidth: 3,
},
smooth: true,
animation: {
appear: {
animation: 'path-in',
duration: 1000,
},
},
});
this.peopleChart.render();
},
renderAmountChart() {
// 销毁已存在的图表
if (this.amountChart) {
this.amountChart.destroy();
}
const data = this.yearlyData.map(item => ({
year: item.year.toString(),
value: item.amount / 10000 // 转换为万元
}));
this.amountChart = new G2Plot.Line(this.$refs.amountChart, {
data,
xField: 'year',
yField: 'value',
point: {
size: 5,
shape: 'diamond',
style: {
fill: 'white',
stroke: '#5AD8A6',
lineWidth: 2,
},
},
tooltip: {
showMarkers: false,
formatter: (datum) => {
return { name: '补助金额', value: datum.value + ' 万元' };
},
},
color: '#5AD8A6',
lineStyle: {
lineWidth: 3,
},
smooth: true,
animation: {
appear: {
animation: 'path-in',
duration: 1000,
},
},
});
this.amountChart.render();
},
getGrowthTagType(rate) {
if (rate > 0) return 'success';
if (rate < 0) return 'danger';
return 'info';
},
getGrowthClass(rate) {
if (rate > 0) return 'growth-positive';
if (rate < 0) return 'growth-negative';
return 'growth-zero';
}
},
beforeDestroy() {
if (this.peopleChart) {
this.peopleChart.destroy();
}
if (this.amountChart) {
this.amountChart.destroy();
}
}
});
</script>
<!--#
}
#-->
@@ -84,7 +84,7 @@ layout("/layouts/platform.html"){
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到提案人</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回修改</el-button>
<el-button @click="handleTaskAction(20)" size="small" type="danger">不同意</el-button>
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意</el-button>
</el-row>
@@ -174,7 +174,7 @@ layout("/layouts/platform.html"){
})
},
exportDocx(id) {
window.open(loc() + "/exportDocx")
this.$downLoad("/platform/proposal/common/exportProposalAsDocx", {id})
},
tagToggle(key, val) {
if (this.pageForm[key].includes(val)) {
@@ -83,7 +83,7 @@ layout("/layouts/platform.html"){
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到提案人</el-button>
<el-button @click="handleTaskAction(20)" size="small" type="danger">不同意</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">不同意</el-button>
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意</el-button>
</el-row>
</div>
@@ -106,7 +106,7 @@ layout("/layouts/platform.html"){
{label: "提案类别", prop: "typeName"},
{label: "届次", prop: "sessionName"},
{label: "代表团", prop: "delegationName"},
{label: "当前节点", prop: "curTaskName"},
{label: "当前节点", prop: "taskName"},
{label: "流程状态", prop: "instanceState"}
]
}