695593266@qq.com пре 5 дана
родитељ
комит
c2fb8c80c0

+ 48 - 5
src/views/factoryCalendar/components/StatisticsView.vue

@@ -206,6 +206,47 @@
           </el-tooltip>
         </div>
       </div>
+
+      <div class="industrial-panel conflict-analysis">
+        <div class="industrial-panel-head">
+          <div>
+            <span>Conflict Trend</span>
+            <strong>冲突趋势</strong>
+            <p>按当前月份分段统计冲突密度</p>
+          </div>
+          <el-tag size="mini" :type="conflictTotal > 0 ? 'danger' : 'success'">
+            {{ conflictTotal > 0 ? '需要处理' : '运行平稳' }}
+          </el-tag>
+        </div>
+
+        <div class="conflict-analysis-body">
+          <div class="conflict-total" :class="{ danger: conflictTotal > 0 }">
+            <span>冲突天数</span>
+            <strong>{{ conflictTotal }}</strong>
+            <em>{{ conflictTotal > 0 ? '待排查' : '无异常' }}</em>
+          </div>
+
+          <div class="conflict-bars">
+            <el-tooltip
+              v-for="item in vm.conflictTrend"
+              :key="item.label"
+              placement="top"
+              effect="light"
+              :content="vm.getConflictTrendTooltip(item)"
+              :disabled="isScrolling"
+              :enterable="false"
+              popper-class="factory-calendar-stat-tooltip"
+            >
+              <div class="conflict-bar" :class="{ 'is-zero': !item.count }">
+                <span :style="{ height: item.height + '%' }">
+                  <b>{{ item.count }}</b>
+                </span>
+                <em>{{ item.label }}</em>
+              </div>
+            </el-tooltip>
+          </div>
+        </div>
+      </div>
     </section>
   </div>
 </template>
@@ -238,14 +279,16 @@
           .length;
       },
       displayVisualKpis() {
-        return (this.vm.visualKpis || []).filter(
-          (item) => item.key !== 'conflict'
-        );
+        return this.vm.visualKpis || [];
       },
       displayMonthBars() {
-        return (this.vm.visualMonthBars || []).filter(
-          (item) => item.key !== 'conflict'
+        return this.vm.visualMonthBars || [];
+      },
+      conflictTotal() {
+        const item = (this.vm.monthStats || []).find(
+          (stat) => stat.key === 'conflict'
         );
+        return Number(item?.value || 0);
       },
       healthScore() {
         const scheduleScore = Number(this.vm.scheduleRate) || 0;

+ 214 - 0
src/views/factoryCalendar/components/factoryCalendar.scss

@@ -5805,6 +5805,220 @@
     }
   }
 
