连级修改
This commit is contained in:
parent
1f793aa004
commit
f639840803
|
|
@ -318,7 +318,8 @@ public class AllocationsController : BaseApiController
|
|||
distributionId,
|
||||
unitId.Value,
|
||||
userId.Value,
|
||||
request.ActualCompletion);
|
||||
request.ActualCompletion,
|
||||
request.Remarks);
|
||||
|
||||
return Ok(MapDistributionToResponse(distribution));
|
||||
}
|
||||
|
|
@ -433,6 +434,29 @@ public class AllocationsController : BaseApiController
|
|||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取按单位汇总的上报数据
|
||||
/// </summary>
|
||||
[HttpGet("distributions/{distributionId}/summary")]
|
||||
public async Task<IActionResult> GetReportSummaryByUnit(int distributionId)
|
||||
{
|
||||
var unitId = GetCurrentUnitId();
|
||||
var unitLevel = GetCurrentUnitLevel();
|
||||
|
||||
if (unitId == null || unitLevel == null)
|
||||
return Unauthorized(new { message = "无法获取用户组织信息" });
|
||||
|
||||
// 检查分配记录是否存在
|
||||
var distribution = await _allocationService.GetDistributionByIdAsync(distributionId);
|
||||
if (distribution == null)
|
||||
return NotFound(new { message = "配额分配记录不存在" });
|
||||
|
||||
// 获取按单位汇总的上报数据
|
||||
var summaries = await _allocationService.GetReportSummaryByUnitAsync(distributionId, unitId.Value, unitLevel.Value);
|
||||
|
||||
return Ok(summaries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射实体到响应DTO
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public class ReportsController : BaseApiController
|
|||
/// 需求3.1:显示类别、物资名称、单位、配额、实际完成和完成率
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetByUnit()
|
||||
public async Task<IActionResult> GetByUnit([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
|
||||
{
|
||||
var unitId = GetCurrentUnitId();
|
||||
if (unitId == null)
|
||||
|
|
@ -41,9 +41,23 @@ public class ReportsController : BaseApiController
|
|||
return Unauthorized(new { message = "无法获取用户组织信息" });
|
||||
}
|
||||
|
||||
var distributions = await _reportingService.GetDistributionsByUnitAsync(unitId.Value);
|
||||
var response = distributions.Select(MapToReportResponse);
|
||||
return Ok(response);
|
||||
var allDistributions = await _reportingService.GetDistributionsByUnitAsync(unitId.Value);
|
||||
var distributionsList = allDistributions.ToList();
|
||||
var totalCount = distributionsList.Count;
|
||||
|
||||
var pagedDistributions = distributionsList
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(MapToReportResponse);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
items = pagedDistributions,
|
||||
totalCount,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
totalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ public class UpdateDistributionRequest
|
|||
[Required(ErrorMessage = "实际完成数量为必填项")]
|
||||
[Range(0, double.MaxValue, ErrorMessage = "实际完成数量不能为负数")]
|
||||
public decimal ActualCompletion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
[MaxLength(500)]
|
||||
public string? Remarks { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@ public class UnitReportSummary
|
|||
public decimal CompletionRate { get; set; }
|
||||
public DateTime? ReportedAt { get; set; }
|
||||
public string? ReportedByUserName { get; set; }
|
||||
|
||||
// 新增字段:用于按单位汇总上报数据
|
||||
public decimal TotalReported { get; set; }
|
||||
public int ReportCount { get; set; }
|
||||
public DateTime? LastReportedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ builder.Services.AddControllers()
|
|||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||
// 将枚举序列化为字符串
|
||||
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||
// 使用 camelCase 命名策略(输出)
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
|
||||
// 允许不区分大小写的属性名匹配(输入)
|
||||
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
});
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using MilitaryTrainingManagement.Data;
|
||||
using MilitaryTrainingManagement.Models.Entities;
|
||||
using MilitaryTrainingManagement.Models.DTOs;
|
||||
using MilitaryTrainingManagement.Services.Interfaces;
|
||||
|
||||
namespace MilitaryTrainingManagement.Services.Implementations;
|
||||
|
|
@ -270,7 +271,8 @@ public class AllocationService : IAllocationService
|
|||
int distributionId,
|
||||
int unitId,
|
||||
int userId,
|
||||
decimal actualCompletion)
|
||||
decimal actualCompletion,
|
||||
string? remarks = null)
|
||||
{
|
||||
var distribution = await _context.AllocationDistributions
|
||||
.Include(d => d.Allocation)
|
||||
|
|
@ -312,6 +314,7 @@ public class AllocationService : IAllocationService
|
|||
AllocationDistributionId = distributionId,
|
||||
ReportedAmount = reportedAmount,
|
||||
CumulativeAmount = actualCompletion,
|
||||
Remarks = remarks,
|
||||
ReportedByUserId = userId,
|
||||
ReportedAt = DateTime.UtcNow
|
||||
};
|
||||
|
|
@ -448,4 +451,38 @@ public class AllocationService : IAllocationService
|
|||
.Where(r => visibleUnitIds.Contains(r.ReportedByUser.OrganizationalUnitId))
|
||||
.Sum(r => r.ReportedAmount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取按单位汇总的上报数据(带可见性过滤)
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<UnitReportSummary>> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, Models.Enums.OrganizationalLevel userLevel)
|
||||
{
|
||||
// 获取所有上报记录(不做可见性过滤,因为师团级需要看到所有下级的汇总)
|
||||
var allReports = await _context.ConsumptionReports
|
||||
.Include(r => r.ReportedByUser)
|
||||
.ThenInclude(u => u.OrganizationalUnit)
|
||||
.Where(r => r.AllocationDistributionId == distributionId)
|
||||
.ToListAsync();
|
||||
|
||||
// 按单位分组汇总
|
||||
var summaries = allReports
|
||||
.GroupBy(r => new {
|
||||
UnitId = r.ReportedByUser.OrganizationalUnitId,
|
||||
UnitName = r.ReportedByUser.OrganizationalUnit.Name,
|
||||
UnitLevel = r.ReportedByUser.OrganizationalUnit.Level.ToString()
|
||||
})
|
||||
.Select(g => new UnitReportSummary
|
||||
{
|
||||
UnitId = g.Key.UnitId,
|
||||
UnitName = g.Key.UnitName,
|
||||
UnitLevel = g.Key.UnitLevel,
|
||||
TotalReported = g.Sum(r => r.ReportedAmount),
|
||||
ReportCount = g.Count(),
|
||||
LastReportedAt = g.Max(r => r.ReportedAt)
|
||||
})
|
||||
.OrderByDescending(s => s.TotalReported)
|
||||
.ToList();
|
||||
|
||||
return summaries;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public interface IAllocationService
|
|||
/// <summary>
|
||||
/// 更新单个分配记录的实际完成数量(上报消耗)
|
||||
/// </summary>
|
||||
Task<AllocationDistribution> UpdateDistributionCompletionAsync(int distributionId, int unitId, int userId, decimal actualCompletion);
|
||||
Task<AllocationDistribution> UpdateDistributionCompletionAsync(int distributionId, int unitId, int userId, decimal actualCompletion, string? remarks = null);
|
||||
|
||||
/// <summary>
|
||||
/// 删除物资配额
|
||||
|
|
@ -95,4 +95,9 @@ public interface IAllocationService
|
|||
/// 营部及以下级别:只计算本单位及直接下级的上报
|
||||
/// </summary>
|
||||
Task<decimal> GetVisibleActualCompletionAsync(int allocationId, int userUnitId, OrganizationalLevel userLevel);
|
||||
|
||||
/// <summary>
|
||||
/// 获取按单位汇总的上报数据(带可见性过滤)
|
||||
/// </summary>
|
||||
Task<IEnumerable<Models.DTOs.UnitReportSummary>> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, OrganizationalLevel userLevel);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ export const allocationsApi = {
|
|||
async getConsumptionReports(distributionId: number): Promise<ConsumptionReport[]> {
|
||||
const response = await apiClient.get<ConsumptionReport[]>(`/allocations/distributions/${distributionId}/reports`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getReportSummaryByUnit(distributionId: number): Promise<UnitReportSummary[]> {
|
||||
const response = await apiClient.get<UnitReportSummary[]>(`/allocations/distributions/${distributionId}/summary`)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,3 +67,12 @@ export interface ConsumptionReport {
|
|||
reportedByUserName?: string
|
||||
reportedAt: string
|
||||
}
|
||||
|
||||
export interface UnitReportSummary {
|
||||
unitId: number
|
||||
unitName: string
|
||||
unitLevel: string
|
||||
totalReported: number
|
||||
reportCount: number
|
||||
lastReportedAt?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export { authApi } from './auth'
|
||||
export { organizationsApi } from './organizations'
|
||||
export { allocationsApi } from './allocations'
|
||||
export { allocationsApi, type UnitReportSummary } from './allocations'
|
||||
export { materialCategoriesApi } from './materialCategories'
|
||||
export { reportsApi } from './reports'
|
||||
export { personnelApi } from './personnel'
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@
|
|||
<el-menu-item index="/allocations">配额列表</el-menu-item>
|
||||
<el-menu-item v-if="authStore.canCreateAllocations" index="/allocations/create">创建配额</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="reports">
|
||||
|
||||
<!-- 上报管理:只对团级账号可见(师团创建配额,营部及以下用配额列表上报) -->
|
||||
<el-sub-menu v-if="authStore.organizationalLevelNum === 2" index="reports">
|
||||
<template #title>
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>上报管理</span>
|
||||
|
|
@ -39,7 +40,13 @@
|
|||
<el-menu-item index="/reports">上报列表</el-menu-item>
|
||||
<el-menu-item index="/reports/summary">数据汇总</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
|
||||
<!-- 数据汇总:师团级单独显示(查看下级汇总数据) -->
|
||||
<el-menu-item v-if="authStore.organizationalLevelNum === 1" index="/reports/summary">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>数据汇总</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="personnel">
|
||||
<template #title>
|
||||
<el-icon><User /></el-icon>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export interface DistributionRequest {
|
|||
|
||||
export interface UpdateDistributionRequest {
|
||||
actualCompletion: number
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
// Report types
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@
|
|||
<div class="summary-item">
|
||||
<div class="summary-label">物资类别</div>
|
||||
<div class="summary-value">
|
||||
<el-tag :type="getCategoryTagType(selectedAllocation?.category)" size="large" effect="plain">
|
||||
<el-tag :type="getCategoryTagType(selectedAllocation?.category || '')" size="large" effect="plain">
|
||||
{{ selectedAllocation?.category }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
|
@ -325,29 +325,35 @@
|
|||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
<!-- Consumption Reports Dialog -->
|
||||
<el-dialog
|
||||
v-model="showReportsDialog"
|
||||
:title="`上报记录 - ${selectedDistribution?.targetUnitName || ''}`"
|
||||
width="700px"
|
||||
<el-dialog
|
||||
v-model="showReportsDialog"
|
||||
:title="`上报记录 - ${selectedDistribution?.targetUnitName || ''}`"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="reports-summary">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-col :span="6">
|
||||
<div class="report-stat">
|
||||
<div class="stat-label">分配配额</div>
|
||||
<div class="stat-value highlight">{{ formatNumber(selectedDistribution?.unitQuota || 0) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="6">
|
||||
<div class="report-stat">
|
||||
<div class="stat-label">累计消耗</div>
|
||||
<div class="stat-value consumed">{{ formatNumber(selectedDistribution?.actualCompletion || 0) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="6">
|
||||
<div class="report-stat">
|
||||
<div class="stat-label">上报单位数</div>
|
||||
<div class="stat-value">{{ unitSummaries.length }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="report-stat">
|
||||
<div class="stat-label">上报次数</div>
|
||||
<div class="stat-value">{{ consumptionReports.length }}</div>
|
||||
|
|
@ -355,40 +361,87 @@
|
|||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="consumptionReports"
|
||||
style="width: 100%"
|
||||
v-loading="loadingReports"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="reportedAmount" label="本次上报" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="report-amount">+{{ formatNumber(row.reportedAmount) }}</span>
|
||||
|
||||
<el-tabs v-model="reportsTabActive" class="reports-tabs">
|
||||
<el-tab-pane label="按单位汇总" name="summary">
|
||||
<el-table
|
||||
:data="unitSummaries"
|
||||
style="width: 100%"
|
||||
v-loading="loadingReports"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="unitName" label="单位名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="unit-cell">
|
||||
<el-icon class="unit-icon"><OfficeBuilding /></el-icon>
|
||||
<span class="unit-name">{{ row.unitName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unitLevel" label="单位级别" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getUnitLevelTagType(row.unitLevel)" size="small">
|
||||
{{ getUnitLevelText(row.unitLevel) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalReported" label="上报总量" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="total-reported">{{ formatNumber(row.totalReported) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportCount" label="上报次数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" size="small">{{ row.reportCount }} 次</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastReportedAt" label="最后上报时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ row.lastReportedAt ? formatDate(row.lastReportedAt) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template v-if="unitSummaries.length === 0 && !loadingReports">
|
||||
<el-empty description="暂无上报数据" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cumulativeAmount" label="累计数量" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="cumulative-amount">{{ formatNumber(row.cumulativeAmount) }}</span>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="上报明细" name="detail">
|
||||
<el-table
|
||||
:data="consumptionReports"
|
||||
style="width: 100%"
|
||||
v-loading="loadingReports"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="reportedAmount" label="本次上报" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="report-amount">+{{ formatNumber(row.reportedAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cumulativeAmount" label="累计数量" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="cumulative-amount">{{ formatNumber(row.cumulativeAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedByUserName" label="上报人" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.reportedByUserName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedAt" label="上报时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDate(row.reportedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template v-if="consumptionReports.length === 0 && !loadingReports">
|
||||
<el-empty description="暂无上报记录" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedByUserName" label="上报人" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.reportedByUserName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedAt" label="上报时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDate(row.reportedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template v-if="consumptionReports.length === 0 && !loadingReports">
|
||||
<el-empty description="暂无上报记录" />
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -399,7 +452,7 @@ import { useRouter } from 'vue-router'
|
|||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, View, Edit, Delete, Box, Setting, OfficeBuilding, Clock } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { allocationsApi } from '@/api'
|
||||
import { allocationsApi, type UnitReportSummary } from '@/api'
|
||||
import type { MaterialAllocation, AllocationDistribution } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
|
|
@ -408,6 +461,8 @@ const authStore = useAuthStore()
|
|||
const allocations = ref<MaterialAllocation[]>([])
|
||||
const distributions = ref<AllocationDistribution[]>([])
|
||||
const consumptionReports = ref<any[]>([])
|
||||
const unitSummaries = ref<UnitReportSummary[]>([])
|
||||
const reportsTabActive = ref('summary')
|
||||
const loading = ref(false)
|
||||
const loadingReports = ref(false)
|
||||
const showDistributionDialog = ref(false)
|
||||
|
|
@ -458,7 +513,7 @@ function getCategoryTagType(category: string): string {
|
|||
case '装备': return 'warning'
|
||||
case '物资': return 'success'
|
||||
case '器材': return 'info'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -468,6 +523,26 @@ function getProgressStatus(rate: number): string {
|
|||
return 'warning'
|
||||
}
|
||||
|
||||
function getUnitLevelTagType(level: string): string {
|
||||
switch (level) {
|
||||
case 'Division': return 'danger'
|
||||
case 'Regiment': return 'warning'
|
||||
case 'Battalion': return 'success'
|
||||
case 'Company': return 'info'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
function getUnitLevelText(level: string): string {
|
||||
switch (level) {
|
||||
case 'Division': return '师团'
|
||||
case 'Regiment': return '团'
|
||||
case 'Battalion': return '营'
|
||||
case 'Company': return '连'
|
||||
default: return level
|
||||
}
|
||||
}
|
||||
|
||||
function getDistributionPercentage(allocation: MaterialAllocation): number {
|
||||
if (!allocation.distributions || allocation.distributions.length === 0) return 0
|
||||
const distributed = allocation.distributions.reduce((sum, d) => sum + (d.unitQuota || 0), 0)
|
||||
|
|
@ -556,13 +631,21 @@ function handleReportFromSummary() {
|
|||
async function handleViewReports(distribution: AllocationDistribution) {
|
||||
selectedDistribution.value = distribution
|
||||
showReportsDialog.value = true
|
||||
reportsTabActive.value = 'summary'
|
||||
loadingReports.value = true
|
||||
try {
|
||||
const reports = await allocationsApi.getConsumptionReports(distribution.id)
|
||||
// 同时加载汇总数据和明细数据
|
||||
const [reports, summaries] = await Promise.all([
|
||||
allocationsApi.getConsumptionReports(distribution.id),
|
||||
allocationsApi.getReportSummaryByUnit(distribution.id)
|
||||
])
|
||||
consumptionReports.value = reports
|
||||
} catch {
|
||||
unitSummaries.value = summaries
|
||||
} catch (error) {
|
||||
console.error('加载上报记录失败', error)
|
||||
ElMessage.error('加载上报记录失败')
|
||||
consumptionReports.value = []
|
||||
unitSummaries.value = []
|
||||
} finally {
|
||||
loadingReports.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,20 @@
|
|||
label-width="120px"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<!-- 连部显示上报目标选择 -->
|
||||
<el-form-item v-if="isCompanyLevel" label="上报到" prop="reportToLevel">
|
||||
<el-radio-group v-model="form.reportToLevel">
|
||||
<el-radio value="Battalion">
|
||||
<el-tag type="success" size="small">营部</el-tag>
|
||||
<span class="radio-desc">上报到所属营部</span>
|
||||
</el-radio>
|
||||
<el-radio value="Regiment">
|
||||
<el-tag type="warning" size="small">团部</el-tag>
|
||||
<span class="radio-desc">直接上报到团部</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="isBattalionOrBelow ? '本次消耗数量' : '本次上报数量'" prop="actualCompletion">
|
||||
<el-input-number
|
||||
v-model="form.actualCompletion"
|
||||
|
|
@ -141,7 +155,7 @@
|
|||
<h3 class="section-title">上报历史</h3>
|
||||
<el-table
|
||||
v-if="consumptionReports.length > 0"
|
||||
:data="consumptionReports"
|
||||
:data="reportsWithRecalculatedCumulative"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
|
|
@ -160,7 +174,7 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="累计数量" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="cumulative-amount">{{ formatNumber(row.cumulativeAmount) }}</span>
|
||||
<span class="cumulative-amount">{{ formatNumber(row.calculatedCumulativeAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上报人" width="120" align="center">
|
||||
|
|
@ -205,7 +219,13 @@ const formRef = ref<FormInstance>()
|
|||
|
||||
const form = reactive({
|
||||
actualCompletion: 0,
|
||||
remarks: ''
|
||||
remarks: '',
|
||||
reportToLevel: '' as string // 上报目标级别:Battalion(营部)或 Regiment(团部)
|
||||
})
|
||||
|
||||
// 判断是否为连部级别(Company = 4)
|
||||
const isCompanyLevel = computed(() => {
|
||||
return authStore.organizationalLevelNum === 4
|
||||
})
|
||||
|
||||
// 判断是否为营部及以下级别(Battalion = 3, Company = 4)
|
||||
|
|
@ -220,6 +240,24 @@ const totalReportedAmount = computed(() => {
|
|||
return consumptionReports.value.reduce((sum, report) => sum + report.reportedAmount, 0)
|
||||
})
|
||||
|
||||
// 重新计算累计数量(基于可见范围内的上报记录)
|
||||
// 因为数据库中的cumulativeAmount是所有单位的累计,需要根据过滤后的记录重新计算
|
||||
const reportsWithRecalculatedCumulative = computed(() => {
|
||||
// 按时间正序排列计算累计
|
||||
const sortedByTimeAsc = [...consumptionReports.value].sort(
|
||||
(a, b) => new Date(a.reportedAt).getTime() - new Date(b.reportedAt).getTime()
|
||||
)
|
||||
|
||||
let cumulative = 0
|
||||
const reportsWithCumulative = sortedByTimeAsc.map(report => {
|
||||
cumulative += report.reportedAmount
|
||||
return { ...report, calculatedCumulativeAmount: cumulative }
|
||||
})
|
||||
|
||||
// 按时间倒序返回(与原始顺序一致)
|
||||
return reportsWithCumulative.reverse()
|
||||
})
|
||||
|
||||
// 根据用户级别动态生成验证规则
|
||||
const formRules = computed<FormRules>(() => {
|
||||
const baseRules: FormRules = {
|
||||
|
|
@ -234,6 +272,13 @@ const formRules = computed<FormRules>(() => {
|
|||
]
|
||||
}
|
||||
|
||||
// 连部需要选择上报目标
|
||||
if (isCompanyLevel.value) {
|
||||
baseRules.reportToLevel = [
|
||||
{ required: true, message: '请选择上报目标', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 只有师团/团级才需要验证不超过剩余配额
|
||||
if (!isBattalionOrBelow.value) {
|
||||
baseRules.actualCompletion.push({
|
||||
|
|
@ -266,7 +311,7 @@ function getCategoryTagType(category?: string): string {
|
|||
case '装备': return 'warning'
|
||||
case '物资': return 'success'
|
||||
case '器材': return 'info'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,10 +340,16 @@ async function handleSubmit() {
|
|||
|
||||
const newTotal = (distribution.value?.actualCompletion || 0) + form.actualCompletion
|
||||
|
||||
// 营部及以下级别的确认信息不显示总数,使用"消耗"文字
|
||||
const confirmMessage = isBattalionOrBelow.value
|
||||
? `本次消耗数量:${form.actualCompletion} ${allocation.value?.unit}\n\n确认提交吗?`
|
||||
: `本次上报数量:${form.actualCompletion} ${allocation.value?.unit}\n上报后总数:${newTotal} ${allocation.value?.unit}\n\n确认提交吗?`
|
||||
// 构建确认信息
|
||||
let confirmMessage = ''
|
||||
if (isCompanyLevel.value) {
|
||||
const targetName = form.reportToLevel === 'Battalion' ? '营部' : '团部'
|
||||
confirmMessage = `本次消耗数量:${form.actualCompletion} ${allocation.value?.unit}\n上报目标:${targetName}\n\n确认提交吗?`
|
||||
} else if (isBattalionOrBelow.value) {
|
||||
confirmMessage = `本次消耗数量:${form.actualCompletion} ${allocation.value?.unit}\n\n确认提交吗?`
|
||||
} else {
|
||||
confirmMessage = `本次上报数量:${form.actualCompletion} ${allocation.value?.unit}\n上报后总数:${newTotal} ${allocation.value?.unit}\n\n确认提交吗?`
|
||||
}
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
confirmMessage,
|
||||
|
|
@ -314,9 +365,17 @@ async function handleSubmit() {
|
|||
|
||||
if (!distribution.value) return
|
||||
|
||||
// 构建备注信息(包含上报目标)
|
||||
let remarks = form.remarks || ''
|
||||
if (isCompanyLevel.value && form.reportToLevel) {
|
||||
const targetName = form.reportToLevel === 'Battalion' ? '营部' : '团部'
|
||||
remarks = remarks ? `[上报到${targetName}] ${remarks}` : `[上报到${targetName}]`
|
||||
}
|
||||
|
||||
// 累加到已有数量
|
||||
await allocationsApi.updateDistribution(distribution.value.id, {
|
||||
actualCompletion: newTotal
|
||||
actualCompletion: newTotal,
|
||||
remarks: remarks
|
||||
})
|
||||
|
||||
ElMessage.success(isBattalionOrBelow.value ? '消耗提交成功' : '上报成功')
|
||||
|
|
@ -560,4 +619,23 @@ onMounted(() => {
|
|||
.history-section :deep(.el-table) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.radio-desc {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-radio-group) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ function getTypeTagType(type: ApprovalRequestType): string {
|
|||
case ApprovalRequestType.AllocationModification: return 'primary'
|
||||
case ApprovalRequestType.ReportModification: return 'success'
|
||||
case ApprovalRequestType.PersonnelModification: return 'warning'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ function getStatusTagType(status: ApprovalStatus): string {
|
|||
case ApprovalStatus.Pending: return 'warning'
|
||||
case ApprovalStatus.Approved: return 'success'
|
||||
case ApprovalStatus.Rejected: return 'danger'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ function getTypeTagType(type: ApprovalRequestType): string {
|
|||
case ApprovalRequestType.AllocationModification: return 'primary'
|
||||
case ApprovalRequestType.ReportModification: return 'success'
|
||||
case ApprovalRequestType.PersonnelModification: return 'warning'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ function getStatusTagType(status: ApprovalStatus): string {
|
|||
case ApprovalStatus.Pending: return 'warning'
|
||||
case ApprovalStatus.Approved: return 'success'
|
||||
case ApprovalStatus.Rejected: return 'danger'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ function getLevelTagType(level: OrganizationalLevel): string {
|
|||
case OrganizationalLevel.Regiment: return 'warning'
|
||||
case OrganizationalLevel.Battalion: return 'success'
|
||||
case OrganizationalLevel.Company: return 'info'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ function getStatusTagType(status: PersonnelStatus): string {
|
|||
case PersonnelStatus.Pending: return 'warning'
|
||||
case PersonnelStatus.Approved: return 'success'
|
||||
case PersonnelStatus.Rejected: return 'danger'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ function getLevelTagType(level: PersonnelLevel): string {
|
|||
case PersonnelLevel.Regiment: return 'warning'
|
||||
case PersonnelLevel.Battalion: return 'success'
|
||||
case PersonnelLevel.Company: return 'info'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
</el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.gender === '男' ? '' : 'danger'" size="small" effect="plain">
|
||||
<el-tag :type="row.gender === '男' ? 'primary' : 'danger'" size="small" effect="plain">
|
||||
{{ row.gender }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
|
@ -202,7 +202,7 @@ function getStatusTagType(status: PersonnelStatus): string {
|
|||
case PersonnelStatus.Pending: return 'warning'
|
||||
case PersonnelStatus.Approved: return 'success'
|
||||
case PersonnelStatus.Rejected: return 'danger'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ function getLevelTagType(level: PersonnelLevel): string {
|
|||
case PersonnelLevel.Regiment: return 'warning'
|
||||
case PersonnelLevel.Battalion: return 'success'
|
||||
case PersonnelLevel.Company: return 'info'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +1,196 @@
|
|||
<template>
|
||||
<div class="report-list">
|
||||
<el-card>
|
||||
<el-card class="report-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>上报管理</span>
|
||||
<div class="header-title">
|
||||
<el-icon class="title-icon" :size="22"><DataAnalysis /></el-icon>
|
||||
<span>上报管理</span>
|
||||
</div>
|
||||
<div class="header-stats">
|
||||
<el-tag type="info" effect="plain">
|
||||
共 {{ pagination.total }} 条记录
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="reports" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="category" label="类别" width="120" />
|
||||
<el-table-column prop="materialName" label="物资名称" />
|
||||
<el-table-column prop="unit" label="单位" width="80" />
|
||||
<el-table-column prop="unitQuota" label="配额" width="100" />
|
||||
<el-table-column prop="actualCompletion" label="实际完成" width="120">
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon total">
|
||||
<el-icon :size="24"><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ pagination.total }}</div>
|
||||
<div class="stat-label">总配额数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon reported">
|
||||
<el-icon :size="24"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ reportedCount }}</div>
|
||||
<div class="stat-label">已上报</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon pending">
|
||||
<el-icon :size="24"><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ pendingCount }}</div>
|
||||
<div class="stat-label">待上报</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon rate">
|
||||
<el-icon :size="24"><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ averageCompletionRate }}%</div>
|
||||
<div class="stat-label">平均完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="reports"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="category" label="类别" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.actualCompletion !== null">{{ row.actualCompletion }}</span>
|
||||
<el-tag v-else type="warning" size="small">未上报</el-tag>
|
||||
<el-tag :type="getCategoryTagType(row.category)" size="small">
|
||||
{{ row.category }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="completionRate" label="完成率" width="150">
|
||||
<el-table-column prop="materialName" label="物资名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Math.round(row.completionRate * 100)" :status="getProgressStatus(row.completionRate)" />
|
||||
<span class="material-name">{{ row.materialName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="targetUnitName" label="所属单位" width="150" />
|
||||
<el-table-column prop="reportedAt" label="上报时间" width="180">
|
||||
<el-table-column prop="unit" label="单位" width="80" align="center" />
|
||||
<el-table-column prop="unitQuota" label="配额" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.reportedAt ? formatDate(row.reportedAt) : '-' }}
|
||||
<span class="quota-value">{{ formatNumber(row.unitQuota) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column prop="actualCompletion" label="实际完成" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.actualCompletion !== null" class="completion-value">
|
||||
{{ formatNumber(row.actualCompletion) }}
|
||||
</span>
|
||||
<el-tag v-else type="warning" size="small" effect="light">未上报</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="completionRate" label="完成率" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="progress-cell">
|
||||
<el-progress
|
||||
:percentage="Math.round(row.completionRate * 100)"
|
||||
:status="getProgressStatus(row.completionRate)"
|
||||
:stroke-width="8"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="targetUnitName" label="所属单位" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="unit-name">{{ row.targetUnitName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedAt" label="上报时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.reportedAt" class="time-cell">
|
||||
<el-icon class="time-icon"><Clock /></el-icon>
|
||||
<span>{{ formatDate(row.reportedAt) }}</span>
|
||||
</div>
|
||||
<span v-else class="no-data">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canReport"
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
:type="row.actualCompletion !== null ? 'warning' : 'primary'"
|
||||
size="small"
|
||||
@click="handleReport(row)"
|
||||
>
|
||||
<el-icon class="btn-icon"><Edit /></el-icon>
|
||||
{{ row.actualCompletion !== null ? '修改' : '上报' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
:current-page="pagination.pageNumber"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
class="pagination"
|
||||
@update:current-page="handlePageChange"
|
||||
@update:page-size="handleSizeChange"
|
||||
/>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNumber"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
||||
<!-- Report Dialog -->
|
||||
<el-dialog v-model="showReportDialog" title="上报数据" width="500px">
|
||||
<el-dialog
|
||||
v-model="showReportDialog"
|
||||
:title="selectedReport?.actualCompletion !== null ? '修改上报数据' : '上报数据'"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formRef" :model="reportForm" :rules="rules" label-width="100px">
|
||||
<el-form-item label="物资类别">
|
||||
<el-tag :type="getCategoryTagType(selectedReport?.category)">
|
||||
{{ selectedReport?.category }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="物资名称">
|
||||
<el-input :value="selectedReport?.materialName" disabled />
|
||||
<span class="dialog-value">{{ selectedReport?.materialName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="配额">
|
||||
<el-input :value="selectedReport?.unitQuota" disabled />
|
||||
<span class="dialog-value">{{ formatNumber(selectedReport?.unitQuota || 0) }} {{ selectedReport?.unit }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前完成" v-if="selectedReport?.actualCompletion !== null">
|
||||
<span class="dialog-value highlight">{{ formatNumber(selectedReport?.actualCompletion || 0) }} {{ selectedReport?.unit }}</span>
|
||||
</el-form-item>
|
||||
<el-divider />
|
||||
<el-form-item label="实际完成" prop="actualCompletion">
|
||||
<el-input-number v-model="reportForm.actualCompletion" :min="0" :precision="2" style="width: 100%" />
|
||||
<el-input-number
|
||||
v-model="reportForm.actualCompletion"
|
||||
:min="0"
|
||||
:max="selectedReport?.unitQuota"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="unit-hint">{{ selectedReport?.unit }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="完成进度">
|
||||
<el-progress
|
||||
:percentage="getFormCompletionRate()"
|
||||
:status="getProgressStatus(getFormCompletionRate() / 100)"
|
||||
:stroke-width="12"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showReportDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmitReport">提交</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmitReport">
|
||||
<el-icon class="btn-icon"><Check /></el-icon>
|
||||
确认提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
|
@ -80,6 +199,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, FormInstance, FormRules } from 'element-plus'
|
||||
import { DataAnalysis, Box, CircleCheck, Clock, TrendCharts, Edit, Check } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { reportsApi } from '@/api'
|
||||
import type { ReportData } from '@/types'
|
||||
|
|
@ -105,24 +225,55 @@ const reportForm = reactive({
|
|||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
actualCompletion: [{ required: true, message: '请输入实际完成数量', trigger: 'blur' }]
|
||||
actualCompletion: [
|
||||
{ required: true, message: '请输入实际完成数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '数量不能为负数', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const reportedCount = computed(() => reports.value.filter(r => r.actualCompletion !== null).length)
|
||||
const pendingCount = computed(() => reports.value.filter(r => r.actualCompletion === null).length)
|
||||
const averageCompletionRate = computed(() => {
|
||||
if (reports.value.length === 0) return 0
|
||||
const total = reports.value.reduce((sum, r) => sum + (r.completionRate || 0), 0)
|
||||
return Math.round(total / reports.value.length * 100)
|
||||
})
|
||||
|
||||
// Company level users can only view, not report
|
||||
const canReport = computed(() => {
|
||||
return authStore.user?.organizationalLevel !== OrganizationalLevel.Company
|
||||
})
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return num?.toLocaleString('zh-CN') ?? '0'
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function getCategoryTagType(category?: string): string {
|
||||
switch (category) {
|
||||
case '弹药': return 'danger'
|
||||
case '装备': return 'warning'
|
||||
case '物资': return 'success'
|
||||
case '器材': return 'info'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
function getProgressStatus(rate: number): string {
|
||||
if (rate >= 1) return 'success'
|
||||
if (rate >= 0.6) return ''
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function getFormCompletionRate(): number {
|
||||
if (!selectedReport.value?.unitQuota) return 0
|
||||
return Math.round((reportForm.actualCompletion / selectedReport.value.unitQuota) * 100)
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.pageNumber = page
|
||||
loadReports()
|
||||
|
|
@ -158,10 +309,10 @@ function handleReport(report: ReportData) {
|
|||
|
||||
async function handleSubmitReport() {
|
||||
if (!formRef.value || !selectedReport.value) return
|
||||
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await reportsApi.submit({
|
||||
|
|
@ -185,14 +336,158 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.report-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #fff 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
||||
}
|
||||
|
||||
.stat-icon.reported {
|
||||
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
||||
}
|
||||
|
||||
.stat-icon.pending {
|
||||
background: linear-gradient(135deg, #e6a23c 0%, #ebb563 100%);
|
||||
}
|
||||
|
||||
.stat-icon.rate {
|
||||
background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%);
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.material-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.quota-value {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.completion-value {
|
||||
font-weight: 600;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.unit-name {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.dialog-value {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.dialog-value.highlight {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.unit-hint {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ function getLevelTagType(level: OrganizationalLevel): string {
|
|||
case OrganizationalLevel.Regiment: return 'warning'
|
||||
case OrganizationalLevel.Battalion: return 'success'
|
||||
case OrganizationalLevel.Company: return 'info'
|
||||
default: return ''
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user