feat:首页添加质量模块

master
黄伟杰 13 hours ago
parent 6c8eef9771
commit 24b91e495c

@ -0,0 +1,124 @@
<template>
<view>
<view class="mode-switch-btn" @click="showModePopup = true">
<text class="mode-icon"></text>
</view>
<view v-if="showModePopup" class="popup-overlay" @click="showModePopup = false">
<view class="popup-content" @click.stop>
<view
class="popup-item"
:class="{ active: currentMode === 'production' }"
@click="switchMode('production')"
>
<text class="popup-icon">📋</text>
<text class="popup-text">{{ t('dashboard.production') }}</text>
</view>
<view
class="popup-item"
:class="{ active: currentMode === 'quality' }"
@click="switchMode('quality')"
>
<text class="popup-icon">🛡</text>
<text class="popup-text">{{ t('dashboard.quality') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
currentMode: {
type: String,
default: 'production'
}
})
const emit = defineEmits(['modeChange'])
const showModePopup = ref(false)
function switchMode(mode) {
showModePopup.value = false
emit('modeChange', mode)
}
</script>
<style lang="scss" scoped>
.mode-switch-btn {
width: 64rpx;
height: 64rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(26, 58, 92, 0.3);
&:active {
transform: scale(0.95);
}
}
.mode-icon {
font-size: 32rpx;
}
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-start;
justify-content: flex-end;
z-index: 1000;
padding: 200rpx 24rpx 24rpx;
}
.popup-content {
width: 200rpx;
background: linear-gradient(180deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(26, 58, 92, 0.4);
}
.popup-item {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
justify-content: center;
gap: 12rpx;
&:active {
background: rgba(255, 255, 255, 0.1);
}
&.active {
background: rgba(255, 255, 255, 0.2);
}
&:not(:last-child) {
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
}
}
.popup-icon {
font-size: 36rpx;
}
.popup-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
</style>

@ -502,6 +502,7 @@ defineExpose({ loadData })
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
margin-top: 24rpx;
}
.section-header {
@ -613,7 +614,7 @@ defineExpose({ loadData })
.trend-stat-card {
flex-shrink: 0;
width: 164rpx;
width: 152rpx;
background: #f8fafc;
border-radius: 12rpx;
padding: 20rpx 12rpx;

@ -0,0 +1,373 @@
<template>
<view class="quality-section">
<view class="section-header">
<view>
<text class="section-title">{{ t('dashboard.qualityOverview') }}</text>
<picker mode="selector" :range="periodRange" range-key="text" :value="periodIndex" @change="onPeriodChange">
<view class="filter-select">
<text class="filter-text">{{ currentPeriodLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<ModeSwitchPopup :currentMode="currentMode" @modeChange="switchMode" />
</view>
<view class="quality-stats">
<view class="quality-stat-card">
<text class="stat-value">{{ formatNumber(qualityData.totalWangongNumber) }}</text>
<text class="stat-label">{{ t('dashboard.totalWangongNumber') }}</text>
</view>
<view class="quality-stat-card">
<text class="stat-value pass">{{ formatNumber(qualityData.totalPassNumber) }}</text>
<text class="stat-label">{{ t('dashboard.totalPassNumber') }}</text>
</view>
<view class="quality-stat-card">
<text class="stat-value fail">{{ formatNumber(qualityData.totalNoPassNumber) }}</text>
<text class="stat-label">{{ t('dashboard.totalNoPassNumber') }}</text>
</view>
<view class="quality-stat-card">
<text class="stat-value rate">{{ formatPercent(qualityData.totalPassRate) }}</text>
<text class="stat-label">{{ t('dashboard.totalPassRate') }}</text>
</view>
</view>
<view class="chart-section">
<view class="chart-header">
<text class="chart-title">{{ t('dashboard.productPassRateRanking') }}</text>
</view>
<scroll-view class="ranking-scroll" scroll-y :style="{ height: rankingScrollHeight }">
<view :style="{ height: rankingChartHeight }">
<qiun-data-charts type="bar" :chartData="rankingChartData" :canvas2d="false" :opts="rankingChartOpts" />
</view>
</scroll-view>
</view>
<view class="chart-section">
<view class="chart-header">
<text class="chart-title">{{ t('dashboard.qualityTrend') }}</text>
</view>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="trendChartData" :canvas2d="false" :opts="trendChartOpts" />
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { formatNumber, formatPercent } from '@/utils/format'
import ModeSwitchPopup from '@/components/dashboard/ModeSwitchPopup.vue'
const { t } = useI18n()
const emit = defineEmits(['modeChange'])
const currentMode = ref('quality')
const currentPeriod = ref('LAST_7_DAYS')
const isInitialLoad = ref(true)
const periodRange = computed(() => [
{ text: t('dashboard.periodLast7Days'), value: 'LAST_7_DAYS' },
{ text: t('dashboard.periodLastWeek'), value: 'LAST_WEEK' },
{ text: t('dashboard.periodThisWeek'), value: 'THIS_WEEK' },
{ text: t('dashboard.periodLastMonth'), value: 'LAST_MONTH' },
{ text: t('dashboard.periodThisMonth'), value: 'THIS_MONTH' },
{ text: t('dashboard.periodLastYear'), value: 'LAST_YEAR' }
])
const periodIndex = computed(() => {
const idx = periodRange.value.findIndex(item => item.value === currentPeriod.value)
return idx >= 0 ? idx : 0
})
const periodLabelMap = computed(() => ({
LAST_7_DAYS: t('dashboard.periodLast7Days'),
LAST_WEEK: t('dashboard.periodLastWeek'),
THIS_WEEK: t('dashboard.periodThisWeek'),
LAST_MONTH: t('dashboard.periodLastMonth'),
THIS_MONTH: t('dashboard.periodThisMonth'),
LAST_YEAR: t('dashboard.periodLastYear')
}))
const currentPeriodLabel = computed(() => {
return periodLabelMap.value[currentPeriod.value] || t('dashboard.periodLast7Days')
})
const qualityData = reactive({
totalWangongNumber: 0,
totalPassNumber: 0,
totalNoPassNumber: 0,
totalPassRate: 0
})
const rankingChartOpts = {
color: ['#1a3a5c'],
dataLabel: true,
legend: { show: false },
xAxis: { disableGrid: true, max: 100, axisLabel: { padding: [0, 0, 0, 10] } },
yAxis: {
disableGrid: true, axisLabel: {
padding: [0, 10, 0, 0], formatter: function (value) {
if (value.length > 10) {
return value.substring(0, 10) + '...'
}
return value
}
}
},
extra: {
bar: {
type: 'group',
width: 20,
seriesGap: 4,
categoryGap: 4,
barBorderRadius: [4, 4, 0, 0],
linearType: 'custom',
linearOpacity: 0.6,
activeBgColor: '#1a3a5c',
activeBgOpacity: 0.08
}
}
}
const rankingChartData = reactive({
categories: [],
series: [{ name: '', data: [] }]
})
const ITEM_HEIGHT_PX = 30
const MAX_VISIBLE = 6
const rankingScrollHeight = computed(() => {
const count = rankingChartData.categories.length || 1
const visible = Math.min(count, MAX_VISIBLE)
return `${visible * ITEM_HEIGHT_PX + 30}px`
})
const rankingChartHeight = computed(() => {
const count = rankingChartData.categories.length || 1
return `${count * ITEM_HEIGHT_PX + 30}px`
})
const trendChartOpts = {
color: ['#18bc37', '#ff4d4f'],
dataLabel: false,
dataPointShape: false,
legend: { show: true, position: 'bottom' },
xAxis: { disableGrid: true, labelCount: 5 },
yAxis: { gridType: 'dash', dashLength: 2 },
extra: { line: { type: 'straight', width: 1, activeType: 'hollow' } }
}
const trendChartData = reactive({
categories: [],
series: [
{ name: '', data: [] },
{ name: '', data: [] }
]
})
function switchMode(mode) {
currentMode.value = mode
emit('modeChange', mode)
}
function onPeriodChange(e) {
const idx = e.detail.value
const val = periodRange.value[idx]?.value
if (!val) return
currentPeriod.value = val
loadQualityData()
}
async function loadQualityData() {
const res = await request({
url: '/admin-api/mes/plan/quality-overview',
method: 'get',
params: { period: currentPeriod.value },
showLoading: !isInitialLoad.value
})
const data = res?.data || {}
qualityData.totalWangongNumber = data.totalWangongNumber ?? 0
qualityData.totalPassNumber = data.totalPassNumber ?? 0
qualityData.totalNoPassNumber = data.totalNoPassNumber ?? 0
qualityData.totalPassRate = data.totalPassRate ?? 0
const productList = data.productPassRateList || []
const sorted = [...productList].sort((a, b) => {
const va = parseFloat(a.passRate) || 0
const vb = parseFloat(b.passRate) || 0
return vb - va
})
const categories = sorted.map((item) => item.productName || '')
const rateData = sorted.map((item) => {
const v = parseFloat(item.passRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
const total = sorted.length
const colors = sorted.map((_, index) => {
const ratio = total > 1 ? index / (total - 1) : 0
const r = Math.round(26 + ratio * (74 - 26))
const g = Math.round(58 + ratio * (144 - 58))
const b = Math.round(92 + ratio * (194 - 92))
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
})
rankingChartData.categories = categories
rankingChartData.series = [{
name: t('dashboard.passRate'),
data: rateData,
linearColor: colors.map((c) => ['#e8f0f8', c])
}]
const trendList = data.trendList || []
const trendCategories = trendList.map((item) => (item.day || '').substring(5))
const passData = trendList.map((item) => item.passNumber ?? 0)
const noPassData = trendList.map((item) => item.noPassNumber ?? 0)
trendChartData.categories = trendCategories
trendChartData.series = [
{ name: t('dashboard.passNumber'), data: passData },
{ name: t('dashboard.noPassNumber'), data: noPassData }
]
}
onMounted(async () => {
await loadQualityData()
isInitialLoad.value = false
})
defineExpose({ loadQualityData })
</script>
<style lang="scss" scoped>
.quality-section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
gap: 16rpx;
view {
display: flex;
align-items: center;
gap: 20rpx;
}
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
flex-shrink: 0;
}
.filter-select {
display: flex;
align-items: center;
padding: 12rpx 24rpx;
background: #ffffff;
border-radius: 12rpx;
margin-right: 20rpx;
&:active {
background: #e8ecf0;
}
}
.filter-text {
font-size: 26rpx;
color: #1a3a5c;
font-weight: 500;
margin-right: 8rpx;
}
.filter-arrow {
font-size: 20rpx;
color: #999999;
}
.quality-stats {
display: flex;
justify-content: space-between;
margin-bottom: 24rpx;
}
.quality-stat-card {
flex: 1;
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx 12rpx;
margin: 0 6rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.stat-value {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #1a3a5c;
margin-bottom: 8rpx;
&.pass {
color: #18bc37;
}
&.fail {
color: #ff4d4f;
}
&.rate {
color: #4a90c2;
}
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666666;
word-break: break-all;
}
.chart-section {
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
margin-bottom: 24rpx;
}
.chart-header {
margin-bottom: 20rpx;
}
.chart-title {
font-size: 28rpx;
font-weight: 500;
color: #1a3a5c;
}
.ranking-scroll {
width: 100%;
min-width: 100%;
}
.chart-box {
width: 100%;
height: 450rpx;
min-width: 100%;
}
</style>

@ -1,6 +1,10 @@
<template>
<view class="stats-section">
<view class="section-title">{{ t('dashboard.productionOverview') }}</view>
<view class="section-header">
<text class="section-title">{{ currentMode === 'production' ? t('dashboard.productionOverview') : t('dashboard.qualityOverview') }}</text>
<ModeSwitchPopup :currentMode="currentMode" @modeChange="switchMode" />
</view>
<scroll-view scroll-x enable-flex class="stats-scroll">
<view class="stats-grid">
<view
@ -14,6 +18,9 @@
</view>
</view>
</scroll-view>
<PlanSection />
<DeviceSection />
</view>
</template>
@ -22,9 +29,16 @@ import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { formatNumber } from '@/utils/format'
import PlanSection from '@/components/dashboard/PlanSection.vue'
import DeviceSection from '@/components/dashboard/DeviceSection.vue'
import ModeSwitchPopup from '@/components/dashboard/ModeSwitchPopup.vue'
const { t } = useI18n()
const emit = defineEmits(['modeChange'])
const currentMode = ref('production')
const COLOR_PALETTE = [
{ border: '#1a3a5c', value: '#1a3a5c' },
{ border: '#ff8c00', value: '#ff8c00' },
@ -58,6 +72,11 @@ async function loadProductionStats() {
}))
}
function switchMode(mode) {
currentMode.value = mode
emit('modeChange', mode)
}
onMounted(() => {
loadProductionStats()
})
@ -70,11 +89,17 @@ defineExpose({ loadProductionStats })
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.stats-scroll {

@ -1,8 +1,8 @@
// 应用全局配置
const config = {
// baseUrl: 'http://47.106.185.127:48080',127.0.0.1
baseUrl: 'https://besure.ngsk.tech:7001',
// baseUrl: 'http://192.168.5.167:48081',
// baseUrl: 'https://besure.ngsk.tech:7001',
baseUrl: 'http://192.168.5.167:48081',
// baseUrl: '',
// 应用信息
appInfo: {

@ -31,7 +31,28 @@ export default {
subtitle: 'Besure Digital Intelligent Control Platform',
functionNav: 'Function Navigation',
productionOverview: 'Production Overview',
qualityOverview: 'Quality Overview',
productionPlan: 'Production Summary',
production: 'Production',
quality: 'Quality',
totalCount: 'Total',
passCount: 'Pass Count',
failCount: 'Fail Count',
qualityTrend: 'Quality Trend',
chartPlaceholder: 'Chart loading...',
totalWangongNumber: 'Total Reported',
totalPassNumber: 'Total Passed',
totalNoPassNumber: 'Total Failed',
totalPassRate: 'Total Pass Rate',
productPassRateRanking: 'Product Pass Rate Ranking',
passNumber: 'Pass Count',
noPassNumber: 'Fail Count',
periodLastWeek: 'Last Week',
periodThisWeek: 'This Week',
periodLast7Days: 'Last 7 Days',
periodLastMonth: 'Last Month',
periodThisMonth: 'This Month',
periodLastYear: 'Last Year',
collapseList: 'Collapse',
viewMore: 'View More ',
productName: 'Product',
@ -110,6 +131,7 @@ export default {
periodLast7Days: 'Last 7 Days',
periodLastMonth: 'Last Month',
periodThisMonth: 'This Month',
periodLastYear: 'Last Year',
utilizationRanking: 'Last 7 Days Utilization Ranking',
utilizationRateTrend: 'Utilization Rate',
deviceRateTrend: 'Single Device 7-Day Utilization/Boot Rate Trend',

@ -31,7 +31,28 @@ export default {
subtitle: '必硕数字化智能中控平台',
functionNav: '功能导航',
productionOverview: '生产整体概况',
qualityOverview: '质量概况',
productionPlan: '生产概括',
production: '生产',
quality: '质量',
totalCount: '总数',
passCount: '合格数',
failCount: '不合格数',
qualityTrend: '质量趋势',
chartPlaceholder: '图表加载中...',
totalWangongNumber: '报工总数',
totalPassNumber: '合格总数',
totalNoPassNumber: '不合格总数',
totalPassRate: '总合格率',
productPassRateRanking: '产品合格率排行',
passNumber: '合格数',
noPassNumber: '不合格数',
periodLastWeek: '上周',
periodThisWeek: '本周',
periodLast7Days: '近7日',
periodLastMonth: '上月',
periodThisMonth: '本月',
periodLastYear: '近一年',
collapseList: '收起列表',
viewMore: '查看更多 ',
productName: '产品名称',
@ -110,6 +131,7 @@ export default {
periodLast7Days: '近7日',
periodLastMonth: '上月',
periodThisMonth: '本月',
periodLastYear: '近一年',
utilizationRanking: '近7日平均稼动率排名',
utilizationRateTrend: '稼动率',
deviceRateTrend: '单设备近7日稼动率/开机率趋势',

@ -5,12 +5,9 @@
<view class="content-section">
<!-- 功能导航 -->
<NavSection />
<!-- 生产概览统计 -->
<StatsSection />
<!-- 生产概括 -->
<PlanSection />
<!-- 设备概括 -->
<DeviceSection />
<!-- 生产/质量概览统计 -->
<StatsSection v-if="currentMode === 'production'" @mode-change="onModeChange" />
<QualitySection v-else @mode-change="onModeChange" />
</view>
</scroll-view>
@ -27,10 +24,14 @@ import { onLocaleChange, offLocaleChange, setNavigationTitle } from '@/locales'
import BannerSection from '@/components/dashboard/BannerSection.vue'
import NavSection from '@/components/dashboard/NavSection.vue'
import StatsSection from '@/components/dashboard/StatsSection.vue'
import PlanSection from '@/components/dashboard/PlanSection.vue'
import DeviceSection from '@/components/dashboard/DeviceSection.vue'
import QualitySection from '@/components/dashboard/QualitySection.vue'
const scrollTop = ref(0)
const currentMode = ref('production')
function onModeChange(mode) {
currentMode.value = mode
}
const currentScrollTop = ref(0)
const showGoTop = ref(false)

@ -3,9 +3,8 @@ import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig(() => {
return {
base: './',
base: '/',
build: {
minify: true,
outDir: 'dist',
},
server: {

Loading…
Cancel
Save