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"> <el-sub-menu v-if="canViewAllocations" index="allocations">
<template #title> <template #title>
<el-icon><Box /></el-icon> <el-icon><Box /></el-icon>
<span>弹种配额</span> <span>训练消耗</span>
</template> </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 v-if="authStore.canCreateAllocations" index="/allocations/create">创建配额</el-menu-item>
<el-menu-item index="/allocations/change-requests">删改申请</el-menu-item> <el-menu-item index="/allocations/change-requests">删改申请</el-menu-item>
</el-sub-menu> </el-sub-menu>
@ -41,10 +41,11 @@
<el-menu-item index="/personnel/create">添加人才</el-menu-item> <el-menu-item index="/personnel/create">添加人才</el-menu-item>
</el-sub-menu> </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> <el-icon><Checked /></el-icon>
<span>审批管理</span> <span>审批管理</span>
</el-menu-item> </el-menu-item> -->
<el-menu-item v-if="authStore.hasPermission(1)" index="/audit-logs"> <el-menu-item v-if="authStore.hasPermission(1)" index="/audit-logs">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>

View File

@ -1,55 +1,70 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<el-row :gutter="20"> <!-- 欢迎横幅 -->
<!-- 师本部和团本部显示弹种配额 --> <div class="welcome-banner">
<el-col v-if="authStore.organizationalLevelNum <= 2" :span="6"> <div class="welcome-content">
<el-card class="stat-card"> <div class="welcome-text">
<template #header> <h1>欢迎回来{{ authStore.user?.username }}</h1>
<div class="card-header"> <p>{{ currentDate }} · {{ levelName }}</p>
<el-icon :size="24" color="#409EFF"><Box /></el-icon>
<span>弹种配额</span>
</div> </div>
</template> <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-value">{{ stats.allocations }}</div>
<div class="stat-label">总配额条数</div> <div class="stat-label">弹种配额</div>
</el-card>
</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> </div>
</template> <div class="stat-decoration"></div>
</div>
</el-col>
<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-value">{{ stats.completionRate }}%</div>
<div class="stat-label">平均完成率</div> <div class="stat-label">平均完成率</div>
</el-card>
</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> </div>
</template> <div class="stat-decoration"></div>
</div>
</el-col>
<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-value">{{ stats.personnel }}</div>
<div class="stat-label">人员总数</div> <div class="stat-label">人才总数</div>
</el-card> </div>
<div class="stat-decoration"></div>
</div>
</el-col> </el-col>
</el-row> </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-col :span="24">
<el-card> <el-card class="chart-card" shadow="hover">
<template #header> <template #header>
<div class="card-header"> <div class="chart-header">
<el-icon :size="24" color="#409EFF"><PieChart /></el-icon> <div class="chart-title">
<el-icon :size="22" color="#409EFF"><PieChart /></el-icon>
<span>各团物资消耗情况</span> <span>各团物资消耗情况</span>
</div> </div>
<el-tag type="info" effect="plain">实时数据</el-tag>
</div>
</template> </template>
<div v-if="regimentStats.length > 0" class="pie-charts-container"> <div v-if="regimentStats.length > 0" class="pie-charts-container">
<div <div
@ -57,11 +72,20 @@
:key="regiment.regimentId" :key="regiment.regimentId"
class="pie-chart-item" class="pie-chart-item"
> >
<div class="pie-chart-wrapper">
<v-chart :option="getPieOption(regiment)" autoresize class="pie-chart" /> <v-chart :option="getPieOption(regiment)" autoresize class="pie-chart" />
</div>
<div class="regiment-name">{{ regiment.regimentName }}</div> <div class="regiment-name">{{ regiment.regimentName }}</div>
<div class="regiment-info"> <div class="regiment-info">
<span>配额: {{ regiment.totalQuota }}</span> <div class="info-item">
<span>消耗: {{ regiment.totalConsumed }}</span> <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> </div>
</div> </div>
@ -69,15 +93,58 @@
</el-card> </el-card>
</el-col> </el-col>
</el-row> </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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { statsApi, type RegimentAllocationStats } from '@/api/stats' 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 { ElMessage } from 'element-plus'
import { OrganizationalLevel } from '@/types'
import VChart from 'vue-echarts' import VChart from 'vue-echarts'
import { use } from 'echarts/core' import { use } from 'echarts/core'
import { PieChart as EchartsPie } from 'echarts/charts' import { PieChart as EchartsPie } from 'echarts/charts'
@ -98,11 +165,26 @@ const stats = ref({
const regimentStats = ref<RegimentAllocationStats[]>([]) const regimentStats = ref<RegimentAllocationStats[]>([])
const loading = ref(false) 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) { function getPieOption(regiment: RegimentAllocationStats) {
const consumed = regiment.totalConsumed const consumed = regiment.totalConsumed
const remaining = regiment.totalQuota - consumed const remaining = regiment.totalQuota - consumed
// 0
const hasData = regiment.totalQuota > 0 const hasData = regiment.totalQuota > 0
return { return {
@ -113,20 +195,20 @@ function getPieOption(regiment: RegimentAllocationStats) {
series: [ series: [
{ {
type: 'pie', type: 'pie',
radius: ['50%', '70%'], radius: ['55%', '75%'],
avoidLabelOverlap: false, avoidLabelOverlap: false,
label: { label: {
show: true, show: true,
position: 'center', position: 'center',
formatter: `${regiment.percentage}%`, formatter: `${regiment.percentage}%`,
fontSize: 18, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
color: '#303133' color: '#303133'
}, },
emphasis: { emphasis: {
label: { label: {
show: true, show: true,
fontSize: 20, fontSize: 22,
fontWeight: 'bold' fontWeight: 'bold'
} }
}, },
@ -160,7 +242,6 @@ async function loadStats() {
async function loadRegimentStats() { async function loadRegimentStats() {
try { try {
const data = await statsApi.getRegimentAllocationsStats() const data = await statsApi.getRegimentAllocationsStats()
console.log('Regiment stats loaded:', data)
regimentStats.value = data regimentStats.value = data
} catch (error) { } catch (error) {
console.error('加载团级统计数据失败', error) console.error('加载团级统计数据失败', error)
@ -175,39 +256,144 @@ onMounted(() => {
<style scoped> <style scoped>
.dashboard { .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 { .stat-card {
text-align: center; position: relative;
} border-radius: 12px;
padding: 24px;
.card-header {
display: flex; display: flex;
align-items: center; 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 { .stat-value {
font-size: 32px; font-size: 36px;
font-weight: bold; font-weight: bold;
color: #303133; color: #fff;
line-height: 1.2;
} }
.stat-label { .stat-label {
color: #909399; color: rgba(255, 255, 255, 0.9);
margin-top: 8px; font-size: 14px;
margin-top: 4px;
} }
.mt-20 { .stat-decoration {
margin-top: 20px; 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 { .pie-charts-container {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 24px;
padding: 16px 0;
} }
.pie-chart-item { .pie-chart-item {
@ -216,17 +402,30 @@ onMounted(() => {
align-items: center; align-items: center;
flex: 1; flex: 1;
min-width: 180px; 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 { .pie-chart {
width: 180px; width: 160px;
height: 180px; height: 160px;
} }
.regiment-name { .regiment-name {
margin-top: 8px; margin-top: 12px;
font-size: 16px; font-size: 20px;
color: #303133; color: #303133;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
@ -234,9 +433,104 @@ onMounted(() => {
.regiment-info { .regiment-info {
display: flex; 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; font-size: 14px;
color: #606266; color: #606266;
margin-top: 6px; font-weight: 500;
} }
</style> </style>

View File

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

View File

@ -21,11 +21,19 @@
</template> </template>
</el-input> </el-input>
<el-select v-model="categoryFilter" placeholder="类别筛选" clearable style="width: 120px; margin-right: 12px"> <el-select v-model="categoryFilter" placeholder="类别筛选" clearable style="width: 120px; margin-right: 12px">
<el-option label="弹药" value="弹药" /> <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.name" />
<el-option label="装备" value="装备" />
<el-option label="其他" value="其他" />
<el-option label="器材" value="器材" />
</el-select> </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-button v-if="authStore.canCreateAllocations" @click="$router.push('/allocations/categories')">
<el-icon><Setting /></el-icon> <el-icon><Setting /></el-icon>
类别管理 类别管理
@ -315,7 +323,7 @@
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold', fontSize: '14px' }" :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold', fontSize: '14px' }"
:row-style="{ height: '60px' }" :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 }"> <template #default="{ row }">
<div class="unit-cell"> <div class="unit-cell">
<el-icon class="unit-icon" :size="16"><OfficeBuilding /></el-icon> <el-icon class="unit-icon" :size="16"><OfficeBuilding /></el-icon>
@ -323,7 +331,7 @@
</div> </div>
</template> </template>
</el-table-column> </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 }"> <template #default="{ row }">
<div class="quota-cell-dialog"> <div class="quota-cell-dialog">
<span class="quota-number-dialog">{{ formatNumber(row.unitQuota) }}</span> <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 { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, View, Edit, Delete, Box, Setting, OfficeBuilding, Clock } from '@element-plus/icons-vue' import { Plus, Search, View, Edit, Delete, Box, Setting, OfficeBuilding, Clock } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth' 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' import type { MaterialAllocation, AllocationDistribution } from '@/types'
const router = useRouter() const router = useRouter()
@ -568,6 +576,7 @@ const distributions = ref<AllocationDistribution[]>([])
const consumptionReports = ref<any[]>([]) const consumptionReports = ref<any[]>([])
const unitSummaries = ref<UnitReportSummary[]>([]) const unitSummaries = ref<UnitReportSummary[]>([])
const consumptionStats = ref<UnitConsumptionStat[]>([]) const consumptionStats = ref<UnitConsumptionStat[]>([])
const categories = ref<{ id: number; name: string }[]>([])
const reportsTabActive = ref('summary') const reportsTabActive = ref('summary')
const loading = ref(false) const loading = ref(false)
const loadingReports = ref(false) const loadingReports = ref(false)
@ -576,6 +585,7 @@ const showDistributionDialog = ref(false)
const showReportsDialog = ref(false) const showReportsDialog = ref(false)
const searchKeyword = ref('') const searchKeyword = ref('')
const categoryFilter = ref('') const categoryFilter = ref('')
const unitFilter = ref('')
const selectedAllocation = ref<MaterialAllocation | null>(null) const selectedAllocation = ref<MaterialAllocation | null>(null)
const selectedDistribution = ref<AllocationDistribution | null>(null) const selectedDistribution = ref<AllocationDistribution | null>(null)
const selectedPeriod = ref('all') const selectedPeriod = ref('all')
@ -621,6 +631,12 @@ const filteredAllocations = computed(() => {
if (categoryFilter.value) { if (categoryFilter.value) {
result = result.filter(a => a.category === 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 return result
}) })
@ -1110,7 +1126,16 @@ async function handleDelete(allocation: MaterialAllocation) {
onMounted(() => { onMounted(() => {
loadAllocations() loadAllocations()
loadCategories()
}) })
async function loadCategories() {
try {
categories.value = await materialCategoriesApi.getAll()
} catch {
console.error('加载类别失败')
}
}
</script> </script>
<style scoped> <style scoped>