yanghao před 4 dny
rodič
revize
fdc601ef7c
1 změnil soubory, kde provedl 755 přidání a 0 odebrání
  1. 755 0
      src/views/pms/qhse/kanban/index.vue

+ 755 - 0
src/views/pms/qhse/kanban/index.vue

@@ -0,0 +1,755 @@
+<script lang="ts" setup>
+import {
+  AlarmClock,
+  Checked,
+  CollectionTag,
+  DataAnalysis,
+  DocumentChecked,
+  Files,
+  Flag,
+  Histogram,
+  Opportunity,
+  Postcard,
+  Warning
+} from '@element-plus/icons-vue'
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
+
+defineOptions({
+  name: 'PmsQhseKanban'
+})
+
+type SummaryCard = {
+  title: string
+  value: string
+  note: string
+  accent: string
+  glow: string
+  icon: any
+}
+
+type MetricBar = {
+  label: string
+  value: number
+  color: string
+}
+
+type RiskZone = {
+  title: string
+  desc: string
+  color: string
+}
+
+type PermitStat = {
+  label: string
+  value: string
+  accent: string
+  soft: string
+}
+
+type BottomCard = {
+  title: string
+  icon: any
+  accent: string
+  glow: string
+  lines: string[]
+}
+
+const wrapperRef = ref<HTMLDivElement>()
+const scale = ref(1)
+const supportsZoom = ref(false)
+
+let resizeObserver: ResizeObserver | null = null
+let resizeRaf = 0
+
+const pageTitle = 'QHSE管理看板'
+
+const summaryCards: SummaryCard[] = [
+  {
+    title: '风险总数(处)',
+    value: '126',
+    note: 'R:12',
+    accent: '#ff5a62',
+    glow: 'rgba(255, 90, 98, 0.26)',
+    icon: Warning
+  },
+  {
+    title: '今日隐患(条)',
+    value: '29',
+    note: 'Exp:2',
+    accent: '#ff9f2f',
+    glow: 'rgba(255, 159, 47, 0.24)',
+    icon: Opportunity
+  },
+  {
+    title: '隐患整改率',
+    value: '92.3%',
+    note: '较昨日 +1.6%',
+    accent: '#2ac7c9',
+    glow: 'rgba(42, 199, 201, 0.26)',
+    icon: DataAnalysis
+  },
+  {
+    title: '在线作业许可',
+    value: '11',
+    note: 'Active',
+    accent: '#4f8dff',
+    glow: 'rgba(79, 141, 255, 0.22)',
+    icon: DocumentChecked
+  },
+  {
+    title: '人员持证率',
+    value: '97.8%',
+    note: 'Warn:3',
+    accent: '#f2b800',
+    glow: 'rgba(242, 184, 0, 0.22)',
+    icon: Postcard
+  }
+]
+
+const hazardBars: MetricBar[] = [
+  { label: '已整改 628', value: 100, color: '#43c7ca' },
+  { label: '未整改 29', value: 61, color: '#ff981f' },
+  { label: '超期 2', value: 38, color: '#ff4c49' }
+]
+
+const incidentStats = [
+  { label: '安全事故', value: '0起', accent: '#2ac7c9' },
+  { label: '险情事件', value: '3起', accent: '#f2c11a' }
+]
+
+const riskZones: RiskZone[] = [
+  { title: '高危风险区', desc: '危化库 / 试压区 / 配电房', color: '#ff4c49' },
+  { title: '中风险区', desc: '焊接 / 机加 / 吊装区', color: '#ff981f' },
+  { title: '低风险区', desc: '物料库 / 维修 / 装卸区', color: '#f2c11a' },
+  { title: '安全控制区', desc: '办公区 / 展厅 / 主通道', color: '#5794ff' }
+]
+
+const permitStats: PermitStat[] = [
+  { label: '在线作业', value: '11', accent: '#4d8cff', soft: 'rgba(77, 140, 255, 0.16)' },
+  { label: '异常预警', value: '2', accent: '#d0a400', soft: 'rgba(208, 164, 0, 0.14)' }
+]
+
+const qualificationWarnings = [
+  { label: '证件过期', value: '0人', accent: '#24364f' },
+  { label: '即将到期', value: '3人(需复审)', accent: '#e6ab00' }
+]
+
+const bottomCards: BottomCard[] = [
+  {
+    title: '体系合规',
+    icon: Files,
+    accent: '#39c6cc',
+    glow: 'rgba(57, 198, 204, 0.22)',
+    lines: ['内审已完成', '外审待安排']
+  },
+  {
+    title: '特种设备',
+    icon: Histogram,
+    accent: '#4f8dff',
+    glow: 'rgba(79, 141, 255, 0.2)',
+    lines: ['在用: 32台', '待检: 2台(重点关注)']
+  },
+  {
+    title: '应急演练',
+    icon: AlarmClock,
+    accent: '#ff5b61',
+    glow: 'rgba(255, 91, 97, 0.22)',
+    lines: ['年度计划完成率', '89%(需补1次)']
+  },
+  {
+    title: '质量检验',
+    icon: Checked,
+    accent: '#f2c11a',
+    glow: 'rgba(242, 193, 26, 0.2)',
+    lines: ['产品一次合格率', '98.7%(达标)']
+  },
+  {
+    title: '环境危废',
+    icon: Flag,
+    accent: '#28c98b',
+    glow: 'rgba(40, 201, 139, 0.2)',
+    lines: ['危废暂存合规', '三废排放100%达标']
+  }
+]
+
+const targetWrapperStyle = computed(() => ({
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const targetAreaStyle = computed(() => {
+  const style = {
+    width: `${DESIGN_WIDTH}px`,
+    height: `${DESIGN_HEIGHT}px`,
+    transformOrigin: '0 0'
+  } as Record<string, string | number>
+
+  if (supportsZoom.value) {
+    style.zoom = scale.value
+  } else {
+    style.transform = `scale(${scale.value})`
+  }
+
+  return style
+})
+
+function updateScale() {
+  cancelAnimationFrame(resizeRaf)
+
+  resizeRaf = requestAnimationFrame(() => {
+    const wrapper = wrapperRef.value
+    if (!wrapper) return
+
+    const { clientWidth, clientHeight } = wrapper
+    if (!clientWidth || !clientHeight) return
+
+    scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+  })
+}
+
+onMounted(() => {
+  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  window.addEventListener('resize', updateScale)
+})
+
+onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  cancelAnimationFrame(resizeRaf)
+})
+</script>
+
+<template>
+  <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div id="qhse-kanban" class="bg qhse-board" :style="targetAreaStyle">
+        <header class="header">{{ pageTitle }}</header>
+        <div class="board-body">
+          <section class="panel summary-panel kb-stage-card kb-stage-card--1">
+            <div class="panel-title">
+              <span class="icon-decorator"><span></span><span></span></span>
+              风险总览
+            </div>
+            <div class="summary-grid">
+              <article
+                v-for="card in summaryCards"
+                :key="card.title"
+                class="summary-card summary-tile"
+                :style="
+                  {
+                    '--card-accent': card.accent,
+                    '--card-glow': card.glow
+                  } as any
+                "
+              >
+                <span class="summary-card__shine"></span>
+                <span class="summary-card__corner"></span>
+                <div class="summary-card__icon">
+                  <el-icon class="summary-card__icon-glyph">
+                    <component :is="card.icon" />
+                  </el-icon>
+                </div>
+                <div class="summary-card__content">
+                  <div class="summary-card__label">{{ card.title }}</div>
+                  <div class="summary-tile__meta">
+                    <span class="summary-tile__value">{{ card.value }}</span>
+                    <span class="summary-tile__note" :style="{ color: card.accent }">{{ card.note }}</span>
+                  </div>
+                </div>
+              </article>
+            </div>
+          </section>
+
+          <div class="board-main">
+            <div class="left-column">
+              <section class="panel board-panel kb-stage-card kb-stage-card--2">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  隐患排查治理统计
+                </div>
+                <div class="chart-panel chart-panel--bars">
+                  <div v-for="bar in hazardBars" :key="bar.label" class="bar-card">
+                    <div class="bar-card__chart">
+                      <div class="bar-card__fill" :style="{ height: `${bar.value}%`, background: bar.color }"></div>
+                    </div>
+                    <div class="bar-card__label">{{ bar.label }}</div>
+                  </div>
+                </div>
+              </section>
+
+              <section class="panel board-panel kb-stage-card kb-stage-card--3">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  事故事件趋势(近12月)
+                </div>
+                <div class="incident-panel">
+                  <div class="incident-graphic">
+                    <div class="incident-graphic__axis"></div>
+                    <div class="incident-graphic__area"></div>
+                    <div class="incident-graphic__line"></div>
+                  </div>
+                  <div class="incident-metrics">
+                    <div v-for="item in incidentStats" :key="item.label" class="incident-metric">
+                      <span>{{ item.label }}:</span>
+                      <strong :style="{ color: item.accent }">{{ item.value }}</strong>
+                    </div>
+                  </div>
+                </div>
+              </section>
+            </div>
+
+            <div class="center-column">
+              <section class="panel board-panel board-panel--center kb-stage-card kb-stage-card--4">
+                <div class="panel-title panel-title--center">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  安全风险四色动态分布
+                </div>
+                <div class="risk-grid">
+                  <article v-for="zone in riskZones" :key="zone.title" class="risk-card">
+                    <div class="risk-card__title">
+                      <span class="risk-card__dot" :style="{ background: zone.color }"></span>
+                      <span :style="{ color: zone.color }">{{ zone.title }}</span>
+                    </div>
+                    <div class="risk-card__desc">{{ zone.desc }}</div>
+                  </article>
+                </div>
+              </section>
+            </div>
+
+            <div class="right-column">
+              <section class="panel board-panel kb-stage-card kb-stage-card--5">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  作业许可实时监控
+                </div>
+                <div class="permit-grid">
+                  <div
+                    v-for="item in permitStats"
+                    :key="item.label"
+                    class="permit-card"
+                    :style="{ '--permit-accent': item.accent, '--permit-soft': item.soft } as any"
+                  >
+                    <div class="permit-card__label">{{ item.label }}</div>
+                    <div class="permit-card__value">{{ item.value }}</div>
+                  </div>
+                </div>
+              </section>
+
+              <section class="panel board-panel kb-stage-card kb-stage-card--6">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  人员资质风险预警
+                </div>
+                <div class="qualification-panel">
+                  <div class="qualification-icon">
+                    <el-icon>
+                      <CollectionTag />
+                    </el-icon>
+                  </div>
+                  <div class="qualification-list">
+                    <div
+                      v-for="item in qualificationWarnings"
+                      :key="item.label"
+                      class="qualification-item"
+                    >
+                      <span class="qualification-item__label">{{ item.label }}:</span>
+                      <strong :style="{ color: item.accent }">{{ item.value }}</strong>
+                    </div>
+                  </div>
+                </div>
+              </section>
+            </div>
+          </div>
+
+          <section class="bottom-grid">
+            <article
+              v-for="(card, index) in bottomCards"
+              :key="card.title"
+              class="panel bottom-card kb-stage-card"
+              :class="`kb-stage-card--${index + 7}`"
+              :style="
+                {
+                  '--card-accent': card.accent,
+                  '--card-glow': card.glow
+                } as any
+              "
+            >
+              <span class="summary-card__shine"></span>
+              <div class="bottom-card__header">
+                <div class="summary-card__icon bottom-card__icon">
+                  <el-icon class="summary-card__icon-glyph">
+                    <component :is="card.icon" />
+                  </el-icon>
+                </div>
+                <div class="bottom-card__title">{{ card.title }}</div>
+              </div>
+              <div class="bottom-card__content">
+                <p v-for="line in card.lines" :key="line">{{ line }}</p>
+              </div>
+            </article>
+          </section>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.qhse-board {
+  color: #24364f;
+}
+
+.board-body {
+  padding: 18px 18px 24px;
+}
+
+.summary-panel {
+  padding-bottom: 22px;
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 24px;
+  margin-top: 20px;
+}
+
+.summary-tile {
+  display: flex;
+  height: 136px;
+  padding: 28px 28px 24px;
+  border-radius: 22px;
+  align-items: center;
+  gap: 22px;
+}
+
+.summary-card__content {
+  position: relative;
+  z-index: 2;
+  min-width: 0;
+  flex: 1;
+}
+
+.summary-tile__meta {
+  display: flex;
+  margin-top: 14px;
+  align-items: baseline;
+  gap: 8px;
+}
+
+.summary-tile__value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: 40px;
+  line-height: 1;
+  color: #114a9b;
+  letter-spacing: 1px;
+}
+
+.summary-tile__note {
+  font-size: 18px;
+  font-weight: 700;
+}
+
+.board-main {
+  display: grid;
+  grid-template-columns: 1.02fr 1fr 0.98fr;
+  gap: 28px;
+  margin-top: 24px;
+}
+
+.left-column,
+.right-column {
+  display: grid;
+  gap: 24px;
+}
+
+.center-column {
+  display: block;
+}
+
+.board-panel {
+  min-height: 258px;
+}
+
+.board-panel--center {
+  min-height: 540px;
+}
+
+.chart-panel {
+  margin-top: 18px;
+}
+
+.chart-panel--bars {
+  display: flex;
+  height: 176px;
+  padding: 6px 10px 0;
+  align-items: flex-end;
+  gap: 34px;
+}
+
+.bar-card {
+  display: flex;
+  height: 100%;
+  flex: 1;
+  flex-direction: column;
+  align-items: center;
+  justify-content: flex-end;
+}
+
+.bar-card__chart {
+  display: flex;
+  width: 100%;
+  height: 120px;
+  padding: 0 20px;
+  align-items: flex-end;
+}
+
+.bar-card__fill {
+  width: 100%;
+  min-height: 32px;
+  border-radius: 0;
+  box-shadow: 0 10px 24px rgb(31 91 184 / 10%);
+}
+
+.bar-card__label {
+  margin-top: 14px;
+  font-size: 18px;
+  font-weight: 700;
+  color: #5b6f8f;
+}
+
+.incident-panel {
+  display: grid;
+  margin-top: 22px;
+  grid-template-columns: 180px 1fr;
+  align-items: center;
+  gap: 28px;
+}
+
+.incident-graphic {
+  position: relative;
+  width: 170px;
+  height: 132px;
+}
+
+.incident-graphic__axis {
+  position: absolute;
+  inset: 24px 16px 16px 16px;
+  border-bottom: 6px solid #4f8dff;
+  border-left: 6px solid #4f8dff;
+  border-radius: 2px;
+  opacity: 0.92;
+}
+
+.incident-graphic__area {
+  position: absolute;
+  right: 22px;
+  bottom: 24px;
+  width: 102px;
+  height: 68px;
+  background: linear-gradient(180deg, rgb(79 141 255 / 88%) 0%, rgb(79 141 255 / 28%) 100%);
+  clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 100%);
+}
+
+.incident-graphic__line {
+  position: absolute;
+  right: 22px;
+  bottom: 24px;
+  width: 102px;
+  height: 68px;
+  border-top: 5px solid #4f8dff;
+  clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 5%, 54% 77%, 22% 55%, 0 100%);
+}
+
+.incident-metrics {
+  display: grid;
+  gap: 20px;
+}
+
+.incident-metric {
+  font-size: 24px;
+  font-weight: 700;
+  color: #556b89;
+}
+
+.incident-metric strong {
+  margin-left: 4px;
+  font-size: 34px;
+  line-height: 1;
+}
+
+.panel-title--center {
+  justify-content: center;
+  padding-left: 0;
+  font-size: 32px;
+}
+
+.panel-title--center .icon-decorator {
+  left: 28px;
+}
+
+.risk-grid {
+  display: grid;
+  margin-top: 34px;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 26px 28px;
+}
+
+.risk-card {
+  min-height: 132px;
+  padding: 34px 24px;
+  background: linear-gradient(180deg, rgb(255 255 255 / 42%) 0%, rgb(220 232 250 / 28%) 100%);
+  border: 1px solid rgb(255 255 255 / 58%);
+  border-radius: 18px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 74%),
+    0 8px 18px rgb(63 103 171 / 7%);
+}
+
+.risk-card__title {
+  display: flex;
+  font-size: 20px;
+  font-weight: 800;
+  align-items: center;
+  gap: 12px;
+}
+
+.risk-card__dot {
+  width: 18px;
+  height: 18px;
+  border-radius: 999px;
+  box-shadow: 0 0 0 6px rgb(255 255 255 / 35%);
+}
+
+.risk-card__desc {
+  margin-top: 16px;
+  font-size: 17px;
+  font-weight: 600;
+  color: #6f819a;
+  line-height: 1.55;
+}
+
+.permit-grid {
+  display: grid;
+  margin-top: 32px;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 20px;
+}
+
+.permit-card {
+  min-height: 128px;
+  padding: 28px 20px;
+  text-align: center;
+  background: linear-gradient(180deg, var(--permit-soft) 0%, rgb(255 255 255 / 18%) 100%);
+  border: 1px solid rgb(255 255 255 / 54%);
+  border-radius: 18px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 68%),
+    0 8px 18px rgb(63 103 171 / 6%);
+}
+
+.permit-card__label {
+  font-size: 18px;
+  font-weight: 700;
+  color: #506684;
+}
+
+.permit-card__value {
+  margin-top: 18px;
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: 44px;
+  line-height: 1;
+  color: var(--permit-accent);
+}
+
+.qualification-panel {
+  display: grid;
+  margin-top: 26px;
+  grid-template-columns: 140px 1fr;
+  align-items: center;
+  gap: 26px;
+}
+
+.qualification-icon {
+  display: flex;
+  width: 132px;
+  height: 132px;
+  font-size: 74px;
+  color: #f09717;
+  background: radial-gradient(circle at 50% 40%, rgb(255 255 255 / 74%) 0%, rgb(231 240 255 / 42%) 100%);
+  border: 1px solid rgb(255 255 255 / 72%);
+  border-radius: 32px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 85%),
+    0 10px 22px rgb(55 94 160 / 10%);
+  align-items: center;
+  justify-content: center;
+}
+
+.qualification-list {
+  display: grid;
+  gap: 22px;
+}
+
+.qualification-item {
+  font-size: 24px;
+  font-weight: 700;
+  color: #516785;
+}
+
+.qualification-item strong {
+  font-size: 28px;
+}
+
+.bottom-grid {
+  display: grid;
+  margin-top: 26px;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 24px;
+}
+
+.bottom-card {
+  min-height: 190px;
+  padding: 28px 22px 24px;
+  overflow: hidden;
+}
+
+.bottom-card__header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.bottom-card__icon {
+  width: 54px;
+  height: 54px;
+}
+
+.bottom-card__title {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: 24px;
+  color: #114a9b;
+  letter-spacing: 1px;
+}
+
+.bottom-card__content {
+  position: relative;
+  z-index: 2;
+  margin-top: 24px;
+}
+
+.bottom-card__content p {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 700;
+  color: #5d718e;
+  line-height: 1.7;
+}
+</style>