+  .factory-calendar .industrial-stat-board .industrial-kpi-strip {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+
+  .factory-calendar .industrial-stat-board .industrial-main-grid {
+    grid-template-columns: minmax(0, 1.28fr) minmax(380px, 0.72fr);
+    grid-template-areas:
+      'schedule types'
+      'month conflict';
+  }
+
+  .factory-calendar .industrial-stat-board .month-analysis {
+    grid-area: month;
+    grid-column: auto;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-analysis {
+    display: flex;
+    grid-area: conflict;
+    min-height: 0;
+    flex-direction: column;
+  }
+
+  .factory-calendar .industrial-stat-board .month-telemetry-list {
+    grid-template-columns: repeat(2, minmax(180px, 1fr));
+  }
+
+  .factory-calendar .industrial-stat-board .month-telemetry-row {
+    min-height: 76px;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-analysis-body {
+    display: grid;
+    flex: 1;
+    grid-template-columns: 1fr;
+    grid-template-rows: auto minmax(168px, 1fr);
+    gap: 14px;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total {
+    position: relative;
+    display: flex;
+    min-height: 74px;
+    align-items: center;
+    justify-content: space-between;
+    flex-direction: row;
+    padding: 14px 18px 14px 20px;
+    overflow: hidden;
+    border: 1px solid rgba(74, 162, 196, 0.24);
+    border-radius: 8px;
+    background:
+      linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(231, 249, 252, 0.78)),
+      rgba(255, 255, 255, 0.82);
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.9),
+      0 8px 18px rgba(45, 95, 123, 0.07);
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 14px;
+    bottom: 14px;
+    width: 4px;
+    border-radius: 0 4px 4px 0;
+    background: linear-gradient(180deg, #2bc4b6, #308ee8);
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total.danger::before {
+    background: linear-gradient(180deg, #ff8f70, #f05d68);
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total span {
+    color: #607789;
+    font-size: 14px;
+    line-height: 22px;
+    font-weight: 600;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total strong {
+    margin-left: auto;
+    color: #0b9f91;
+    font-size: 36px;
+    line-height: 42px;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total.danger strong {
+    color: #df5a63;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-total em {
+    margin-left: 10px;
+    color: #6d8292;
+    font-size: 12px;
+    line-height: 18px;
+    font-style: normal;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bars {
+    position: relative;
+    display: grid;
+    height: 100%;
+    min-height: 214px;
+    grid-template-columns: repeat(6, minmax(54px, 1fr));
+    gap: 12px;
+    align-items: end;
+    padding: 32px 18px 18px;
+    overflow: hidden;
+    border: 1px solid rgba(74, 162, 196, 0.22);
+    border-radius: 8px;
+    background:
+      linear-gradient(rgba(66, 151, 186, 0.11) 1px, transparent 1px),
+      linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(237, 249, 251, 0.72));
+    background-size: 100% 34px, 100% 100%;
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.88),
+      0 8px 18px rgba(45, 95, 123, 0.06);
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bar {
+    display: flex;
+    height: 158px;
+    min-width: 0;
+    align-items: center;
+    justify-content: flex-end;
+    flex-direction: column;
+    gap: 9px;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bar span {
+    position: relative;
+    display: flex;
+    width: min(46px, 70%);
+    min-height: 8px;
+    max-width: 46px;
+    align-items: flex-start;
+    justify-content: center;
+    border: 1px solid rgba(255, 255, 255, 0.68);
+    border-radius: 6px 6px 3px 3px;
+    background:
+      linear-gradient(180deg, rgba(255, 255, 255, 0.36), transparent 45%),
+      linear-gradient(180deg, #ff9b8f, #f05d68);
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.44),
+      0 7px 13px rgba(229, 87, 97, 0.2);
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bar.is-zero span {
+    background:
+      linear-gradient(180deg, rgba(255, 255, 255, 0.52), transparent 45%),
+      linear-gradient(180deg, #8fe2e0, #35b7c8);
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.5),
+      0 6px 12px rgba(53, 183, 200, 0.16);
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bar b {
+    position: absolute;
+    top: -24px;
+    color: #d86363;
+    font-size: 13px;
+    line-height: 18px;
+    font-weight: 700;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bar.is-zero b {
+    color: #22a6a2;
+  }
+
+  .factory-calendar .industrial-stat-board .conflict-bar em {
+    width: 64px;
+    overflow: visible;
+    color: #587284;
+    font-size: 12px;
+    line-height: 16px;
+    text-align: center;
+    text-overflow: clip;
+    white-space: nowrap;
+    font-style: normal;
+  }
+
+  @media (max-width: 1500px) {
+    .factory-calendar .industrial-stat-board .industrial-main-grid {
+      grid-template-columns: minmax(0, 1.12fr) minmax(350px, 0.88fr);
+    }
+
+    .factory-calendar .industrial-stat-board .industrial-kpi-strip {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+  }
+
+  @media (max-width: 1360px) {
+    .factory-calendar .industrial-stat-board .industrial-main-grid {
+      grid-template-columns: 1fr;
+      grid-template-areas:
+        'schedule'
+        'types'
+        'month'
+        'conflict';
+    }
+
+    .factory-calendar .industrial-stat-board .month-telemetry-list {
+      grid-template-columns: repeat(3, minmax(160px, 1fr));
+    }
+  }
+
+  @media (max-width: 900px) {
+    .factory-calendar .industrial-stat-board .industrial-kpi-strip,
+    .factory-calendar .industrial-stat-board .month-telemetry-list {
+      grid-template-columns: 1fr;
+    }
+  }
+
   @keyframes factoryStatTipPulse {
     0% {
       transform: translateY(0) scale(1);

+ 163 - 32
src/views/factoryCalendar/index.vue

@@ -24,6 +24,10 @@
           <multi-calendar-view :vm="vm" />
         </el-tab-pane>
 
+        <el-tab-pane label="冲突检测" name="conflict">
+          <conflict-panel ref="conflictPanel" :vm="vm" />
+        </el-tab-pane>
+
         <el-tab-pane label="临时调整审批" name="adjust">
           <adjust-approval ref="adjustApproval" :vm="vm" />
         </el-tab-pane>
@@ -740,6 +744,7 @@
   import { solarToLunar } from 'lunar-calendar';
   import BaseManage from './components/BaseManage.vue';
   import MultiCalendarView from './components/MultiCalendarView.vue';
+  import ConflictPanel from './components/ConflictPanel.vue';
   import AdjustApproval from './components/AdjustApproval.vue';
   import StatisticsView from './components/StatisticsView.vue';
   import {
@@ -873,6 +878,7 @@
     components: {
       BaseManage,
       MultiCalendarView,
+      ConflictPanel,
       AdjustApproval,
       StatisticsView
     },
@@ -1399,29 +1405,80 @@
         );
       },
       monthStats() {
-        const visibleDays = this.statCells;
-        const workDays = visibleDays.filter((item) => !item.isRest).length;
-        const scheduledDays = visibleDays.filter(
-          (item) => !item.isRest && item.scheduleStatus
-        ).length;
-        const conflictDays = visibleDays.filter(
-          (item) => item.isConflict
-        ).length;
-        const tempDays = visibleDays.filter((item) => item.isTempAdjust).length;
-        const restDays = visibleDays.filter((item) => item.isRest).length;
+        const details = this.getStatMonthDetails();
+        const countDistinctDates = (predicate) =>
+          new Set(
+            details
+              .filter(predicate)
+              .map((item) => item.calendarDate)
+              .filter(Boolean)
+          ).size;
+        const workDays = countDistinctDates(
+          (item) => Number(item.dateType) === 1
+        );
+        const scheduledDateSet = new Set(
+          details
+            .filter((item) => this.isDetailScheduled(item))
+            .map((item) => item.calendarDate)
+            .filter(Boolean)
+        );
+        const scheduledDays = scheduledDateSet.size;
+        const scheduledWorkdaySet = new Set(
+          details
+            .filter(
+              (item) =>
+                Number(item.dateType) === 1 &&
+                scheduledDateSet.has(item.calendarDate)
+            )
+            .map((item) => item.calendarDate)
+        );
+        const emptyWorkdaySet = new Set(
+          details
+            .filter(
+              (item) =>
+                Number(item.dateType) === 1 &&
+                !scheduledDateSet.has(item.calendarDate)
+            )
+            .map((item) => item.calendarDate)
+        );
+        const idleDays = Math.max(
+          emptyWorkdaySet.size,
+          workDays - scheduledWorkdaySet.size,
+          0
+        );
+        const conflictDays = countDistinctDates(
+          (item) => Number(item.isConflict) === 1
+        );
+        const tempDays = countDistinctDates(
+          (item) => Number(item.isTempAdjust) === 1
+        );
+        const restDays = countDistinctDates((item) =>
+          [2, 3].includes(Number(item.dateType))
+        );
         return [
           { key: 'work', label: '总工作日', value: workDays },
           { key: 'scheduled', label: '已排班天数', value: scheduledDays },
           {
             key: 'idle',
             label: '未排班天数',
-            value: Math.max(workDays - scheduledDays, 0)
+            value: idleDays
           },
           { key: 'conflict', label: '冲突天数', value: conflictDays },
           { key: 'temp', label: '临时调整天数', value: tempDays },
           { key: 'rest', label: '休息/节假日', value: restDays }
         ];
       },
+      scheduledWorkdayDays() {
+        return new Set(
+          this.getStatMonthDetails()
+            .filter(
+              (item) =>
+                Number(item.dateType) === 1 && this.isDetailScheduled(item)
+            )
+            .map((item) => item.calendarDate)
+            .filter(Boolean)
+        ).size;
+      },
       dayDrawerTitle() {
         return this.currentDay.date
           ? `${this.currentDay.date} 日历详情`
@@ -1542,9 +1599,12 @@
       scheduleRate() {
         const workDays =
           this.monthStats.find((item) => item.key === 'work')?.value || 0;
-        const scheduledDays =
-          this.monthStats.find((item) => item.key === 'scheduled')?.value || 0;
-        return workDays ? Math.round((scheduledDays / workDays) * 100) : 0;
+        return workDays
+          ? Math.min(
+              100,
+              Math.round((this.scheduledWorkdayDays / workDays) * 100)
+            )
+          : 0;
       },
       scheduleSegments() {
         const scheduled =
@@ -1627,9 +1687,15 @@
               '0'
             )}-01`
           ).date(index * 5 + 1);
-          const count = this.statCells.filter(
-            (item) => item.isConflict && dayjs(item.date).isSame(date, 'week')
-          ).length;
+          const count = new Set(
+            this.getStatMonthDetails()
+              .filter(
+                (item) =>
+                  Number(item.isConflict) === 1 &&
+                  dayjs(item.calendarDate).isSame(date, 'week')
+              )
+              .map((item) => item.calendarDate)
+          ).size;
           return {
             label: date.format('MM/DD'),
             count
@@ -1864,9 +1930,10 @@
             this.viewDataPromise = null;
             return;
           }
-          const remoteDetails = data.map((item) =>
-            this.normalizeRemoteDetail(item)
-          );
+          const remoteDetails = data.map((item) => ({
+            ...this.normalizeRemoteDetail(item),
+            isViewDetail: true
+          }));
           const visibleDates =
             queryOverride && query.viewType === 'month'
               ? this.getMonthDateRange(query.year, query.month).map((item) =>
@@ -2249,7 +2316,17 @@
         return (data.conflictDetail || []).map((item, index) => ({
           id:
             item.currentDetailId ||
-            `${item.calendarDate}-${item.startTime}-${item.endTime}-${index}`,
+            [
+              item.calendarDate,
+              item.startTime,
+              item.endTime,
+              item.currentCalendarType,
+              item.conflictCalendarType,
+              item.conflictCalendarId,
+              item.conflictDetailId,
+              item.conflictMsg || data.msg || '',
+              index
+            ].join('-'),
           calendarDate: item.calendarDate,
           timeRange: `${String(item.startTime || '').slice(0, 5)}-${String(
             item.endTime || ''
@@ -3277,12 +3354,54 @@
         });
       },
       getViewDetails(date) {
-        const calendarIds = this.getCurrentViewCalendars().map(
-          (item) => item.id
-        );
         return this.details.filter(
           (item) =>
-            calendarIds.includes(item.calendarId) && item.calendarDate === date
+            this.isDetailInCurrentView(item) && item.calendarDate === date
+        );
+      },
+      getStatMonthDetails() {
+        const monthStart = dayjs(
+          `${this.viewQuery.year}-${String(this.viewQuery.month).padStart(
+            2,
+            '0'
+          )}-01`
+        );
+        const monthEnd = monthStart.endOf('month');
+        return this.details.filter((item) => {
+          const date = dayjs(item.calendarDate);
+          return (
+            this.isDetailInCurrentView(item) &&
+            date.isValid() &&
+            (date.isSame(monthStart, 'day') || date.isAfter(monthStart)) &&
+            (date.isSame(monthEnd, 'day') || date.isBefore(monthEnd))
+          );
+        });
+      },
+      isDetailInCurrentView(item = {}) {
+        const targetType = Number(this.viewQuery.calendarType);
+        const detailType = Number(item.calendarType);
+        const typeMatched = targetType ? detailType === targetType : true;
+        const calendarIds = new Set(
+          this.getCurrentViewCalendars().map((calendar) => String(calendar.id))
+        );
+        const hasCalendarId =
+          item.calendarId !== undefined &&
+          item.calendarId !== null &&
+          item.calendarId !== '';
+
+        if (hasCalendarId && calendarIds.has(String(item.calendarId))) {
+          return true;
+        }
+        if (item.isViewDetail) {
+          return typeMatched;
+        }
+        return !hasCalendarId && typeMatched;
+      },
+      isDetailScheduled(item = {}) {
+        return (
+          Number(item.scheduleStatus) === 1 ||
+          Number(item.relationPlanCount || 0) > 0 ||
+          this.getDetailRelatedPlans([item]).length > 0
         );
       },
       isWeekend(date) {
@@ -3717,9 +3836,14 @@
       getConflictDataKey() {
         return [
           this.viewQuery.year || '',
-          this.viewQuery.calendarType || ''
+          this.getConflictCheckTypes().join(',')
         ].join('|');
       },
+      getConflictCheckTypes() {
+        return this.calendarTypeOptions
+          .map((item) => Number(item.value))
+          .filter((type) => [1, 2, 3].includes(type));
+      },
       invalidateConflictCache() {
         this.conflictDataKey = '';
       },
@@ -3734,12 +3858,19 @@
           return;
         }
         try {
-          this.conflictCheckPromise = checkAllCalendarConflict({
-            year: this.viewQuery.year,
-            calendarType: this.viewQuery.calendarType || undefined
-          });
-          const data = await this.conflictCheckPromise;
-          this.conflictList = this.normalizeRemoteConflicts(data);
+          const conflictTypes = this.getConflictCheckTypes();
+          this.conflictCheckPromise = Promise.all(
+            conflictTypes.map((calendarType) =>
+              checkAllCalendarConflict({
+                year: this.viewQuery.year,
+                calendarType
+              })
+            )
+          );
+          const list = await this.conflictCheckPromise;
+          this.conflictList = this.uniqueConflicts(
+            list.flatMap((data) => this.normalizeRemoteConflicts(data))
+          );
           this.conflictDataKey = dataKey;
           this.reloadConflictTable(1);
           if (showMessage) {