逻辑修改
This commit is contained in:
parent
dea416c120
commit
8cada25804
102
src/MilitaryTrainingManagement/Controllers/StatsController.cs
Normal file
102
src/MilitaryTrainingManagement/Controllers/StatsController.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilitaryTrainingManagement.Data;
|
||||
using MilitaryTrainingManagement.Models.Enums;
|
||||
|
||||
namespace MilitaryTrainingManagement.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 统计数据控制器
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class StatsController : BaseApiController
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public StatsController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取首页统计数据
|
||||
/// </summary>
|
||||
[HttpGet("dashboard")]
|
||||
public async Task<IActionResult> GetDashboardStats()
|
||||
{
|
||||
var unitId = GetCurrentUnitId();
|
||||
var unitLevel = GetCurrentUnitLevel();
|
||||
|
||||
if (unitId == null || unitLevel == null)
|
||||
return Unauthorized(new { message = "无法获取用户组织信息" });
|
||||
|
||||
// 获取当前单位及下级单位ID列表
|
||||
var unitIds = await GetUnitAndSubordinateIds(unitId.Value);
|
||||
|
||||
// 统计配额数
|
||||
var allocationsCount = await _context.MaterialAllocations
|
||||
.Where(a => a.CreatedByUnitId == unitId.Value || unitIds.Contains(a.CreatedByUnitId))
|
||||
.CountAsync();
|
||||
|
||||
// 统计完成率
|
||||
var distributions = await _context.AllocationDistributions
|
||||
.Where(d => unitIds.Contains(d.TargetUnitId))
|
||||
.ToListAsync();
|
||||
|
||||
decimal completionRate = 0;
|
||||
if (distributions.Any())
|
||||
{
|
||||
var totalQuota = distributions.Sum(d => d.UnitQuota);
|
||||
var totalCompletion = distributions.Sum(d => d.ActualCompletion ?? 0);
|
||||
if (totalQuota > 0)
|
||||
{
|
||||
completionRate = Math.Round((totalCompletion / totalQuota) * 100, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 统计人员数
|
||||
var personnelCount = await _context.Personnel
|
||||
.Where(p => unitIds.Contains(p.SubmittedByUnitId) && p.Status == PersonnelStatus.Approved)
|
||||
.CountAsync();
|
||||
|
||||
// 统计待审批数(仅师团级别显示)
|
||||
var pendingApprovals = 0;
|
||||
if (unitLevel == OrganizationalLevel.Division)
|
||||
{
|
||||
pendingApprovals = await _context.ApprovalRequests
|
||||
.Where(r => r.Status == ApprovalStatus.Pending)
|
||||
.CountAsync();
|
||||
|
||||
// 加上待审批的人员
|
||||
pendingApprovals += await _context.Personnel
|
||||
.Where(p => p.Status == PersonnelStatus.Pending)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
allocations = allocationsCount,
|
||||
completionRate = completionRate,
|
||||
personnel = personnelCount,
|
||||
pendingApprovals = pendingApprovals
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<List<int>> GetUnitAndSubordinateIds(int unitId)
|
||||
{
|
||||
var result = new List<int> { unitId };
|
||||
var children = await _context.OrganizationalUnits
|
||||
.Where(u => u.ParentId == unitId)
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var childId in children)
|
||||
{
|
||||
var subIds = await GetUnitAndSubordinateIds(childId);
|
||||
result.AddRange(subIds);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -13,7 +13,7 @@ using System.Reflection;
|
|||
[assembly: System.Reflection.AssemblyCompanyAttribute("MilitaryTrainingManagement")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+dea416c1206a35a2f77e33f37406891d5377f1a6")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("MilitaryTrainingManagement")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("MilitaryTrainingManagement")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
407f322c0b72f117c925ae6a80f31111d05088df1322e7b528cdc9a219109c9a
|
||||
71761eb666e1de962287ab2d3efffb4d59f9a1ceb181540c49222eadb77fb760
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
c42f36142723a436c8872fc0ee41f859ea9256e151b6b81825ce7ce0070f694e
|
||||
9aa7a804a7b716c6d51cb391fd2292638fb67f08685ae7d440982f042b40fd5e
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -4,4 +4,5 @@ export { allocationsApi } from './allocations'
|
|||
export { reportsApi } from './reports'
|
||||
export { personnelApi } from './personnel'
|
||||
export { approvalsApi } from './approvals'
|
||||
export { statsApi } from './stats'
|
||||
export { default as apiClient } from './client'
|
||||
|
|
|
|||
15
src/frontend/src/api/stats.ts
Normal file
15
src/frontend/src/api/stats.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import apiClient from './client'
|
||||
|
||||
export interface DashboardStats {
|
||||
allocations: number
|
||||
completionRate: number
|
||||
personnel: number
|
||||
pendingApprovals: number
|
||||
}
|
||||
|
||||
export const statsApi = {
|
||||
async getDashboardStats(): Promise<DashboardStats> {
|
||||
const response = await apiClient.get<DashboardStats>('/stats/dashboard')
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
|
@ -97,8 +97,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { statsApi } from '@/api'
|
||||
import { OrganizationalLevel } from '@/types'
|
||||
import { Box, DataAnalysis, User, Checked } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
|
|
@ -109,6 +111,8 @@ const stats = ref({
|
|||
pendingApprovals: 0
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const levelName = computed(() => {
|
||||
const level = authStore.user?.organizationalLevel
|
||||
switch (level) {
|
||||
|
|
@ -120,14 +124,19 @@ const levelName = computed(() => {
|
|||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// TODO: Fetch actual stats from API
|
||||
stats.value = {
|
||||
allocations: 12,
|
||||
completionRate: 78,
|
||||
personnel: 156,
|
||||
pendingApprovals: 5
|
||||
async function loadStats() {
|
||||
loading.value = true
|
||||
try {
|
||||
stats.value = await statsApi.getDashboardStats()
|
||||
} catch {
|
||||
ElMessage.error('加载统计数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,29 +22,31 @@
|
|||
<el-divider content-position="left">配额分配</el-divider>
|
||||
|
||||
<el-form-item label="分配明细">
|
||||
<div class="distribution-list">
|
||||
<div v-for="(dist, index) in form.distributions" :key="index" class="distribution-item">
|
||||
<el-select v-model="dist.targetUnitId" placeholder="选择目标单位" style="width: 200px">
|
||||
<el-option
|
||||
v-for="unit in subordinateUnits"
|
||||
:key="unit.id"
|
||||
:label="unit.name"
|
||||
:value="unit.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number v-model="dist.unitQuota" :min="0" :precision="2" placeholder="配额" />
|
||||
<el-button type="danger" :icon="Delete" circle @click="removeDistribution(index)" />
|
||||
<div class="distribution-section">
|
||||
<div class="distribution-list">
|
||||
<div v-for="(dist, index) in form.distributions" :key="index" class="distribution-item">
|
||||
<el-select v-model="dist.targetUnitId" placeholder="选择目标单位" style="width: 200px">
|
||||
<el-option
|
||||
v-for="unit in subordinateUnits"
|
||||
:key="unit.id"
|
||||
:label="unit.name"
|
||||
:value="unit.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number v-model="dist.unitQuota" :min="0" :precision="2" placeholder="配额" />
|
||||
<el-button type="danger" :icon="Delete" circle @click="removeDistribution(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="distribution-actions">
|
||||
<el-button type="primary" text @click="addDistribution">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分配
|
||||
</el-button>
|
||||
<span class="quota-info">已分配: {{ distributedTotal }}</span>
|
||||
<span class="quota-info" :class="{ 'over-quota': distributedTotal > form.totalQuota }">
|
||||
剩余: {{ (form.totalQuota - distributedTotal).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button type="primary" text @click="addDistribution">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分配
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="quota-summary">
|
||||
<span>已分配: {{ distributedTotal }}</span>
|
||||
<span :class="{ 'over-quota': distributedTotal > form.totalQuota }">
|
||||
剩余: {{ (form.totalQuota - distributedTotal).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
|
|
@ -181,10 +183,15 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.distribution-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.distribution-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.distribution-item {
|
||||
|
|
@ -193,11 +200,15 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.quota-summary {
|
||||
margin-top: 12px;
|
||||
.distribution-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.quota-info {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.over-quota {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user