| | |
| | | <template> |
| | | <div> |
| | | <PanelHeader title="不合格预警" /> |
| | | <div class="warn-panel"> |
| | | <div class="warn-header"> |
| | | <div class="warn-header-left"> |
| | | <div class="warn-badge"></div> |
| | | <span class="warn-title">不合格预警</span> |
| | | </div> |
| | | <div class="warn-range" @click="handleRangeClick">近7天</div> |
| | | </div> |
| | | |
| | | <div class="warn-body"> |
| | | <div class="warn-list" role="list"> |
| | | <div v-for="item in warnings" :key="item.id" class="warn-item" role="listitem" @click="openWarning(item)"> |
| | | <div class="warn-tag" :class="tagClass(item.type)">{{ item.parentProductTitle }}-{{ item.productTitle }} |
| | | <!-- loading:展示骨架架子 --> |
| | | <template v-if="loading"> |
| | | <div v-for="n in 6" :key="`skeleton-${n}`" class="warn-item skeleton" role="listitem"> |
| | | <div class="warn-tag skeleton-block"></div> |
| | | <div class="warn-text skeleton-block"></div> |
| | | <div class="warn-action skeleton-block"></div> |
| | | <div class="warn-date skeleton-block"></div> |
| | | </div> |
| | | <div class="warn-text" :title="item.title">{{ item.title }}</div> |
| | | <div class="warn-action" @click.stop="openWarning(item)">查看</div> |
| | | <div class="warn-date">{{ item.date }}</div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- 空数据:先展示架子 + 空状态 --> |
| | | <template v-else-if="warnings.length === 0"> |
| | | <div v-for="n in 4" :key="`empty-row-${n}`" class="warn-item is-empty" role="listitem"> |
| | | <div class="warn-tag tag-empty">—</div> |
| | | <div class="warn-text empty-text">暂无预警信息</div> |
| | | <div class="warn-action empty-action">查看</div> |
| | | <div class="warn-date empty-date">--</div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- 有数据:正常渲染 --> |
| | | <template v-else> |
| | | <div |
| | | v-for="item in warnings" |
| | | :key="item.id" |
| | | class="warn-item" |
| | | role="listitem" |
| | | @click="openWarning(item)" |
| | | > |
| | | <div class="warn-tag" :class="tagClass(item.type)"> |
| | | {{ item.parentProductTitle }}-{{ item.productTitle }} |
| | | </div> |
| | | <div class="warn-text" :title="item.title">{{ item.title }}</div> |
| | | <div class="warn-action" @click.stop="openWarning(item)">查看</div> |
| | | <div class="warn-date">{{ item.date }}</div> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, ref, onMounted } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { getCurrentInstance, ref, onMounted } from 'vue' |
| | | import { nonComplianceWarning } from '@/api/viewIndex.js' |
| | | import PanelHeader from "@/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue"; |
| | | |
| | | const { proxy } = getCurrentInstance() || {} |
| | | |
| | | const loading = ref(false) |
| | | const warnings = ref([]) |
| | | |
| | | // 占比数据 |
| | |
| | | semiFinishedProductRatio: 0, |
| | | finishedProductRatio: 0, |
| | | }) |
| | | |
| | | const TAG_COLORS = { |
| | | raw: '#7C4DFF', |
| | | final: '#F5A000', |
| | | semi: '#FF66CC', |
| | | } |
| | | |
| | | const tagClass = (type) => { |
| | | if (type === 'raw') return 'tag-raw' |
| | |
| | | return 'raw' // 默认值 |
| | | } |
| | | |
| | | const pieChartStyle = { width: '100%', height: '100%' } |
| | | |
| | | const pieOptions = { |
| | | backgroundColor: 'transparent', |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | const pieTooltip = { |
| | | trigger: 'item', |
| | | formatter: (p) => `${p.name}:${p.value}%`, |
| | | } |
| | | |
| | | const pieData = computed(() => { |
| | | return [ |
| | | { name: '原材料', value: ratios.value.rawMaterialRatio, itemStyle: { color: TAG_COLORS.raw } }, |
| | | { name: '半成品', value: ratios.value.semiFinishedProductRatio, itemStyle: { color: TAG_COLORS.semi } }, |
| | | { name: '成品', value: ratios.value.finishedProductRatio, itemStyle: { color: TAG_COLORS.final } }, |
| | | ] |
| | | }) |
| | | |
| | | const pieSeries = computed(() => { |
| | | return [ |
| | | { |
| | | type: 'pie', |
| | | radius: ['0%', '68%'], |
| | | center: ['50%', '50%'], |
| | | startAngle: 90, |
| | | clockwise: true, |
| | | avoidLabelOverlap: true, |
| | | label: { show: false }, |
| | | labelLine: { show: false }, |
| | | itemStyle: { |
| | | borderColor: '#071a3a', |
| | | borderWidth: 4, |
| | | shadowBlur: 14, |
| | | shadowColor: 'rgba(0, 0, 0, 0.35)', |
| | | }, |
| | | data: pieData.value, |
| | | }, |
| | | { |
| | | // 内圈暗环,增强层次 |
| | | type: 'pie', |
| | | radius: ['70%', '74%'], |
| | | center: ['50%', '50%'], |
| | | silent: true, |
| | | label: { show: false }, |
| | | labelLine: { show: false }, |
| | | itemStyle: { color: 'rgba(78, 228, 255, 0.12)' }, |
| | | data: [1], |
| | | }, |
| | | ] |
| | | }) |
| | | |
| | | const fetchWarnings = async () => { |
| | | loading.value = true |
| | | try { |
| | | const res = await nonComplianceWarning() |
| | | if (res?.code === 200 && res?.data) { |
| | |
| | | date, |
| | | } |
| | | }) |
| | | } else { |
| | | warnings.value = [] |
| | | } |
| | | } catch (e) { |
| | | // 接口失败则保持空数据 |
| | | warnings.value = [] |
| | | console.error('获取不合格预警失败:', e) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | |
| | | proxy.$modal.alert(title) |
| | | return |
| | | } |
| | | // 兜底:没有全局 modal 时用 console |
| | | console.log('warning:', { ...item }) |
| | | } |
| | | |
| | | const handleRangeClick = () => { |
| | | // 先按截图做静态“近7天”,后续有真实筛选需求再接入 |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | height: 100%; |
| | | height: 90%; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | |
| | | opacity: 0.35; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .warn-empty { |
| | | padding: 10px 0 0; |
| | | } |
| | | |
| | | .warn-item.is-empty { |
| | | opacity: 0.9; |
| | | cursor: default; |
| | | } |
| | | |
| | | .tag-empty { |
| | | background: rgba(184, 200, 224, 0.22); |
| | | color: rgba(184, 200, 224, 0.85); |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .empty-text { |
| | | color: rgba(232, 241, 255, 0.75); |
| | | } |
| | | |
| | | .empty-action { |
| | | color: rgba(255, 77, 79, 0.55); |
| | | } |
| | | |
| | | .empty-date { |
| | | color: rgba(184, 200, 224, 0.45); |
| | | } |
| | | |
| | | /* skeleton */ |
| | | .warn-item.skeleton { |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .skeleton-block { |
| | | height: 18px; |
| | | border-radius: 4px; |
| | | background: linear-gradient(90deg, rgba(184, 200, 224, 0.08) 25%, rgba(184, 200, 224, 0.18) 37%, rgba(184, 200, 224, 0.08) 63%); |
| | | background-size: 400% 100%; |
| | | animation: shimmer 1.2s ease-in-out infinite; |
| | | } |
| | | |
| | | .warn-item.skeleton .warn-tag.skeleton-block { |
| | | height: 28px; |
| | | } |
| | | |
| | | @keyframes shimmer { |
| | | 0% { background-position: 100% 0; } |
| | | 100% { background-position: -100% 0; } |
| | | } |
| | | </style> |