b9e53d7500e3497f8659fc373e6c707071b6dfb7..8aff4f4bb7308c9875562a2b898a9a4a70c32329
2026-03-02 gaoluyang
金鹰黄金 1.登录日志中登陆地点改为登录方式
8aff4f 对比 | 目录
2026-03-02 gaoluyang
金鹰黄金 1.劳保管理添加发放进度报表页面
303808 对比 | 目录
已添加1个文件
已修改2个文件
476 ■■■■■ 文件已修改
src/api/lavorissce/ledger.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lavorissue/report/index.vue 447 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/logininfor/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/lavorissce/ledger.js
@@ -18,6 +18,33 @@
    })
}
// å‘放进度-总计
export function progressTotal(params) {
    return request({
        url: '/lavorIssue/progressTotal',
        method: 'get',
        params
    })
}
// é¢†å–进度占比
export function progressPercent(params) {
    return request({
        url: '/lavorIssue/progressPercent',
        method: 'get',
        params
    })
}
// è¿›åº¦åˆ†å¸ƒ
export function progressDistribution(params) {
    return request({
        url: '/lavorIssue/progressDistribution',
        method: 'get',
        params
    })
}
export function statisticsList(params) {
    return request({
        url: '/lavorIssue/statisticsList',
src/views/lavorissue/report/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,447 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">发放季度:</span>
        <el-select
          style="width: 200px"
          v-model="searchForm.season"
          placeholder="请选择"
          @change="handleQuery"
          @clear="clearSeason"
          clearable
          :disabled="!!searchForm.issueDate"
        >
          <el-option
            v-for="item in jidu"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">发放月份:</span>
        <el-date-picker
          style="width: 200px"
          v-model="searchForm.issueDate"
          type="month"
          value-format="YYYY-MM-DD"
          format="YYYY-MM"
          placeholder="请选择月份"
          clearable
          @change="handleQuery"
          @clear="clearIssueDaten"
          :disabled="!!searchForm.season"
        />
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">
          æœç´¢
        </el-button>
        <el-button style="margin-left: 10px" @click="resetHandleQuery">
          é‡ç½®
        </el-button>
      </div>
      <div>
        <el-button @click="handleOut" icon="download">导出</el-button>
      </div>
    </div>
    <!-- å‘放进度(图表模式) -->
    <el-row :gutter="20" class="progress-cards">
      <el-col :span="6">
        <el-card class="progress-card">
          <div class="pc-title">发放总数量</div>
          <div class="pc-value">{{ totalNum }}</div>
          <div class="pc-sub">已领 + æœªé¢†ï¼ˆå«è¶…时)</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="progress-card success">
          <div class="pc-title">已领取</div>
          <div class="pc-value">{{ adoptedNum }}</div>
          <div class="pc-sub">含超时已领取</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="progress-card warning">
          <div class="pc-title">未领取</div>
          <div class="pc-value">{{ unAdoptedNum }}</div>
          <div class="pc-sub">含超时未领取</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="progress-card info">
          <div class="pc-title">领取完成率</div>
          <div class="pc-value">{{ progressPercentVal }}%</div>
          <el-progress
            :percentage="progressPercentVal"
            :stroke-width="10"
            status="success"
          />
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="20" class="charts-section">
      <el-col :span="12">
        <el-card class="chart-card" v-loading="statsLoading">
          <template #header>
            <div class="card-header">领取进度占比</div>
          </template>
          <div ref="pieChartRef" class="chart"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card class="chart-card" v-loading="statsLoading">
          <template #header>
            <div class="card-header">进度分布(含超时)</div>
          </template>
          <div ref="barChartRef" class="chart"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, onUnmounted, computed, nextTick, getCurrentInstance } from 'vue'
import dayjs from 'dayjs'
import * as echarts from 'echarts'
import { progressTotal, progressPercent, progressDistribution } from '@/api/lavorissce/ledger'
import { ElMessageBox, ElMessage } from 'element-plus'
import { getCurrentMonth } from '@/utils/util'
const { proxy } = getCurrentInstance()
// æŸ¥è¯¢æ¡ä»¶
const data = reactive({
  searchForm: {
    season: getCurrentMonth(),
    issueDate: '',
  },
})
const { searchForm } = toRefs(data)
// å­£åº¦é€‰é¡¹
const jidu = ref([
  { value: '1', label: '第一季度' },
  { value: '2', label: '第二季度' },
  { value: '3', label: '第三季度' },
  { value: '4', label: '第四季度' },
])
// è¿›åº¦ç»Ÿè®¡ï¼ˆå›¾è¡¨æ•°æ®ï¼‰
const statsLoading = ref(false)
// å‘放进度-总计(来自 progressTotal æŽ¥å£ï¼‰
const totalData = ref({
  totalNum: 0,
  adoptedNum: 0,
  unAdoptedNum: 0,
  progressPercent: 0,
})
// é¢†å–进度占比(来自 progressPercent æŽ¥å£ï¼Œé¥¼å›¾ï¼‰
const percentData = ref([])
// è¿›åº¦åˆ†å¸ƒï¼ˆæ¥è‡ª progressDistribution æŽ¥å£ï¼ŒæŸ±çŠ¶å›¾ï¼‰
const distributionData = ref({
  ylqNum: 0,
  wlqNum: 0,
  csylqNum: 0,
  cswlqNum: 0,
})
const totalNum = computed(() => Number(totalData.value.totalNum || 0))
const adoptedNum = computed(() => Number(totalData.value.adoptedNum || 0))
const unAdoptedNum = computed(() => Number(totalData.value.unAdoptedNum || 0))
const progressPercentVal = computed(() => Number(totalData.value.progressPercent ?? 0))
const pieChartRef = ref(null)
const barChartRef = ref(null)
let pieChart = null
let barChart = null
const resizeCharts = () => {
  pieChart?.resize()
  barChart?.resize()
}
const initChartsIfNeeded = async () => {
  await nextTick()
  if (pieChartRef.value && !pieChart) pieChart = echarts.init(pieChartRef.value)
  if (barChartRef.value && !barChart) barChart = echarts.init(barChartRef.value)
  renderCharts()
  window.addEventListener('resize', resizeCharts)
}
const renderCharts = () => {
  const s = distributionData.value
  const timelyAdopted = Number(s.ylqNum || 0)
  const timelyUnAdopted = Number(s.wlqNum || 0)
  const overtimeAdopted = Number(s.csylqNum || 0)
  const overtimeUnAdopted = Number(s.cswlqNum || 0)
  const colors = ['#67C23A', '#E6A23C']
  const pieData = percentData.value.length
    ? percentData.value.map((it, i) => ({
        ...it,
        itemStyle: it.itemStyle || { color: colors[i % colors.length] },
      }))
    : [
        { name: '已领取', value: adoptedNum.value, itemStyle: { color: '#67C23A' } },
        { name: '未领取', value: unAdoptedNum.value, itemStyle: { color: '#E6A23C' } },
      ]
  if (pieChart) {
    pieChart.setOption(
      {
        tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
        legend: { orient: 'vertical', left: 'left', top: 'middle' },
        series: [
          {
            name: '领取进度',
            type: 'pie',
            radius: ['45%', '72%'],
            center: ['60%', '50%'],
            label: { formatter: '{b}\n{c}' },
            data: pieData,
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.3)',
              },
            },
          },
        ],
      },
      true,
    )
  }
  if (barChart) {
    barChart.setOption(
      {
        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
        grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
        xAxis: { type: 'category', data: ['及时', '超时'] },
        yAxis: { type: 'value' },
        legend: { data: ['已领取', '未领取'] },
        series: [
          {
            name: '已领取',
            type: 'bar',
            stack: 'total',
            barWidth: 42,
            itemStyle: { color: '#67C23A' },
            data: [timelyAdopted, overtimeAdopted],
          },
          {
            name: '未领取',
            type: 'bar',
            stack: 'total',
            itemStyle: { color: '#F56C6C' },
            data: [timelyUnAdopted, overtimeUnAdopted],
          },
        ],
      },
      true,
    )
  }
}
const clearSeason = () => {
  searchForm.value.season = ''
  searchForm.value.issueDate = dayjs().format('YYYY-MM-DD')
}
const clearIssueDaten = () => {
  searchForm.value.issueDate = ''
  searchForm.value.season = getCurrentMonth()
}
const resetHandleQuery = () => {
  searchForm.value.issueDate = ''
  searchForm.value.season = getCurrentMonth()
  handleQuery()
}
// æŸ¥è¯¢
const handleQuery = async () => {
  await getStatistics()
}
const getStatistics = async () => {
  statsLoading.value = true
  const params = { ...searchForm.value }
  try {
    const [totalRes, percentRes, distRes] = await Promise.all([
      progressTotal(params),
      progressPercent(params),
      progressDistribution(params),
    ])
    const d = totalRes?.data || {}
    totalData.value = {
      totalNum: d.total ?? 0,
      adoptedNum: d.adopted ?? 0,
      unAdoptedNum: d.notAdopted ?? 0,
      progressPercent: d.adoptedPercent ?? 0,
    }
    const p = percentRes?.data || {}
    if (Array.isArray(p)) {
      percentData.value = p
    } else if (p.data && Array.isArray(p.data)) {
      percentData.value = p.data
    } else if (p.adopted != null || p.notAdopted != null) {
      percentData.value = [
        { name: '已领取', value: Number(p.adopted || 0), itemStyle: { color: '#67C23A' } },
        { name: '未领取', value: Number(p.notAdopted || 0), itemStyle: { color: '#E6A23C' } },
      ]
    } else {
      percentData.value = []
    }
    const dist = distRes?.data || {}
    // åŽç«¯è¿”回示例:{ series: [[2,14],[0,0]] }
    // çº¦å®šï¼šseries[0] = [及时已领, åŠæ—¶æœªé¢†],series[1] = [超时已领, è¶…时未领]
    if (
      Array.isArray(dist.series) &&
      dist.series.length >= 2 &&
      Array.isArray(dist.series[0]) &&
      Array.isArray(dist.series[1]) &&
      dist.series[0].length >= 2 &&
      dist.series[1].length >= 2
    ) {
      distributionData.value = {
        ylqNum: Number(dist.series[0][0] ?? 0),
        wlqNum: Number(dist.series[0][1] ?? 0),
        csylqNum: Number(dist.series[1][0] ?? 0),
        cswlqNum: Number(dist.series[1][1] ?? 0),
      }
    } else {
      // å…¼å®¹å…¶å®ƒè¿”回字段命名
      distributionData.value = {
        ylqNum: Number(dist.ylqNum ?? dist.timelyAdopted ?? 0),
        wlqNum: Number(dist.wlqNum ?? dist.timelyUnAdopted ?? 0),
        csylqNum: Number(dist.csylqNum ?? dist.overtimeAdopted ?? 0),
        cswlqNum: Number(dist.cswlqNum ?? dist.overtimeUnAdopted ?? 0),
      }
    }
    await initChartsIfNeeded()
  } catch (e) {
    totalData.value = { totalNum: 0, adoptedNum: 0, unAdoptedNum: 0, progressPercent: 0 }
    percentData.value = []
    distributionData.value = { ylqNum: 0, wlqNum: 0, csylqNum: 0, cswlqNum: 0 }
    renderCharts()
  } finally {
    statsLoading.value = false
  }
}
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm('选中的内容将被导出,是否确认导出?', '导出', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      proxy.download(
        '/lavorIssue/exportCopy',
        {
          season: searchForm.value.season,
          issueDate: searchForm.value.issueDate,
        },
        '劳保发放报表.xlsx',
      )
    })
    .catch(() => {
      ElMessage.info('已取消')
    })
}
onMounted(() => {
  handleQuery()
})
onUnmounted(() => {
  window.removeEventListener('resize', resizeCharts)
  if (pieChart) {
    pieChart.dispose()
    pieChart = null
  }
  if (barChart) {
    barChart.dispose()
    barChart = null
  }
})
</script>
<style scoped>
.progress-cards {
  margin: 14px 0 18px;
}
.progress-card :deep(.el-card__body) {
  padding: 16px;
}
.pc-title {
  color: #606266;
  font-size: 13px;
  margin-bottom: 8px;
}
.pc-value {
  font-size: 28px;
  font-weight: 700;
  color: #303133;
  line-height: 1.2;
  margin-bottom: 6px;
}
.pc-sub {
  color: #909399;
  font-size: 12px;
}
.charts-section {
  margin-bottom: 12px;
}
.chart-card :deep(.el-card__body) {
  padding: 12px 16px 16px;
}
.card-header {
  font-weight: 600;
  color: #303133;
}
.chart {
  height: 320px;
  width: 100%;
}
.dynamic-table-container {
  width: 100%;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
::deep(.el-table .el-table__header-wrapper th) {
  background-color: #f0f1f5 !important;
  color: #333333;
  font-weight: 600;
}
::deep(.el-table .el-table__body-wrapper td) {
  padding: 8px 0;
}
::deep(.el-select) {
  width: 100%;
}
::deep(.el-input) {
  width: 100%;
}
</style>
src/views/monitor/logininfor/index.vue
@@ -98,7 +98,7 @@
         <el-table-column label="访问编号" align="center" prop="infoId" />
         <el-table-column label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
         <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
         <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
         <el-table-column label="登录方式" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
         <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
         <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
         <el-table-column label="登录状态" align="center" prop="status">