| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- <script lang="ts" setup>
- import { ref, onMounted } from 'vue'
- import dayjs from 'dayjs'
- import CountTo from '@/components/count-to1.vue'
- import { IotReportApi } from '@/api/pms/report'
- import { useDebounceFn } from '@vueuse/core'
- import download from '@/utils/download'
- import { rangeShortcuts } from '@/utils/formatTime'
- // 定义时间类型
- type TimeType = 'year' | 'month' | 'day'
- interface Query {
- deptId?: number
- createTime?: [string, string]
- pageNo: number
- pageSize: number
- type?: string
- }
- // 选项配置数组
- const timeOptions: { label: string; value: TimeType }[] = [
- { label: '年', value: 'year' },
- { label: '月', value: 'month' },
- { label: '日', value: 'day' }
- ]
- const activeTimeType = ref<TimeType | undefined>('year')
- const query = ref<Query>({
- pageNo: 1,
- pageSize: 10
- })
- const handleTimeChange = (type: TimeType, init = false) => {
- activeTimeType.value = type
- const formatStr = 'YYYY-MM-DD HH:mm:ss'
- const endTime = dayjs().endOf('day').format(formatStr)
- let startTime = ''
- switch (type) {
- case 'year':
- startTime = dayjs().startOf('year').format(formatStr)
- break
- case 'month':
- startTime = dayjs().startOf('month').format(formatStr)
- break
- case 'day':
- startTime = dayjs().startOf('day').format(formatStr)
- break
- }
- query.value.createTime = [startTime, endTime]
- console.log(`切换为[${type}]:`, query.value.createTime)
- if (!init) loadData()
- }
- interface StatItem {
- key: string
- title: string
- icon: string
- type: string
- class: { bg1: string; bg2: string; text: string }
- value: number
- tb: number
- hb: number
- }
- const defaultStats: StatItem[] = [
- {
- key: 'repair',
- title: '内部维修成本',
- type: '内部维修',
- icon: 'i-material-symbols:home-repair-service-outline',
- class: {
- bg1: 'bg-[var(--el-color-success-light-7)]',
- bg2: 'bg-[var(--el-color-success-light-5)]',
- text: 'text-[var(--el-color-success)]'
- },
- value: 0,
- tb: 0,
- hb: 0
- },
- {
- key: 'byFee',
- title: '保养成本',
- type: '保养',
- icon: 'i-material-symbols:construction-rounded',
- class: {
- bg1: 'bg-[var(--el-color-primary-light-7)]',
- bg2: 'bg-[var(--el-color-primary-light-5)]',
- text: 'text-[var(--el-color-primary)]'
- },
- value: 0,
- tb: 0,
- hb: 0
- },
- {
- key: 'out',
- title: '委外维修',
- type: '委外维修',
- icon: 'i-material-symbols:work-outline',
- class: {
- bg1: 'bg-[var(--el-color-warning-light-7)]',
- bg2: 'bg-[var(--el-color-warning-light-5)]',
- text: 'text-[var(--el-color-warning)]'
- },
- value: 0,
- tb: 0,
- hb: 0
- }
- ]
- const statList = ref<StatItem[]>(defaultStats)
- const dataLoading = ref(false)
- const loadData = useDebounceFn(async function () {
- dataLoading.value = true
- try {
- const { pageNo, pageSize, type, ...other } = query.value
- const res = await IotReportApi.getCostsFee({ ...other, timeType: activeTimeType.value })
- const repair = statList.value[0]!
- const out = statList.value[2]!
- const byFee = statList.value[1]!
- out.value = res[out.key].total || 0
- out.tb = Number(((res[out.key].tb || 0) * 100).toFixed(0))
- out.hb = Number(((res[out.key].hb || 0) * 100).toFixed(0))
- repair.value = res[repair.key].total || 0
- repair.tb = Number(((res[repair.key].tb || 0) * 100).toFixed(0))
- repair.hb = Number(((res[repair.key].hb || 0) * 100).toFixed(0))
- byFee.value = res[byFee.key].total || 0
- byFee.tb = Number(((res[byFee.key].tb || 0) * 100).toFixed(0))
- byFee.hb = Number(((res[byFee.key].hb || 0) * 100).toFixed(0))
- } catch (error) {
- console.log('error :>> ', error)
- } finally {
- dataLoading.value = false
- }
- }, 500)
- const list = ref<any[]>([])
- const loading = ref(false)
- const total = ref(0)
- function handleSizeChange(val: number) {
- query.value.pageSize = val
- query.value.pageNo = 1
- loadList()
- }
- function handleCurrentChange(val: number) {
- query.value.pageNo = val
- loadList()
- }
- function handleQuery(setPage = true) {
- if (setPage) {
- query.value.pageNo = 1
- }
- loadList()
- loadData()
- }
- const loadList = useDebounceFn(async function () {
- loading.value = true
- try {
- const data = await IotReportApi.getCostsPage(query.value)
- list.value = data.list
- total.value = data.total
- // nextTick(() => {
- // calculateColumnWidths(columns.value)
- // })
- } finally {
- loading.value = false
- }
- }, 500)
- onMounted(() => {
- handleTimeChange('year', true)
- })
- watch(
- [() => query.value.createTime, () => query.value.deptId],
- () => {
- handleQuery()
- },
- { immediate: true }
- )
- function selectType(type: string | undefined) {
- query.value.type = type
- query.value.pageNo = 1
- loadList()
- }
- function handleReset() {
- handleTimeChange('year')
- selectType(undefined)
- }
- const exportLoading = ref(false)
- const handleExport = async () => {
- exportLoading.value = true
- try {
- const data = await IotReportApi.exportCosts(query.value)
- download.excel(data, '运维成本.xls')
- } finally {
- exportLoading.value = false
- }
- }
- const handleClear = () => {
- handleTimeChange('year')
- }
- const handleChange = () => {
- activeTimeType.value = undefined
- }
- </script>
- <template>
- <div
- class="grid grid-cols-[auto_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
- >
- <DeptTreeSelect
- :top-id="156"
- :deptId="156"
- v-model="query.deptId"
- :init-select="false"
- :show-title="false"
- class="row-span-2"
- />
- <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
- <div class="grid grid-rows-[1fr_32px] gap-4">
- <div class="grid grid-cols-3 gap-4" v-loading="dataLoading">
- <!-- 使用 v-for 循环渲染 -->
- <section
- v-for="item in statList"
- :key="item.key"
- class="flex flex-col items-center gap-y-4 rounded-lg shadow p-4 transition-transform hover:scale-105 duration-500"
- :class="{ [item.class.bg1]: true, 'scale-105': item.type === query.type }"
- @click="selectType(item.type)"
- >
- <!-- 头部:图标 + 标题 -->
- <div class="flex items-center gap-x-3">
- <div class="rounded-2 p-2" :class="[item.class.text, item.class.bg2]">
- <div :class="item.icon"></div>
- </div>
- <div class="text-[var(--el-text-color-primary)] text-sm font-medium">
- {{ item.title }}
- </div>
- </div>
- <!-- 数值区域:CountTo -->
- <count-to
- class="text-3xl font-semibold"
- :start-val="0"
- :end-val="item.value"
- :decimals="2"
- suffix="元"
- :duration="1000"
- >
- <!-- 插槽内容:当数据为空或0时的显示 (根据 count-to 组件的具体实现决定是否显示) -->
- <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
- </count-to>
- <!-- 底部:环比数据 -->
- <!-- 根据 trend 正负动态改变图标和颜色 -->
- <div class="mt-2 flex items-center gap-x-4">
- <div
- v-show="item.hb"
- class="flex items-center gap-x-1 text-xs font-medium"
- :class="item.class.text"
- >
- <!-- 动态图标:大于等于0向上,小于0向下 -->
- <div
- :class="
- item.hb >= 0
- ? 'i-material-symbols:arrow-warm-up-rounded'
- : 'i-material-symbols:arrow-cool-down-rounded'
- "
- ></div>
- <span class="vertical-middle"> {{ item.hb > 0 ? '+' + item.hb : item.hb }}% </span>
- <span>环比</span>
- </div>
- <div
- v-show="item.tb"
- class="flex items-center gap-x-1 text-xs font-medium"
- :class="item.class.text"
- >
- <!-- 动态图标:大于等于0向上,小于0向下 -->
- <div
- :class="
- item.tb >= 0
- ? 'i-material-symbols:arrow-warm-up-rounded'
- : 'i-material-symbols:arrow-cool-down-rounded'
- "
- ></div>
- <span class="vertical-middle"> {{ item.tb > 0 ? '+' + item.tb : item.tb }}% </span>
- <span>同比</span>
- </div>
- </div>
- </section>
- </div>
- <div class="flex justify-between gap-4">
- <div class="flex items-center gap-4">
- <el-date-picker
- size="default"
- v-model="query.createTime"
- value-format="YYYY-MM-DD HH:mm:ss"
- type="daterange"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :shortcuts="rangeShortcuts"
- class="!w-220px"
- @clear="handleClear"
- @change="handleChange"
- :clearable="false"
- :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
- />
- <el-button-group size="default">
- <el-button
- v-for="item in timeOptions"
- :key="item.value"
- :type="activeTimeType === item.value ? 'primary' : ''"
- @click="handleTimeChange(item.value)"
- >
- {{ item.label }}
- </el-button>
- </el-button-group>
- </div>
- <div class="flex items-center gap-2">
- <el-button size="default" @click="handleReset">重置</el-button>
- <el-button
- size="default"
- plain
- type="success"
- @click="handleExport"
- :loading="exportLoading"
- >
- <Icon icon="ep:download" class="mr-5px" /> 导出
- </el-button>
- </div>
- </div>
- </div>
- <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col mt-4">
- <div class="flex-1 relative">
- <el-auto-resizer class="absolute">
- <template #default="{ width, height }">
- <el-table
- :data="list"
- v-loading="loading"
- stripe
- class="absolute"
- :max-height="height"
- :height="height"
- show-overflow-tooltip
- :width="width"
- scrollbar-always-on
- >
- <el-table-column label="序号" type="index" width="50" align="center" />
- <el-table-column label="日期" prop="date" align="center" />
- <el-table-column label="类别" prop="type" align="center" />
- <el-table-column label="设备编号" prop="deviceCode" align="center" />
- <el-table-column label="设备名称" prop="deviceName" align="center" />
- <el-table-column
- label="成本"
- prop="cost"
- align="center"
- :formatter="(row) => (row.cost ?? 0) + '元'"
- />
- </el-table>
- </template>
- </el-auto-resizer>
- </div>
- <div class="h-10 mt-4 flex items-center justify-end">
- <el-pagination
- size="default"
- v-show="total > 0"
- v-model:current-page="query.pageNo"
- v-model:page-size="query.pageSize"
- :background="true"
- :page-sizes="[10, 20, 30, 50, 100]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- :deep(.el-table) {
- border-top-right-radius: 8px;
- border-top-left-radius: 8px;
- .el-table__cell {
- height: 52px;
- }
- .el-table__header-wrapper {
- .el-table__cell {
- background: var(--el-fill-color-light);
- }
- }
- }
- </style>
|