yanghao 3 viikkoa sitten
vanhempi
commit
674aeb07ad

+ 16 - 0
src/api/pms/iotprojecttask/index.ts

@@ -21,6 +21,16 @@ export interface GenerateReportParams {
   reportDate: number
 }
 
+export interface TaskWellNameVO {
+  id: number
+  wellName: string
+}
+
+export interface TaskWellNameParams {
+  deptId: number
+  wellName: string
+}
+
 // 项目信息任务拆分 API
 export const IotProjectTaskApi = {
   // 查询项目信息任务拆分分页
@@ -30,6 +40,12 @@ export const IotProjectTaskApi = {
   getIotProjectTaskList: async (params: any) => {
     return await request.get({ url: `/rq/iot-project-task/list`, params })
   },
+  getTaskWellNames: async (params: TaskWellNameParams) => {
+    return await request.get<TaskWellNameVO[]>({
+      url: `/rq/iot-project-task/taskWellNames`,
+      params
+    })
+  },
   // 查询项目信息任务拆分详情
   getIotProjectTask: async (id: number) => {
     return await request.get({ url: `/rq/iot-project-task/get?id=` + id })

+ 8 - 0
src/api/pms/iotrddailyreport/index.ts

@@ -50,6 +50,14 @@ export const IotRdDailyReportApi = {
   nptStatistics: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/nptStatistics`, params })
   },
+
+  exportNptStatistics: async (params: any) => {
+    return await request.download({
+      url: `/pms/iot-rd-daily-report/exportNptStatistics`,
+      params
+    })
+  },
+
   // 查询瑞都日报分页
   getIotRdDailyReportPage: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/page`, params })

+ 3 - 0
src/api/pms/stat/index.ts

@@ -198,6 +198,9 @@ export const IotStatApi = {
   getRyNptCount: async (params: any) => {
     return await request.get({ url: `/rq/stat/ry/device/nptCount`, params })
   },
+  getRhNptCount: async (params: any) => {
+    return await request.get({ url: `/rq/stat/rh/device/nptCount`, params })
+  },
   getRdNptCount: async (params: any) => {
     return await request.get({ url: `/rq/stat/rd/device/productionAbnormality`, params })
   },

+ 3 - 3
src/router/modules/remaining.ts

@@ -516,7 +516,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         component: () => import('@/views/pms/device/IotDeviceForm.vue'),
         name: 'DeviceDetailForm',
         meta: {
-          noCache: true,
+          noCache: false,
           hidden: true,
           canTo: true,
           icon: 'ep:document',
@@ -1103,10 +1103,10 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintain/edit/:id(\\d+)',
-        component: () => import('@/views/pms/maintain/IotMaintainAddEdit.vue'),
+        component: () => import('@/views/pms/maintain/IotMaintainEditPage.vue'),
         name: 'MaintainEdit',
         meta: {
-          noCache: true,
+          noCache: false,
           hidden: true,
           canTo: true,
           icon: 'ep:edit',

+ 2 - 0
src/views/pms/device/IotDeviceForm.vue

@@ -16,6 +16,8 @@ import type { IotModelVO } from '@/api/pms/model'
 import type { SupplierVO } from '@/api/supplier/base'
 import { useRefreshStore } from '@/store/modules/pms/refreshStore.js'
 
+defineOptions({ name: 'DeviceDetailForm' })
+
 type DynamicFieldType = 'text' | 'textarea' | 'date' | 'double' | 'int'
 
 interface DynamicField extends Omit<DeviceAttrModelData, 'code' | 'name' | 'type'> {

+ 31 - 0
src/views/pms/iotrddailyreport/summary.vue

@@ -1,5 +1,7 @@
 <script setup lang="ts">
 import dayjs from 'dayjs'
+import { IotRdDailyReportApi } from '@/api/pms/iotrddailyreport'
+import download from '@/utils/download'
 import { rangeShortcuts } from '@/utils/formatTime'
 import { resolveDailyReportDeptId } from '@/utils/dailyReportDept'
 import { useUserStore } from '@/store/modules/user'
@@ -12,6 +14,7 @@ defineOptions({
 
 const route = useRoute()
 const pageRouteName = route.name
+const message = useMessage()
 const TOP_DEPT_ID = 163
 const deptId = resolveDailyReportDeptId(useUserStore().getUser.deptId, TOP_DEPT_ID)
 
@@ -42,6 +45,7 @@ const activeTab = ref<'日报统计' | '非生产时效'>('日报统计')
 const defaultView = ref<'表格' | '看板'>('表格')
 const deptName = ref('四川瑞都')
 const refreshKey = ref(0)
+const exportLoading = ref(false)
 
 const getRouteCreateTime = () => {
   const createTime = route.query.createTime
@@ -97,6 +101,23 @@ const resetQuery = () => {
   handleQuery()
 }
 
+const handleExport = async () => {
+  try {
+    await message.exportConfirm()
+
+    exportLoading.value = true
+    const { pageNo: _pageNo, pageSize: _pageSize, ...params } = query.value
+
+    void _pageNo
+    void _pageSize
+
+    const data = await IotRdDailyReportApi.exportNptStatistics(params)
+    download.excel(data, '瑞都非生产时效统计.xlsx')
+  } finally {
+    exportLoading.value = false
+  }
+}
+
 onMounted(() => {
   syncQueryFromRoute()
 })
@@ -158,6 +179,16 @@ watch(
         <el-button size="small" @click="resetQuery">
           <Icon icon="ep:refresh" class="mr-5px" /> 重置
         </el-button>
+        <el-button
+          v-if="activeTab === '非生产时效'"
+          v-hasPermi="['pms:iot-rd-daily-report:export']"
+          size="small"
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
       </el-form-item>
     </el-form>
 

+ 64 - 7
src/views/pms/iotrydailyreport/ry-xj-form.vue

@@ -1,5 +1,6 @@
-<script lang="ts" setup generic="T">
+<script lang="ts" setup>
 import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import { IotProjectTaskApi } from '@/api/pms/iotprojecttask'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { calculateDuration, formatT } from '@/utils/formatTime'
@@ -15,6 +16,8 @@ const userStore = useUserStore()
 interface Props {
   visible: boolean
   type?: 'edit' | 'approval' | 'readonly'
+  deptId?: number
+  deptName?: string
   loadList: () => void
   noValidateStatus?: boolean
 }
@@ -159,7 +162,8 @@ const FORM_KEYS: (keyof FormOriginal)[] = [
 const formRef = ref<FormInstance>()
 const loading = ref(false)
 const formLoading = ref(false)
-const formType = ref<'edit' | 'readonly'>('edit')
+type FormMode = 'create' | 'edit' | 'readonly'
+const formType = ref<FormMode>('edit')
 const message = useMessage()
 const { t } = useI18n()
 
@@ -184,7 +188,41 @@ const isEdit = computed(() => props.type === 'edit')
 const isMainFieldDisabled = computed(() => formType.value === 'readonly' || isApproval.value)
 const isConstructionBriefDisabled = computed(() => formType.value === 'readonly')
 
-const wellOptions = ref<{ label: string; value: number }[]>([])
+interface WellOption {
+  label: string
+  value: number
+}
+
+const wellOptions = ref<WellOption[]>([])
+
+async function loadCreateTaskOptions() {
+  if (props.deptId === undefined) {
+    wellOptions.value = []
+    return
+  }
+
+  loading.value = true
+  try {
+    const tasks = await IotProjectTaskApi.getTaskWellNames({
+      deptId: props.deptId,
+      wellName: ''
+    })
+
+    wellOptions.value = tasks.map((task) => ({
+      label: task.wellName,
+      value: task.id
+    }))
+  } catch {
+    wellOptions.value = []
+    message.error('任务井加载失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleTaskChange(taskId: number) {
+  form.value.taskId = taskId
+}
 
 async function loadDetail(id: number) {
   loading.value = true
@@ -261,9 +299,21 @@ const removeProductionStatusRow = (index: number) => {
 
 const { ZmTable, ZmTableColumn } = useTableComponents<ReportDetail>()
 
-function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+function handleOpenForm(id: number | undefined, type: FormMode) {
   formType.value = type
   emits('update:visible', true)
+
+  if (id === undefined) {
+    wellOptions.value = []
+    form.value = initFormData()
+    form.value.deptId = props.deptId
+    form.value.deptName = props.deptName
+    addProductionStatusRow()
+    loadCreateTaskOptions()
+    nextTick(() => formRef.value?.clearValidate())
+    return
+  }
+
   loadDetail(id).then(() => {
     nextTick(() => formRef.value?.clearValidate())
   })
@@ -420,7 +470,9 @@ const submitForm = async () => {
     }
 
     await IotRyDailyReportApi.createIotRyDailyReport(submitData)
-    message.success(t('common.updateSuccess'))
+    message.success(
+      t(formType.value === 'create' ? 'common.createSuccess' : 'common.updateSuccess')
+    )
     emits('update:visible', false)
     props.loadList()
   } catch (error) {
@@ -498,7 +550,7 @@ const allocatedTimeDisabled = computed(() => {
     size="100%">
     <template #header>
       <span class="text-xl font-bold text-[var(--el-text-color-primary)]">
-        {{ type === 'edit' ? '编辑日报' : '审批日报' }}
+        {{ formType === 'create' ? '新增日报' : type === 'edit' ? '编辑日报' : '审批日报' }}
       </span>
     </template>
 
@@ -578,7 +630,12 @@ const allocatedTimeDisabled = computed(() => {
             v-model="form.taskId"
             placeholder="请选择任务井"
             :options="wellOptions"
-            :disabled="form.auditStatus !== 20 && form.auditStatus !== 30" />
+            filterable
+            @change="handleTaskChange"
+            :disabled="
+              formType === 'readonly' ||
+              (formType === 'edit' && form.auditStatus !== 20 && form.auditStatus !== 30)
+            " />
         </el-form-item>
         <el-form-item label="施工区域" prop="location">
           <el-input v-model="form.location" disabled />

+ 51 - 2
src/views/pms/iotrydailyreport/xfill.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import * as DeptApi from '@/api/system/dept'
 import { useUserStore } from '@/store/modules/user'
 import { resolveDailyReportDeptId } from '@/utils/dailyReportDept'
 import { rangeShortcuts } from '@/utils/formatTime'
@@ -72,6 +73,7 @@ function handleQuery(setPage = true) {
 
 function resetQuery() {
   query.value = { ...initQuery }
+  loadSelectedDeptName(query.value.deptId)
 
   handleQuery()
 }
@@ -93,7 +95,9 @@ const visible = ref(false)
 
 const formRef = ref()
 
-function handleOpenForm(id: number, type: 'edit' | 'readonly') {
+type FormMode = 'create' | 'edit' | 'readonly'
+
+function handleOpenForm(id: number | undefined, type: FormMode) {
   if (formRef.value) {
     formRef.value.handleOpenForm(id, type)
   }
@@ -101,7 +105,38 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
 
 const route = useRoute()
 
+interface DeptTreeNode {
+  id: number
+  name: string
+}
+
+const selectedDeptName = ref('')
+
+function handleDeptNodeClick(node: DeptTreeNode) {
+  selectedDeptName.value = node.name
+}
+
+async function loadSelectedDeptName(deptId?: number) {
+  if (deptId === undefined) {
+    selectedDeptName.value = ''
+    return
+  }
+
+  const currentDeptId = deptId
+  try {
+    const dept = await DeptApi.getDept(currentDeptId)
+    if (query.value.deptId === currentDeptId) {
+      selectedDeptName.value = dept.name
+    }
+  } catch {
+    if (query.value.deptId === currentDeptId) {
+      selectedDeptName.value = ''
+    }
+  }
+}
+
 onMounted(() => {
+  loadSelectedDeptName(query.value.deptId)
   if (Object.keys(route.query).length > 0) {
     handleOpenForm(Number(route.query.id), 'edit')
   }
@@ -117,6 +152,7 @@ onMounted(() => {
       v-model="query.deptId"
       :init-select="false"
       :show-title="false"
+      @node-click="handleDeptNodeClick"
       class="row-span-2" />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
@@ -156,6 +192,13 @@ onMounted(() => {
           <Icon icon="ep:search" class="mr-5px" /> 搜索
         </el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="handleOpenForm(undefined, 'create')"
+          v-hasPermi="['pms:iot-ry-daily-report:add']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
       </el-form-item>
     </el-form>
     <ry-xj-table
@@ -185,7 +228,13 @@ onMounted(() => {
       </template>
     </ry-xj-table>
   </div>
-  <ry-xj-form v-model:visible="visible" type="edit" ref="formRef" :load-list="loadList" />
+  <ry-xj-form
+    v-model:visible="visible"
+    type="edit"
+    ref="formRef"
+    :dept-id="query.deptId"
+    :dept-name="selectedDeptName"
+    :load-list="loadList" />
 </template>
 
 <style scoped>

+ 9 - 0
src/views/pms/maintain/IotMaintainEditPage.vue

@@ -0,0 +1,9 @@
+<template>
+  <IotMaintainAddEdit />
+</template>
+
+<script setup lang="ts">
+import IotMaintainAddEdit from './IotMaintainAddEdit.vue'
+
+defineOptions({ name: 'MaintainEdit' })
+</script>

+ 4 - 4
src/views/pms/stat/rhkb.vue

@@ -5,13 +5,13 @@ import rhsummary from './rhkb/rhsummary.vue'
 import deviceType from './rhkb/deviceType.vue'
 import deviceStatus from './rhkb/deviceStatus.vue'
 import operation from './rhkb/operation.vue'
-import todayGas from './rhkb/todayGas.vue'
-import historyGas from './rhkb/historyGas.vue'
+import gasStatistics from './rhkb/gasStatistics.vue'
 import deviceList from './rhkb/deviceList.vue'
 import rhsafeday from './rhkb/rhsafeday.vue'
 import assetValue from './rhkb/assetValue.vue'
 import equipmentRate from './rhkb/equipment-rate.vue'
 import inventorySituation from './rhkb/inventorySituation.vue'
+import exceptionPrompt from './rhkb/exceptionPrompt.vue'
 
 defineOptions({
   name: 'IotRhStatt'
@@ -110,8 +110,8 @@ onUnmounted(() => {
               <deviceType class="kb-stage-card kb-stage-card--2" />
               <equipmentRate class="kb-stage-card kb-stage-card--3" />
               <rhsafeday class="kb-stage-card kb-stage-card--4" />
-              <todayGas class="kb-stage-card kb-stage-card--5" />
-              <historyGas class="kb-stage-card kb-stage-card--6" />
+              <gasStatistics class="kb-stage-card kb-stage-card--5" />
+              <exceptionPrompt class="kb-stage-card kb-stage-card--6" />
               <assetValue class="kb-stage-card kb-stage-card--7" />
               <operation class="kb-stage-card kb-stage-card--8" />
               <inventorySituation class="kb-stage-card kb-stage-card--9" />

+ 346 - 0
src/views/pms/stat/rhkb/exceptionPrompt.vue

@@ -0,0 +1,346 @@
+<script lang="ts" setup>
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import { IotStatApi } from '@/api/pms/stat'
+
+dayjs.extend(quarterOfYear)
+
+type TimeRange = 'day' | 'month' | 'quarter' | 'year'
+
+type NptCountItem = {
+  nptName: string
+  displayName: string
+  teamCount: number
+}
+
+const router = useRouter()
+const activeTimeRange = ref<TimeRange>('day')
+const loading = ref(false)
+const productionList = ref<NptCountItem[]>([])
+const deviceExceptionCount = ref(0)
+
+const timeOptions: Array<{ label: string; value: TimeRange }> = [
+  { label: '本日', value: 'day' },
+  { label: '本月', value: 'month' },
+  { label: '本季', value: 'quarter' },
+  { label: '全年', value: 'year' }
+]
+
+const nptNameMap: Record<string, string> = {
+  no: '无工作量',
+  xcdm: '井场待命',
+  wxz: '维修设备',
+  wz: '物资影响',
+  yc: '异常/复杂'
+}
+
+const productionTopList = computed(() => productionList.value.slice(0, 9))
+
+function getNptDisplayName(nptName?: string) {
+  if (!nptName) return '-'
+
+  return nptNameMap[nptName] || nptName
+}
+
+function getCreateTime(type: TimeRange) {
+  const now = dayjs()
+  const start = now.startOf(type === 'day' ? 'day' : type === 'year' ? 'year' : type)
+  const end = now.endOf(type === 'day' ? 'day' : type === 'year' ? 'year' : type)
+
+  return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
+}
+
+function handleProductionClick(item: NptCountItem) {
+  router.push({
+    name: 'IotRhDailyReportSummary',
+    query: {
+      activeTab: '非生产时效',
+      nptName: item.nptName,
+      createTime: getCreateTime(activeTimeRange.value)
+    }
+  })
+}
+
+async function loadData() {
+  loading.value = true
+  try {
+    const res = await IotStatApi.getRhNptCount({
+      createTime: getCreateTime(activeTimeRange.value)
+    })
+
+    productionList.value = Array.isArray(res)
+      ? res.map((item) => ({
+          nptName: item.nptName || '',
+          displayName: getNptDisplayName(item.nptName),
+          teamCount: Number(item.teamCount || 0)
+        }))
+      : []
+  } catch (error) {
+    console.error('获取瑞恒生产异常失败:', error)
+    productionList.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+watch(activeTimeRange, loadData)
+
+onMounted(loadData)
+</script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title flex items-center justify-between">
+      <div class="kb-panel-title-text flex items-center">
+        <div class="icon-decorator">
+          <span></span>
+          <span></span>
+        </div>
+        异常警示
+      </div>
+      <el-segmented
+        v-model="activeTimeRange"
+        :options="timeOptions"
+        size="small"
+        class="exception-time-switch" />
+    </div>
+
+    <div v-loading="loading" class="exception-body">
+      <section class="exception-section exception-section--production">
+        <div class="exception-section__title">
+          <span class="exception-section__marker"></span>
+          生产异常
+          <span class="exception-section__hint">TOP 9</span>
+        </div>
+        <div class="production-grid">
+          <div
+            v-for="(item, index) in productionTopList"
+            :key="item.nptName"
+            class="production-item cursor-pointer"
+            role="button"
+            tabindex="0"
+            @click="handleProductionClick(item)"
+            @keyup.enter="handleProductionClick(item)">
+            <span class="production-item__rank">{{ index + 1 }}</span>
+            <span class="production-item__name">{{ item.displayName }}</span>
+            <span class="production-item__count">
+              <span class="production-item__value">{{ item.teamCount }}</span>
+              <span class="production-item__unit">个</span>
+            </span>
+          </div>
+          <div v-if="!productionTopList.length && !loading" class="exception-empty">
+            暂无生产异常数据
+          </div>
+        </div>
+      </section>
+
+      <section class="exception-section exception-section--device">
+        <div class="exception-section__title">
+          <span class="exception-section__marker"></span>
+          设备异常
+        </div>
+        <div class="device-exception-card">
+          <span class="device-exception-card__label">异常数量:</span>
+          <span class="device-exception-card__value">{{ deviceExceptionCount }}</span>
+          <span class="device-exception-card__unit">个</span>
+        </div>
+      </section>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.exception-time-switch {
+  --el-segmented-item-selected-color: #03409b;
+  --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
+  --el-segmented-bg-color: rgb(31 91 184 / 10%);
+  --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
+
+  min-height: calc(26px * var(--kb-scale, 1));
+  padding: calc(2px * var(--kb-scale, 1));
+  border: 1px solid rgb(31 91 184 / 12%);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
+
+  :deep(.el-segmented__item) {
+    min-height: calc(22px * var(--kb-scale, 1));
+    padding: 0 calc(7px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.exception-body {
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  flex-direction: column;
+  gap: calc(7px * var(--kb-scale, 1));
+  padding: calc(10px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1))
+    calc(12px * var(--kb-scale, 1));
+
+  :deep(.el-loading-mask) {
+    background-color: rgb(221 233 251 / 42%);
+  }
+}
+
+.exception-section {
+  min-height: 0;
+}
+
+.exception-section--production {
+  display: flex;
+  flex: 1 1 0;
+  flex-direction: column;
+}
+
+.exception-section--device {
+  position: relative;
+  z-index: 1;
+  flex: none;
+}
+
+.exception-section__title {
+  display: flex;
+  height: calc(19px * var(--kb-scale, 1));
+  align-items: center;
+  gap: calc(7px * var(--kb-scale, 1));
+  margin-bottom: calc(5px * var(--kb-scale, 1));
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: calc(20px * var(--kb-scale, 1));
+  color: #24364f;
+}
+
+.exception-section__marker {
+  width: calc(5px * var(--kb-scale, 1));
+  height: calc(16px * var(--kb-scale, 1));
+  background: linear-gradient(180deg, #2d7cf8 0%, #03409b 100%);
+  border-radius: 999px;
+  box-shadow: 0 0 calc(8px * var(--kb-scale, 1)) rgb(45 124 248 / 28%);
+}
+
+.exception-section__hint {
+  padding-top: calc(1px * var(--kb-scale, 1));
+  margin-left: auto;
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #6f85aa;
+}
+
+.production-grid {
+  display: grid;
+  min-height: 0;
+  flex: 1;
+  padding: calc(6px * var(--kb-scale, 1));
+  gap: calc(5px * var(--kb-scale, 1)) calc(7px * var(--kb-scale, 1));
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+
+.production-item {
+  display: grid;
+  height: 100%;
+  min-width: 0;
+  min-height: 0;
+  align-items: center;
+  padding: 0 calc(7px * var(--kb-scale, 1));
+  overflow: hidden;
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 36%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(6px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1)) rgb(63 103 171 / 7%);
+  grid-template-columns:
+    calc(18px * var(--kb-scale, 1)) minmax(0, 1fr)
+    calc(46px * var(--kb-scale, 1));
+  gap: calc(6px * var(--kb-scale, 1));
+}
+
+.production-item__rank {
+  display: inline-flex;
+  width: calc(18px * var(--kb-scale, 1));
+  height: calc(18px * var(--kb-scale, 1));
+  align-items: center;
+  justify-content: center;
+  font-size: calc(11px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #1f5bb8;
+  background: rgb(31 91 184 / 8%);
+  border: 1px solid rgb(31 91 184 / 10%);
+  border-radius: 999px;
+}
+
+.production-item__name {
+  overflow: hidden;
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 600;
+  line-height: calc(16px * var(--kb-scale, 1));
+  color: #24364f;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.production-item__count {
+  display: inline-flex;
+  align-items: baseline;
+  justify-content: flex-end;
+  gap: calc(2px * var(--kb-scale, 1));
+  min-width: 0;
+}
+
+.production-item__value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(19px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: 1;
+  color: #e43f5f;
+  text-align: right;
+}
+
+.production-item__unit {
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #24364f;
+}
+
+.exception-empty {
+  grid-column: 1 / -1;
+  place-self: center;
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #6f85aa;
+}
+
+.device-exception-card {
+  display: flex;
+  height: calc(42px * var(--kb-scale, 1));
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 36%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(6px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1)) rgb(63 103 171 / 7%);
+}
+
+.device-exception-card__label,
+.device-exception-card__unit {
+  font-size: calc(15px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #24364f;
+}
+
+.device-exception-card__value {
+  margin: 0 calc(6px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(23px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: 1;
+  color: #e43f5f;
+}
+</style>

+ 64 - 0
src/views/pms/stat/rhkb/gasStatistics.vue

@@ -0,0 +1,64 @@
+<script lang="ts" setup>
+import historyGas from './historyGas.vue'
+import todayGas from './todayGas.vue'
+
+type ActivePanel = 'today' | 'history'
+
+const activePanel = ref<ActivePanel>('today')
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  { label: '当日', value: 'today' },
+  { label: '累计', value: 'history' }
+]
+
+const activeTitle = computed(() =>
+  activePanel.value === 'today' ? '当日注气量统计' : '累计注气量统计'
+)
+</script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title flex items-center justify-between">
+      <div class="kb-panel-title-text flex items-center">
+        <div class="icon-decorator">
+          <span></span>
+          <span></span>
+        </div>
+        {{ activeTitle }}
+      </div>
+      <el-segmented
+        v-model="activePanel"
+        :options="panelOptions"
+        size="small"
+        class="gas-statistics-switch" />
+    </div>
+    <div class="flex-1 min-h-0">
+      <todayGas v-if="activePanel === 'today'" embedded />
+      <historyGas v-else embedded />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.gas-statistics-switch {
+  --el-segmented-item-selected-color: #03409b;
+  --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
+  --el-segmented-bg-color: rgb(31 91 184 / 10%);
+  --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
+
+  min-height: calc(26px * var(--kb-scale, 1));
+  padding: calc(2px * var(--kb-scale, 1));
+  border: 1px solid rgb(31 91 184 / 12%);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
+
+  :deep(.el-segmented__item) {
+    min-height: calc(22px * var(--kb-scale, 1));
+    padding: 0 calc(8px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+</style>

+ 6 - 2
src/views/pms/stat/rhkb/historyGas.vue

@@ -13,6 +13,10 @@ import {
 } from '@/utils/kb'
 import { IotStatApi } from '@/api/pms/stat'
 
+defineProps<{
+  embedded?: boolean
+}>()
+
 const chartData = ref<ChartItem[]>([])
 
 const router = useRouter()
@@ -225,8 +229,8 @@ onUnmounted(() => {
 </script>
 
 <template>
-  <div class="panel flex flex-col">
-    <div class="panel-title">
+  <div :class="embedded ? 'h-full flex flex-col' : 'panel flex flex-col'">
+    <div v-if="!embedded" class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 6 - 2
src/views/pms/stat/rhkb/todayGas.vue

@@ -12,6 +12,10 @@ import {
 } from '@/utils/kb'
 import { IotStatApi } from '@/api/pms/stat'
 
+defineProps<{
+  embedded?: boolean
+}>()
+
 const chartData = ref<ChartData>({
   xAxis: [],
   series: []
@@ -237,8 +241,8 @@ onUnmounted(() => {
 </script>
 
 <template>
-  <div class="panel flex flex-col">
-    <div class="panel-title">
+  <div :class="embedded ? 'h-full flex flex-col' : 'panel flex flex-col'">
+    <div v-if="!embedded" class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>