| | |
| | | <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.typeText }}</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> |
| | | <!-- 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> |
| | | </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 { qualityUnqualifiedListPage } from '@/api/qualityManagement/nonconformingManagement.js' |
| | | 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 warnings = ref([ |
| | | { id: '1', type: 'raw', typeText: '原材料', title: '关于企业原材料调整通知', date: '2024.08.24' }, |
| | | { id: '2', type: 'raw', typeText: '原材料', title: '关于原材料消耗方案建设的通知', date: '2024.08.24' }, |
| | | { id: '3', type: 'final', typeText: '成品', title: '成品工作台系统维护计划安排', date: '2024.08.24' }, |
| | | { id: '4', type: 'final', typeText: '成品', title: '成品工作台系统维护计划安排', date: '2024.08.24' }, |
| | | { id: '5', type: 'semi', typeText: '半成品', title: 'HRM系统安全升级公告:加强访问控制…', date: '2024.08.24' }, |
| | | { id: '6', type: 'semi', typeText: '半成品', title: 'HRM系统安全升级公告:加强访问控制…', date: '2024.08.24' }, |
| | | ]) |
| | | const loading = ref(false) |
| | | const warnings = ref([]) |
| | | |
| | | const TAG_COLORS = { |
| | | raw: '#7C4DFF', |
| | | final: '#F5A000', |
| | | semi: '#FF66CC', |
| | | } |
| | | // 占比数据 |
| | | const ratios = ref({ |
| | | rawMaterialRatio: 0, |
| | | semiFinishedProductRatio: 0, |
| | | finishedProductRatio: 0, |
| | | }) |
| | | |
| | | const tagClass = (type) => { |
| | | if (type === 'raw') return 'tag-raw' |
| | |
| | | return 'tag-semi' |
| | | } |
| | | |
| | | const pieChartStyle = { width: '100%', height: '100%' } |
| | | |
| | | const pieOptions = { |
| | | backgroundColor: 'transparent', |
| | | textStyle: { color: '#B8C8E0' }, |
| | | // 根据productTitle映射类型 |
| | | const mapProductTitleToType = (productTitle) => { |
| | | if (productTitle === '原材料') return 'raw' |
| | | if (productTitle === '半成品') return 'semi' |
| | | if (productTitle === '成品') return 'final' |
| | | return 'raw' // 默认值 |
| | | } |
| | | |
| | | const pieTooltip = { |
| | | trigger: 'item', |
| | | formatter: (p) => `${p.name}:${p.value}`, |
| | | } |
| | | |
| | | const pieData = computed(() => { |
| | | const counts = { raw: 0, final: 0, semi: 0 } |
| | | warnings.value.forEach((w) => { |
| | | const key = w.type in counts ? w.type : 'raw' |
| | | counts[key] += 1 |
| | | }) |
| | | return [ |
| | | { name: '原材料', value: counts.raw, itemStyle: { color: TAG_COLORS.raw } }, |
| | | { name: '半成品', value: counts.semi, itemStyle: { color: TAG_COLORS.semi } }, |
| | | { name: '成品', value: counts.final, 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 qualityUnqualifiedListPage({ pageNum: 1, pageSize: 6 }) |
| | | const rows = res?.rows || res?.data?.rows || res?.data || [] |
| | | if (!Array.isArray(rows) || rows.length === 0) return |
| | | const res = await nonComplianceWarning() |
| | | if (res?.code === 200 && res?.data) { |
| | | const data = res.data |
| | | |
| | | warnings.value = rows.slice(0, 6).map((r, idx) => { |
| | | const typeCode = r.inspectType ?? r.modelType ?? r.type |
| | | const mappedType = typeCode === 0 || typeCode === '0' ? 'raw' : typeCode === 1 || typeCode === '1' ? 'semi' : 'final' |
| | | const title = r.title || r.unqualifiedTitle || r.remark || r.unqualifiedReason || '不合格预警' |
| | | const date = (r.warningTime || r.createTime || r.updateTime || '').slice(0, 10).replace(/-/g, '.') || '2024.08.24' |
| | | return { |
| | | id: r.id ?? r.unqualifiedId ?? `${idx}`, |
| | | type: mappedType, |
| | | typeText: mappedType === 'raw' ? '原材料' : mappedType === 'semi' ? '半成品' : '成品', |
| | | title, |
| | | date, |
| | | // 更新占比数据 |
| | | ratios.value = { |
| | | rawMaterialRatio: data.rawMaterialRatio ?? 0, |
| | | semiFinishedProductRatio: data.semiFinishedProductRatio ?? 0, |
| | | finishedProductRatio: data.finishedProductRatio ?? 0, |
| | | } |
| | | }) |
| | | |
| | | // 更新警告列表 |
| | | const children = data.children || [] |
| | | warnings.value = children.map((item, idx) => { |
| | | const type = mapProductTitleToType(item.parentProductTitle) |
| | | const date = item.date ? item.date.replace(/-/g, '.') : '' |
| | | return { |
| | | id: item.id ?? `warning-${idx}`, |
| | | type, |
| | | parentProductTitle: item.parentProductTitle || '原材料', |
| | | productTitle: item.productTitle || '原材料', |
| | | title: item.description || '不合格预警', |
| | | date, |
| | | } |
| | | }) |
| | | } else { |
| | | warnings.value = [] |
| | | } |
| | | } catch (e) { |
| | | // 接口失败则保持 mock |
| | | warnings.value = [] |
| | | console.error('获取不合格预警失败:', e) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | const openWarning = (item) => { |
| | | const title = `【${item.typeText}】${item.title}` |
| | | const content = `${title}时间:${item.date}` |
| | | const title = `【${item.parentProductTitle}-${item.productTitle}】${item.title}` |
| | | if (proxy?.$modal?.alert) { |
| | | proxy.$modal.alert(content) |
| | | 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; |
| | | } |
| | | |
| | |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | border-bottom: 1px solid; |
| | | border-image: linear-gradient( |
| | | 270deg, |
| | | border-image: linear-gradient(270deg, |
| | | rgba(0, 126, 255, 0) 0%, |
| | | rgba(0, 126, 255, 0.4549) 35%, |
| | | #007eff 78%, |
| | | #007eff 100% |
| | | ) |
| | | 1; |
| | | #007eff 100%) 1; |
| | | padding: 10px 0 6px; |
| | | } |
| | | |
| | |
| | | |
| | | .warn-item { |
| | | display: grid; |
| | | grid-template-columns: 88px 1fr auto 110px; |
| | | grid-template-columns: 130px 1fr auto 110px; |
| | | align-items: center; |
| | | gap: 12px; |
| | | color: #b8c8e0; |
| | | font-size: 14px; |
| | | line-height: 1; |
| | | padding: 6px 0; |
| | | line-height: 1.2; |
| | | padding: 8px 0; |
| | | border-radius: 4px; |
| | | transition: background-color 0.2s, color 0.2s; |
| | | } |
| | |
| | | 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> |