逻辑修改

This commit is contained in:
18631081161 2026-01-13 16:40:57 +08:00
parent dea416c120
commit 8cada25804
16 changed files with 172 additions and 34 deletions

View 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;
}
}

View File

@ -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")]

View File

@ -1 +1 @@
407f322c0b72f117c925ae6a80f31111d05088df1322e7b528cdc9a219109c9a
71761eb666e1de962287ab2d3efffb4d59f9a1ceb181540c49222eadb77fb760

View File

@ -1 +1 @@
c42f36142723a436c8872fc0ee41f859ea9256e151b6b81825ce7ce0070f694e
9aa7a804a7b716c6d51cb391fd2292638fb67f08685ae7d440982f042b40fd5e

View File

@ -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'

View 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
}
}

View File

@ -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>

View File

@ -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 {