This commit is contained in:
18631081161 2026-01-22 22:52:53 +08:00
parent 8f1840d823
commit 3b0a533934
4 changed files with 404 additions and 84 deletions

View File

@ -25,9 +25,9 @@
<el-sub-menu v-if="canViewAllocations" index="allocations">
<template #title>
<el-icon><Box /></el-icon>
<span>弹种配额</span>
<span>训练消耗</span>
</template>
<el-menu-item index="/allocations">配额列表</el-menu-item>
<el-menu-item index="/allocations">弹药指标</el-menu-item>
<el-menu-item v-if="authStore.canCreateAllocations" index="/allocations/create">创建配额</el-menu-item>
<el-menu-item index="/allocations/change-requests">删改申请</el-menu-item>
</el-sub-menu>
@ -41,10 +41,11 @@
<el-menu-item index="/personnel/create">添加人才</el-menu-item>
</el-sub-menu>
<el-menu-item v-if="authStore.canApprove" index="/approvals">
<!-- 审批管理已隐藏功能已整合到各业务页面 -->
<!-- <el-menu-item v-if="authStore.canApprove" index="/approvals">
<el-icon><Checked /></el-icon>
<span>审批管理</span>
</el-menu-item>
</el-menu-item> -->
<el-menu-item v-if="authStore.hasPermission(1)" index="/audit-logs">
<el-icon><Document /></el-icon>

View File

