| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { TableColumnsType } from 'ant-design-vue'; |
| | | import type { MesSafetyStatisticsApi } from '#/api/safety/statistics'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | Col, |
| | | DatePicker, |
| | | message, |
| | | Row, |
| | | Select, |
| | | Spin, |
| | | Table, |
| | | Tabs, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { |
| | | exportHazardDetail, |
| | | getStatisticsReport, |
| | | } from '#/api/safety/statistics'; |
| | | import { getWorkshopOptions } from '#/views/safety/risk/shared'; |
| | | |
| | | defineOptions({ name: 'SafetyStatistics' }); |
| | | |
| | | const { RangePicker } = DatePicker; |
| | | |
| | | const loading = ref(false); |
| | | const exporting = ref(false); |
| | | const report = ref<MesSafetyStatisticsApi.StatisticsReport>(); |
| | | const dateRange = ref<[string, string]>(); |
| | | const workshopId = ref<number>(); |
| | | const workshopOptions = ref<Array<{ id: number; name: string }>>([]); |
| | | const activeTab = ref('hazard'); |
| | | |
| | | const summary = computed(() => report.value?.summary); |
| | | |
| | | /** æ¥è¯¢åæ° */ |
| | | function buildParams(): MesSafetyStatisticsApi.StatisticsParam { |
| | | return { |
| | | startDate: dateRange.value?.[0], |
| | | endDate: dateRange.value?.[1], |
| | | workshopId: workshopId.value, |
| | | }; |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | report.value = await getStatisticsReport(buildParams()); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadWorkshops() { |
| | | const list = await getWorkshopOptions(); |
| | | workshopOptions.value = list; |
| | | } |
| | | |
| | | /** å¿«æ·åºé´ */ |
| | | function setQuickRange(type: 'month' | 'quarter' | 'year') { |
| | | const end = dayjs().format('YYYY-MM-DD'); |
| | | let start = ''; |
| | | if (type === 'month') { |
| | | start = dayjs().startOf('month').format('YYYY-MM-DD'); |
| | | } else if (type === 'quarter') { |
| | | start = dayjs().subtract(2, 'month').startOf('month').format('YYYY-MM-DD'); |
| | | } else { |
| | | start = dayjs().startOf('year').format('YYYY-MM-DD'); |
| | | } |
| | | dateRange.value = [start, end]; |
| | | loadData(); |
| | | } |
| | | |
| | | function resetFilter() { |
| | | dateRange.value = undefined; |
| | | workshopId.value = undefined; |
| | | loadData(); |
| | | } |
| | | |
| | | async function handleExport() { |
| | | exporting.value = true; |
| | | try { |
| | | const data = await exportHazardDetail(buildParams()); |
| | | const blob = new Blob([data as any], { |
| | | type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', |
| | | }); |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement('a'); |
| | | link.href = url; |
| | | link.download = `鿣æç»_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`; |
| | | link.click(); |
| | | window.URL.revokeObjectURL(url); |
| | | message.success('å¯¼åºæå'); |
| | | } catch { |
| | | message.error('导åºå¤±è´¥'); |
| | | } finally { |
| | | exporting.value = false; |
| | | } |
| | | } |
| | | |
| | | /** åç»ç»è®¡è¡¨æ ¼å */ |
| | | const groupColumns: TableColumnsType = [ |
| | | { title: 'åç»', dataIndex: 'name', width: 200 }, |
| | | { title: 'æ°é', dataIndex: 'total', width: 120, align: 'right' }, |
| | | { title: '已宿', dataIndex: 'finished', width: 120, align: 'right' }, |
| | | { title: '宿ç', dataIndex: 'finishRate', width: 140, align: 'right' }, |
| | | ]; |
| | | |
| | | const trainingColumns: TableColumnsType = [ |
| | | { title: 'å¹è®ç±»å', dataIndex: 'name', width: 200 }, |
| | | { title: 'åºæ¬¡', dataIndex: 'total', width: 120, align: 'right' }, |
| | | { title: '已宿', dataIndex: 'finished', width: 120, align: 'right' }, |
| | | ]; |
| | | |
| | | onMounted(async () => { |
| | | await loadWorkshops(); |
| | | await loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page> |
| | | <Spin :spinning="loading"> |
| | | <div class="flex flex-col gap-4 p-4"> |
| | | <!-- çéåº --> |
| | | <Card :bordered="false" class="shadow-sm"> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="text-gray-600">ç»è®¡å¨æ</span> |
| | | <RangePicker |
| | | v-model:value="dateRange" |
| | | value-format="YYYY-MM-DD" |
| | | :placeholder="['å¼å§æ¥æ', 'ç»ææ¥æ']" |
| | | /> |
| | | <span class="ml-2 text-gray-600">车é´</span> |
| | | <Select |
| | | v-model:value="workshopId" |
| | | class="w-45" |
| | | allow-clear |
| | | placeholder="å
¨é¨è½¦é´" |
| | | :options="workshopOptions.map((w) => ({ label: w.name, value: w.id }))" |
| | | /> |
| | | <Button type="primary" @click="loadData">æ¥è¯¢</Button> |
| | | <Button @click="resetFilter">éç½®</Button> |
| | | <div class="ml-auto flex gap-2"> |
| | | <Button size="small" @click="setQuickRange('month')">æ¬æ</Button> |
| | | <Button size="small" @click="setQuickRange('quarter')">è¿ä¸æ</Button> |
| | | <Button size="small" @click="setQuickRange('year')">æ¬å¹´</Button> |
| | | </div> |
| | | </div> |
| | | </Card> |
| | | |
| | | <!-- æ±æ»ææ --> |
| | | <Row :gutter="[16, 16]"> |
| | | <Col :xs="24" :sm="12" :lg="6"> |
| | | <Card :bordered="false" class="shadow-sm"> |
| | | <div class="text-gray-500">éæ£æ»æ°</div> |
| | | <div class="mt-1 text-2xl font-bold text-blue-600"> |
| | | {{ summary?.hazardTotal ?? 0 }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | éç¯ {{ summary?.hazardClosed ?? 0 }} 项ï¼éç¯ç |
| | | {{ summary?.hazardClosedRate ?? 0 }}% |
| | | </div> |
| | | <div |
| | | v-if="(summary?.hazardOverdue ?? 0) > 0" |
| | | class="mt-1 text-xs font-semibold text-red-500" |
| | | > |
| | | è¶
æ {{ summary?.hazardOverdue }} 项 |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :xs="24" :sm="12" :lg="6"> |
| | | <Card :bordered="false" class="shadow-sm"> |
| | | <div class="text-gray-500">ä½ä¸ç¥¨æ»æ°</div> |
| | | <div class="mt-1 text-2xl font-bold text-green-600"> |
| | | {{ summary?.ticketTotal ?? 0 }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | 已彿¡£ {{ summary?.ticketArchived ?? 0 }} å¼ |
| | | </div> |
| | | <div |
| | | v-if="(summary?.ticketRejected ?? 0) > 0" |
| | | class="mt-1 text-xs text-orange-500" |
| | | > |
| | | 驳å {{ summary?.ticketRejected }} å¼ |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :xs="24" :sm="12" :lg="6"> |
| | | <Card :bordered="false" class="shadow-sm"> |
| | | <div class="text-gray-500">å¹è®åºæ¬¡</div> |
| | | <div class="mt-1 text-2xl font-bold text-purple-600"> |
| | | {{ summary?.trainingTotal ?? 0 }} |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-500"> |
| | | åè® {{ summary?.trainingAttendee ?? 0 }} 人次ï¼å¹³åå |
| | | {{ summary?.trainingAvgScore ?? '-' }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :xs="24" :sm="12" :lg="6"> |
| | | <Card :bordered="false" class="shadow-sm"> |
| | | <div class="text-gray-500">è¯ä¹¦æ»æ°</div> |
| | | <div class="mt-1 text-2xl font-bold text-orange-600"> |
| | | {{ summary?.certificateTotal ?? 0 }} |
| | | </div> |
| | | <div |
| | | v-if="(summary?.certificateExpired ?? 0) > 0" |
| | | class="mt-1 text-xs font-semibold text-red-500" |
| | | > |
| | | å·²è¿æ {{ summary?.certificateExpired }} æ¬ |
| | | </div> |
| | | <div v-else class="mt-1 text-xs text-gray-500">æ è¿æè¯ä¹¦</div> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <!-- åç»ç»è®¡ --> |
| | | <Card :bordered="false" class="shadow-sm"> |
| | | <template #title> |
| | | <div class="flex items-center gap-3"> |
| | | <span>åç»ç»è®¡</span> |
| | | <Button |
| | | type="primary" |
| | | size="small" |
| | | :loading="exporting" |
| | | @click="handleExport" |
| | | > |
| | | 导åºéæ£æç» |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | <Tabs v-model:active-key="activeTab"> |
| | | <Tabs.TabPane key="hazard" tab="鿣ç»è®¡"> |
| | | <div class="grid grid-cols-1 gap-4 lg:grid-cols-3"> |
| | | <div> |
| | | <div class="mb-2 font-medium">æè½¦é´</div> |
| | | <Table |
| | | :data-source="report?.hazardByWorkshop ?? []" |
| | | :columns="groupColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'finishRate'"> |
| | | <Tag :color="(record.finishRate ?? 0) >= 80 ? 'success' : 'warning'"> |
| | | {{ record.finishRate ?? 0 }}% |
| | | </Tag> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | <div> |
| | | <div class="mb-2 font-medium">æç级</div> |
| | | <Table |
| | | :data-source="report?.hazardByLevel ?? []" |
| | | :columns="groupColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-2 font-medium">ææ¥æº</div> |
| | | <Table |
| | | :data-source="report?.hazardBySource ?? []" |
| | | :columns="groupColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </Tabs.TabPane> |
| | | |
| | | <Tabs.TabPane key="ticket" tab="ä½ä¸ç¥¨ç»è®¡"> |
| | | <div class="grid grid-cols-1 gap-4 lg:grid-cols-2"> |
| | | <div> |
| | | <div class="mb-2 font-medium">æä½ä¸ç±»å</div> |
| | | <Table |
| | | :data-source="report?.ticketByType ?? []" |
| | | :columns="groupColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-2 font-medium">æç¶æ</div> |
| | | <Table |
| | | :data-source="report?.ticketByStatus ?? []" |
| | | :columns="groupColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </Tabs.TabPane> |
| | | |
| | | <Tabs.TabPane key="training" tab="å¹è®ç»è®¡"> |
| | | <div class="grid grid-cols-1 gap-4 lg:grid-cols-2"> |
| | | <div> |
| | | <div class="mb-2 font-medium">æå¹è®ç±»å</div> |
| | | <Table |
| | | :data-source="report?.trainingByType ?? []" |
| | | :columns="trainingColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-2 font-medium">æè¯ä¹¦ç±»å</div> |
| | | <Table |
| | | :data-source="report?.certificateByType ?? []" |
| | | :columns="groupColumns" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="name" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | | </div> |
| | | </Spin> |
| | | </Page> |
| | | </template> |