..
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
package com.budwk.app.zhgh.dayofficework.fund.dashboard;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.zhgh.dayofficework.fund.member.service.FundMemberService;
|
||||
import com.budwk.app.zhgh.dayofficework.fund.subsidy.models.FundSubsidy;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/fund/dashBoard")
|
||||
@Ok("json:full")
|
||||
@Slf4j
|
||||
public class FundDashBoardController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private FundMemberService fundMemberService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/dayofficework/fund/dashboard/index.html")
|
||||
@SaCheckPermission("fund.dashBoard")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("fund.dashBoard")
|
||||
@ApiOperation("统计数量")
|
||||
public Result statCount() {
|
||||
// 基金会员总数
|
||||
int count = dao.count(Sys_user.class, Cnd.where(Sys_user::getAidFundMember, "=", 1));
|
||||
// 本年新增基金会员数
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
count(1)
|
||||
FROM
|
||||
fund_member_change_record r
|
||||
LEFT JOIN sys_user u ON u.loginname = r.loginName
|
||||
WHERE
|
||||
r.isJoin = 1
|
||||
AND YEAR(r.changeTime) = @year
|
||||
AND u.aidFundMember = 1
|
||||
""");
|
||||
sql.params().set("year", DateUtil.thisYear());
|
||||
sql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(sql);
|
||||
int newCount = sql.getInt();
|
||||
|
||||
// 医疗补助数
|
||||
int subsidyCount = dao.count(FundSubsidy.class, Cnd.NEW());
|
||||
|
||||
// 总补助金额
|
||||
int totalSubsidyCost = dao.func(FundSubsidy.class, "sum", "subsidyCost");
|
||||
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("count", count);
|
||||
map.put("newCount", newCount);
|
||||
map.put("subsidyCount", subsidyCount);
|
||||
map.put("totalSubsidyCost", totalSubsidyCost);
|
||||
|
||||
return Result.success(map);
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("fund.dashBoard")
|
||||
@ApiOperation("各分工会基金会员数")
|
||||
public Result statUnionCount() {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
gh.id,
|
||||
gh.`name`,
|
||||
COUNT(vu.id) AS count
|
||||
FROM
|
||||
sys_union gh
|
||||
LEFT JOIN
|
||||
vw_user vu ON vu.unionId = gh.id AND vu.aidFundMember = 1
|
||||
GROUP BY
|
||||
gh.id, gh.`name`
|
||||
ORDER BY
|
||||
gh.unionCode ASC;
|
||||
""");
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(sql);
|
||||
return Result.success(sql.getList(HashMap.class));
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("fund.dashBoard")
|
||||
@ApiOperation("各在职状态基金会员数")
|
||||
public Result userStateCount(){
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
COUNT(CASE WHEN userState != '退休' THEN 1 END) AS onDutyCount,
|
||||
COUNT(CASE WHEN userState = '退休' THEN 1 END) AS retireCount
|
||||
FROM
|
||||
sys_user
|
||||
WHERE
|
||||
aidFundMember = 1;
|
||||
""");
|
||||
return Result.success(fundMemberService.listMap(sql));
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("fund.dashBoard")
|
||||
@ApiOperation("各人员类型基金会员数")
|
||||
public Result personTypeCount(){
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
personType,
|
||||
COUNT(*) AS userCount
|
||||
FROM
|
||||
sys_user
|
||||
WHERE
|
||||
aidFundMember = 1
|
||||
GROUP BY
|
||||
personType;
|
||||
""");
|
||||
return Result.success(fundMemberService.listMap(sql));
|
||||
}
|
||||
|
||||
}
|
||||
+10
@@ -1,7 +1,9 @@
|
||||
package com.budwk.app.zhgh.dayofficework.fund.subsidy.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
@@ -19,4 +21,12 @@ public class FundSubsidyAnalysisController {
|
||||
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("fundSubsidy.analysis")
|
||||
@ApiOperation("年度补助人数")
|
||||
public Result yearSubsidyCount() {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class TeacherCongressSessionController {
|
||||
public Result pageData(PageForm pageForm, String j, String c, Integer year) {
|
||||
Sql sql = Sqls.create("select * from teacher_congress_session $condition");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("YEAR(startDate)", "=", year);
|
||||
cnd.andEX("year", "=", year);
|
||||
cnd.andEX("j", "=", j);
|
||||
cnd.andEX("c", "=", c);
|
||||
cnd.desc("startDate");
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+550
@@ -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>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+1
-1
@@ -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)) {
|
||||
|
||||
+1
-1
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user