@ -1,54 +1,69 @@
<template>
<div class="dashboard">
<el-row :gutter="20">
<!-- 师本部和团本部显示弹种配额 -->
<el-col v-if="authStore.organizationalLevelNum <= 2" :span="6">
<el-card class="stat-card">
<template #header>
<div class="card-header">
<el-icon :size="24" color="#409EFF"><Box /></el-icon>
<span>弹种配额</span>
</div>
</template>
<div class="stat-value">{{ stats.allocations }}</div>
<div class="stat-label">总配额条数</div>
</el-card>
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="welcome-content">
<div class="welcome-text">
<h1>欢迎回来{{ authStore.user?.username }}</h1>
<p>{{ currentDate }} · {{ levelName }}</p>
</div>
<div class="welcome-icon">
<el-icon :size="64" color="rgba(255,255,255,0.8)"><Promotion /></el-icon>
</div>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stat-row">
<el-col v-if="authStore.organizationalLevelNum <= 2" :span="8">
<div class="stat-card stat-card-blue">
<div class="stat-icon">
<el-icon :size="40"><Box /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.allocations }}</div>
<div class="stat-label">弹种配额</div>
</div>
<div class="stat-decoration"></div>
</div>
</el-col>
<!-- 师团级和团级显示完成率 -->
<el-col v-if="authStore.organizationalLevelNum <= 2" :span="6">
<el-card class="stat-card">
<template #header>
<div class="card-header">
<el-icon :size="24" color="#67C23A"><DataAnalysis /></el-icon>
<span>完成率</span>
</div>
</template>
<div class="stat-value">{{ stats.completionRate }}%</div>
<div class="stat-label">平均完成率</div>
</el-card>
<el-col v-if="authStore.organizationalLevelNum <= 2" :span="8">
<div class="stat-card stat-card-green">
<div class="stat-icon">
<el-icon :size="40"><DataAnalysis /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.completionRate }}%</div>
<div class="stat-label">平均完成率</div>
</div>
<div class="stat-decoration"></div>
</div>
</el-col>
<el-col :span="authStore.organizationalLevelNum <= 2 ? 6 : 12">
<el-card class="stat-card">
<template #header>
<div class="card-header">
<el-icon :size="24" color="#E6A23C"><User /></el-icon>
<span>人才管理</span>
</div>
</template>
<div class="stat-value">{{ stats.personnel }}</div>
<div class="stat-label">人员总数</div>
</el-card>
<el-col :span="authStore.organizationalLevelNum <= 2 ? 8 : 24">
<div class="stat-card stat-card-orange">
<div class="stat-icon">
<el-icon :size="40"><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.personnel }}</div>
<div class="stat-label">人才总数</div>
</div>
<div class="stat-decoration"></div>
</div>
</el-col>
</el-row>
<!-- 饼状图区域 -->
<el-row :gutter="20" class="mt-20">
<el-row v-if="authStore.organizationalLevelNum <= 2" :gutter="20" class="chart-row">
<el-col :span="24">
<el-card>
<el-card class="chart-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon :size="24" color="#409EFF"><PieChart /></el-icon>
<span>各团物资消耗情况</span>
<div class="chart-header">
<div class="chart-title">
<el-icon :size="22" color="#409EFF"><PieChart /></el-icon>
<span>各团物资消耗情况</span>
</div>
<el-tag type="info" effect="plain">实时数据</el-tag>
</div>
</template>
<div v-if="regimentStats.length > 0" class="pie-charts-container">
@ -57,11 +72,20 @@
:key="regiment.regimentId"
class="pie-chart-item"
>
<v-chart :option="getPieOption(regiment)" autoresize class="pie-chart" />
<div class="pie-chart-wrapper">
<v-chart :option="getPieOption(regiment)" autoresize class="pie-chart" />
</div>
<div class="regiment-name">{{ regiment.regimentName }}</div>
<div class="regiment-info">
<span>配额: {{ regiment.totalQuota }}</span>
<span>消耗: {{ regiment.totalConsumed }}</span>
<div class="info-item">
<span class="info-label">配额</span>
<span class="info-value">{{ regiment.totalQuota }}</span>
</div>
<div class="info-divider"></div>
<div class="info-item">
<span class="info-label">消耗</span>
<span class="info-value consumed">{{ regiment.totalConsumed }}</span>
</div>
</div>
</div>
</div>
@ -69,15 +93,58 @@
</el-card>
</el-col>
</el-row>
<!-- 快捷操作 -->
<el-row :gutter="20" class="quick-actions-row">
<el-col :span="24">
<el-card class="quick-card" shadow="hover">
<template #header>
<div class="chart-header">
<div class="chart-title">
<el-icon :size="22" color="#E6A23C"><Operation /></el-icon>
<span>快捷操作</span>
</div>
</div>
</template>
<div class="quick-actions">
<div v-if="authStore.organizationalLevelNum <= 2" class="action-item" @click="$router.push('/allocations')">
<div class="action-icon action-icon-blue">
<el-icon :size="28"><Box /></el-icon>
</div>
<span class="action-text">弹药指标</span>
</div>
<div class="action-item" @click="$router.push('/personnel')">
<div class="action-icon action-icon-orange">
<el-icon :size="28"><User /></el-icon>
</div>
<span class="action-text">人才管理</span>
</div>
<div class="action-item" @click="$router.push('/personnel/create')">
<div class="action-icon action-icon-green">
<el-icon :size="28"><Plus /></el-icon>
</div>
<span class="action-text">添加人才</span>
</div>
<div v-if="authStore.hasPermission(1)" class="action-item" @click="$router.push('/organizations')">
<div class="action-icon action-icon-purple">
<el-icon :size="28"><OfficeBuilding /></el-icon>
</div>
<span class="action-text">组织管理</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { statsApi, type RegimentAllocationStats } from '@/api/stats'
import { Box, DataAnalysis, User, PieChart } from '@element-plus/icons-vue'
import { Box, DataAnalysis, User, PieChart, Promotion, Operation, Plus, OfficeBuilding } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { OrganizationalLevel } from '@/types'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { PieChart as EchartsPie } from 'echarts/charts'
@ -98,11 +165,26 @@ const stats = ref({
const regimentStats = ref<RegimentAllocationStats[]>([])
const loading = ref(false)
const currentDate = computed(() => {
const now = new Date()
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}${weekDays[now.getDay()]}`
})
const levelName = computed(() => {
const level = authStore.user?.organizationalLevel
switch (level) {
case OrganizationalLevel.Division: return '师团级'
case OrganizationalLevel.Regiment: return '团级'
case OrganizationalLevel.Battalion: return '营级'
case OrganizationalLevel.Company: return '连级'
default: return ''
}
})
function getPieOption(regiment: RegimentAllocationStats) {
const consumed = regiment.totalConsumed
const remaining = regiment.totalQuota - consumed
// 0
const hasData = regiment.totalQuota > 0
return {
@ -113,20 +195,20 @@ function getPieOption(regiment: RegimentAllocationStats) {
series: [
{
type: 'pie',
radius: ['50%', '70%'],
radius: ['55%', '75%'],
avoidLabelOverlap: false,
label: {
show: true,
position: 'center',
formatter: `${regiment.percentage}%`,
fontSize: 18,
fontSize: 20,
fontWeight: 'bold',
color: '#303133'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontSize: 22,
fontWeight: 'bold'
}
},
@ -160,7 +242,6 @@ async function loadStats() {
async function loadRegimentStats() {
try {
const data = await statsApi.getRegimentAllocationsStats()
console.log('Regiment stats loaded:', data)
regimentStats.value = data
} catch (error) {
console.error('加载团级统计数据失败', error)
@ -175,39 +256,144 @@ onMounted(() => {
<style scoped>
.dashboard {
padding: 10px;
padding: 0;
}
/* 欢迎横幅 */
.welcome-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 24px 32px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.welcome-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.welcome-text h1 {
color: #fff;
font-size: 24px;
margin: 0 0 8px 0;
font-weight: 600;
}
.welcome-text p {
color: rgba(255, 255, 255, 0.85);
margin: 0;
font-size: 14px;
}
.welcome-icon {
opacity: 0.9;
}
/* 统计卡片 */
.stat-row {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.card-header {
position: relative;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.stat-card-blue {
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
}
.stat-card-green {
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
}
.stat-card-orange {
background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-right: 20px;
}
.stat-content {
flex: 1;
z-index: 1;
}
.stat-value {
font-size: 32px;
font-size: 36px;
font-weight: bold;
color: #303133;
color: #fff;
line-height: 1.2;
}
.stat-label {
color: #909399;
margin-top: 8px;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
margin-top: 4px;
}
.mt-20 {
margin-top: 20px;
.stat-decoration {
position: absolute;
right: -20px;
bottom: -20px;
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
/* 图表区域 */
.chart-row {
margin-bottom: 20px;
}
.chart-card {
border-radius: 12px;
}
.chart-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.chart-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.pie-charts-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 20px;
gap: 24px;
padding: 16px 0;
}
.pie-chart-item {
@ -216,17 +402,30 @@ onMounted(() => {
align-items: center;
flex: 1;
min-width: 180px;
max-width: 220px;
max-width: 240px;
padding: 16px;
border-radius: 12px;
background: #f8f9fa;
transition: all 0.3s;
}
.pie-chart-item:hover {
background: #f0f2f5;
transform: translateY(-2px);
}
.pie-chart-wrapper {
position: relative;
}
.pie-chart {
width: 180px;
height: 180px;
width: 160px;
height: 160px;
}
.regiment-name {
margin-top: 8px;
font-size: 16px;
margin-top: 12px;
font-size: 20px;
color: #303133;
font-weight: 600;
text-align: center;
@ -234,9 +433,104 @@ onMounted(() => {
.regiment-info {
display: flex;
gap: 16px;
align-items: center;
gap: 12px;
margin-top: 12px;
padding: 8px 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
}
.info-label {
font-size: 12px;
color: #909399;
}
.info-value {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.info-value.consumed {
color: #67C23A;
}
.info-divider {
width: 1px;
height: 24px;
background: #e4e7ed;
}
/* 快捷操作 */
.quick-actions-row {
margin-bottom: 20px;
}
.quick-card {
border-radius: 12px;
}
.quick-actions {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 32px;
border-radius: 12px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s;
min-width: 120px;
}
.action-item:hover {
background: #f0f2f5;
transform: translateY(-2px);
}
.action-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-bottom: 12px;
}
.action-icon-blue {
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
}
.action-icon-green {
background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
}
.action-icon-orange {
background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
}
.action-icon-purple {
background: linear-gradient(135deg, #9b59b6 0%, #b07cc6 100%);
}
.action-text {
font-size: 14px;
color: #606266;
margin-top: 6px;
font-weight: 500;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="login-container">
<div class="login-box">
<h1 class="login-title">兵团训练物资管控系统</h1>
<h1 class="login-title">军事训练管理系统</h1>
<el-form
ref="formRef"
:model="form"

View File

@ -21,11 +21,19 @@
</template>
</el-input>
<el-select v-model="categoryFilter" placeholder="类别筛选" clearable style="width: 120px; margin-right: 12px">
<el-option label="弹药" value="弹药" />
<el-option label="装备" value="装备" />
<el-option label="其他" value="其他" />
<el-option label="器材" value="器材" />
<el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.name" />
</el-select>
<el-input
v-if="authStore.organizationalLevelNum === 1"
v-model="unitFilter"
placeholder="搜索单位"
clearable
style="width: 140px; margin-right: 12px"
>
<template #prefix>
<el-icon><OfficeBuilding /></el-icon>
</template>
</el-input>
<el-button v-if="authStore.canCreateAllocations" @click="$router.push('/allocations/categories')">
<el-icon><Setting /></el-icon>
类别管理
@ -315,7 +323,7 @@
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold', fontSize: '14px' }"
:row-style="{ height: '60px' }"
>
<el-table-column prop="targetUnitName" label="目标单位" min-width="150">
<el-table-column prop="targetUnitName" label="单位" min-width="150">
<template #default="{ row }">
<div class="unit-cell">
<el-icon class="unit-icon" :size="16"><OfficeBuilding /></el-icon>
@ -323,7 +331,7 @@
</div>
</template>
</el-table-column>
<el-table-column prop="unitQuota" label="分配配额" width="120" align="center">
<el-table-column prop="unitQuota" label="配额" width="120" align="center">
<template #default="{ row }">
<div class="quota-cell-dialog">
<span class="quota-number-dialog">{{ formatNumber(row.unitQuota) }}</span>
@ -557,7 +565,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, type UnitReportSummary, type UnitConsumptionStat } from '@/api'
import { allocationsApi, materialCategoriesApi, type UnitReportSummary, type UnitConsumptionStat } from '@/api'
import type { MaterialAllocation, AllocationDistribution } from '@/types'
const router = useRouter()
@ -568,6 +576,7 @@ const distributions = ref<AllocationDistribution[]>([])
const consumptionReports = ref<any[]>([])
const unitSummaries = ref<UnitReportSummary[]>([])
const consumptionStats = ref<UnitConsumptionStat[]>([])
const categories = ref<{ id: number; name: string }[]>([])
const reportsTabActive = ref('summary')
const loading = ref(false)
const loadingReports = ref(false)
@ -576,6 +585,7 @@ const showDistributionDialog = ref(false)
const showReportsDialog = ref(false)
const searchKeyword = ref('')
const categoryFilter = ref('')
const unitFilter = ref('')
const selectedAllocation = ref<MaterialAllocation | null>(null)
const selectedDistribution = ref<AllocationDistribution | null>(null)
const selectedPeriod = ref('all')
@ -621,6 +631,12 @@ const filteredAllocations = computed(() => {
if (categoryFilter.value) {
result = result.filter(a => a.category === categoryFilter.value)
}
if (unitFilter.value) {
const keyword = unitFilter.value.toLowerCase()
result = result.filter(a =>
a.distributions?.some(d => d.targetUnitName?.toLowerCase().includes(keyword))
)
}
return result
})
@ -1110,7 +1126,16 @@ async function handleDelete(allocation: MaterialAllocation) {
onMounted(() => {
loadAllocations()
loadCategories()
})
async function loadCategories() {
try {
categories.value = await materialCategoriesApi.getAll()
} catch {
console.error('加载类别失败')
}
}
</script>
<style scoped>