perf:首页-组件化
parent
b1e6d3af72
commit
e41283295b
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<view class="nav-section">
|
||||
<view class="section-title">{{ t('dashboard.functionNav') }}</view>
|
||||
<view class="nav-grid">
|
||||
<view v-for="(item, index) in navList" :key="index" class="nav-item" @click="handleNavClick(item)">
|
||||
<view class="nav-icon" :style="{ backgroundColor: item.bgColor }">
|
||||
<text class="nav-icon-text">{{ item.icon }}</text>
|
||||
</view>
|
||||
<text class="nav-text">{{ t(`dashboard.${item.key}`) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const navList = reactive([
|
||||
{ key: 'mold', icon: '🔧', bgColor: '#1a3a5c', path: '/pages_function/mold' },
|
||||
{ key: 'equipment', icon: '⚙️', bgColor: '#2d5a87', path: '/pages_function/equipment' },
|
||||
{ key: 'keypart', icon: '🔩', bgColor: '#3d7ab5', path: '/pages_function/keypart' },
|
||||
{ key: 'spare', icon: '📦', bgColor: '#4a90c2', path: '/pages_function/spare' },
|
||||
{ key: 'product', icon: '🧾', bgColor: '#5aa0d2', path: '/pages_function/product' }
|
||||
])
|
||||
|
||||
function handleNavClick(item) {
|
||||
const navMap = {
|
||||
mold: '/pages_function/pages/mold/index',
|
||||
equipment: '/pages_function/pages/equipment/index',
|
||||
spare: '/pages_function/pages/spare/index',
|
||||
keypart: '/pages_function/pages/keypart/index',
|
||||
product: '/pages_function/pages/product/index'
|
||||
}
|
||||
const url = navMap[item.key]
|
||||
if (url) {
|
||||
uni.navigateTo({ url })
|
||||
} else {
|
||||
uni.showToast({ title: t('common.moduleBuilding'), icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-section {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.nav-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.nav-icon-text {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,675 @@
|
||||
<template>
|
||||
<view class="plan-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ t('dashboard.productionPlan') }}</text>
|
||||
<text class="section-more" @click="viewMore">
|
||||
{{ t('dashboard.viewMore') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="filter-bar">
|
||||
<view class="filter-select" @click="showFilterPicker = true">
|
||||
<text class="filter-text">{{ currentFilterLabel }}</text>
|
||||
<text class="filter-arrow">▼</text>
|
||||
</view>
|
||||
<view class="filter-select" @click="showRangePicker = true">
|
||||
<text class="filter-text">{{ currentRangeLabel }}</text>
|
||||
<text class="filter-arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="currentRange === 'custom'" class="filter-date-wrap">
|
||||
<view class="date-picker-item">
|
||||
<up-icon name="calendar" size="18" color="#999" />
|
||||
<up-datetime-picker ref="startPickerRef" hasInput v-model="dateRange.start" mode="datetime"
|
||||
:placeholder="t('dashboard.startDate')" :formatter="datetimeFormatter" closeOnClickOverlay
|
||||
@confirm="onStartDateConfirm" @close="onPickerClose" @cancel="onPickerClose" />
|
||||
</view>
|
||||
<view class="date-picker-item">
|
||||
<up-icon name="calendar" size="18" color="#999" />
|
||||
<up-datetime-picker ref="endPickerRef" hasInput v-model="dateRange.end" mode="datetime"
|
||||
:placeholder="t('dashboard.endDate')" :formatter="datetimeFormatter" closeOnClickOverlay
|
||||
@confirm="onEndDateConfirm" @close="onPickerClose" @cancel="onPickerClose" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="currentFilter === 'product'" class="trend-content">
|
||||
<view class="trend-stats">
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value">{{ formatNumber(trendData.baogongNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.baogongNum') }}</text>
|
||||
</view>
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value pass">{{ formatNumber(trendData.passNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.passNum') }}</text>
|
||||
</view>
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value fail">{{ formatNumber(trendData.noPassNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.noPassNum') }}</text>
|
||||
</view>
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value rate">{{ formatPercent(trendData.passRate) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.passRate') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="currentRange !== 'custom'" class="trend-chart">
|
||||
<text class="chart-title">{{ t('dashboard.baogongNum') }}</text>
|
||||
<view class="chart-box">
|
||||
<qiun-data-charts type="line" :chartData="baogongChartData" :canvas2d="false"
|
||||
:opts="chartOpts" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="currentRange !== 'custom'" class="trend-chart">
|
||||
<text class="chart-title">{{ t('dashboard.passRate') }}</text>
|
||||
<view class="chart-box">
|
||||
<qiun-data-charts type="line" :chartData="passRateChartData" :canvas2d="false"
|
||||
:opts="passRateChartOpts" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="trend-content">
|
||||
<view class="trend-stats">
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value">{{ formatNumber(taskTrendData.totalNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.totalTask') }}</text>
|
||||
</view>
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value pending">{{ formatNumber(taskTrendData.waitingProductionNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.waitingProduction') }}</text>
|
||||
</view>
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value running">{{ formatNumber(taskTrendData.producingNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.producing') }}</text>
|
||||
</view>
|
||||
<view class="trend-stat-card">
|
||||
<text class="trend-stat-value pass">{{ formatNumber(taskTrendData.completedNum) }}</text>
|
||||
<text class="trend-stat-label">{{ t('dashboard.completed') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="currentRange !== 'custom'" class="trend-chart">
|
||||
<text class="chart-title">{{ t('dashboard.taskTrend') }}</text>
|
||||
<view class="chart-box">
|
||||
<qiun-data-charts type="line" :chartData="taskChartData" :canvas2d="false"
|
||||
:opts="chartOpts" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<up-picker :show="showFilterPicker" :columns="filterColumns" @confirm="onFilterConfirm"
|
||||
@cancel="showFilterPicker = false" @close="showFilterPicker = false" closeOnClickOverlay />
|
||||
|
||||
<up-picker :show="showRangePicker" :columns="rangeColumns" @confirm="onRangeConfirm"
|
||||
@cancel="showRangePicker = false" @close="showRangePicker = false" closeOnClickOverlay />
|
||||
</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'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const chartOpts = {
|
||||
color: ['#1a3a5c'],
|
||||
dataLabel: false,
|
||||
legend: { show: false },
|
||||
xAxis: { disableGrid: true, labelCount: 4 },
|
||||
yAxis: { gridType: 'dash', dashLength: 2 },
|
||||
extra: { line: { type: 'straight', width: 2, activeType: 'hollow' } }
|
||||
}
|
||||
|
||||
const passRateChartOpts = {
|
||||
color: ['#1a3a5c'],
|
||||
dataLabel: false,
|
||||
legend: { show: false },
|
||||
xAxis: { disableGrid: true, labelCount: 4 },
|
||||
yAxis: { gridType: 'dash', dashLength: 2, data: [{ min: 0, max: 100 }] },
|
||||
extra: { line: { type: 'straight', width: 2, activeType: 'hollow' } }
|
||||
}
|
||||
|
||||
const showFilterPicker = ref(false)
|
||||
const showRangePicker = ref(false)
|
||||
const startPickerRef = ref(null)
|
||||
const endPickerRef = ref(null)
|
||||
const currentFilter = ref('task')
|
||||
const currentRange = ref('year')
|
||||
|
||||
const filterColumns = computed(() => [
|
||||
[
|
||||
{ text: t('dashboard.filterTask'), value: 'task' },
|
||||
{ text: t('dashboard.filterProduct'), value: 'product' }
|
||||
]
|
||||
])
|
||||
|
||||
const currentFilterLabel = computed(() => {
|
||||
return currentFilter.value === 'task' ? t('dashboard.filterTask') : t('dashboard.filterProduct')
|
||||
})
|
||||
|
||||
const rangeColumns = computed(() => [
|
||||
[
|
||||
{ text: t('dashboard.rangeYear'), value: 'year' },
|
||||
{ text: t('dashboard.rangeMonth'), value: 'month' },
|
||||
{ text: t('dashboard.rangeWeek'), value: 'week' },
|
||||
{ text: t('dashboard.rangeToday'), value: 'today' },
|
||||
{ text: t('dashboard.rangeCustom'), value: 'custom' }
|
||||
]
|
||||
])
|
||||
|
||||
const rangeLabelMap = computed(() => ({
|
||||
year: t('dashboard.rangeYear'),
|
||||
month: t('dashboard.rangeMonth'),
|
||||
week: t('dashboard.rangeWeek'),
|
||||
today: t('dashboard.rangeToday'),
|
||||
custom: t('dashboard.rangeCustom')
|
||||
}))
|
||||
|
||||
const currentRangeLabel = computed(() => {
|
||||
return rangeLabelMap.value[currentRange.value] || t('dashboard.rangeMonth')
|
||||
})
|
||||
|
||||
function getTodayZero() {
|
||||
const now = new Date()
|
||||
const pad2 = (n) => String(n).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())} 00:00:00`
|
||||
}
|
||||
|
||||
const dateRange = reactive({ start: getTodayZero(), end: getTodayZero() })
|
||||
|
||||
const trendData = reactive({
|
||||
baogongNum: 0,
|
||||
passNum: 0,
|
||||
noPassNum: 0,
|
||||
passRate: 0
|
||||
})
|
||||
|
||||
const taskTrendData = reactive({
|
||||
totalNum: 0,
|
||||
issuedNum: 0,
|
||||
partialScheduledNum: 0,
|
||||
waitingProductionNum: 0,
|
||||
producingNum: 0,
|
||||
completedNum: 0
|
||||
})
|
||||
|
||||
const baogongChartData = reactive({
|
||||
categories: [],
|
||||
series: [{ name: t('dashboard.baogongNum'), data: [] }]
|
||||
})
|
||||
|
||||
const passRateChartData = reactive({
|
||||
categories: [],
|
||||
series: [{ name: t('dashboard.passRate'), data: [] }]
|
||||
})
|
||||
|
||||
const taskChartData = reactive({
|
||||
categories: [],
|
||||
series: [{ name: t('dashboard.taskTrend'), data: [] }]
|
||||
})
|
||||
|
||||
const weekdayKeys = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
|
||||
|
||||
function getDateRange(type) {
|
||||
const now = new Date()
|
||||
const pad2 = (n) => String(n).padStart(2, '0')
|
||||
const fmt = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
||||
const end = fmt(now)
|
||||
|
||||
if (type === 'year') {
|
||||
const start = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
|
||||
return { start: fmt(start), end }
|
||||
}
|
||||
if (type === 'month') {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
return { start: fmt(start), end }
|
||||
}
|
||||
if (type === 'week') {
|
||||
const day = now.getDay() || 7
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - day + 1)
|
||||
return { start: fmt(start), end }
|
||||
}
|
||||
if (type === 'today') {
|
||||
return { start: end, end }
|
||||
}
|
||||
return { start: '', end: '' }
|
||||
}
|
||||
|
||||
function transformChartData(dayTrend, rangeType) {
|
||||
if (!dayTrend || dayTrend.length === 0) {
|
||||
return { categories: [], baogongData: [], passRateData: [] }
|
||||
}
|
||||
|
||||
if (rangeType === 'year') {
|
||||
const monthMap = new Map()
|
||||
dayTrend.forEach((item) => {
|
||||
const m = item.day ? item.day.substring(0, 7) : ''
|
||||
if (!m) return
|
||||
if (!monthMap.has(m)) {
|
||||
monthMap.set(m, { baogongNum: 0, passRateSum: 0, count: 0 })
|
||||
}
|
||||
const entry = monthMap.get(m)
|
||||
entry.baogongNum += item.baogongNum ?? 0
|
||||
entry.passRateSum += item.passRate ?? 0
|
||||
entry.count += 1
|
||||
})
|
||||
const categories = []
|
||||
const baogongData = []
|
||||
const passRateData = []
|
||||
monthMap.forEach((val, key) => {
|
||||
categories.push(key)
|
||||
baogongData.push(val.baogongNum)
|
||||
passRateData.push(val.count > 0 ? Math.round((val.passRateSum / val.count) * 100) / 100 : 0)
|
||||
})
|
||||
return { categories, baogongData, passRateData }
|
||||
}
|
||||
|
||||
if (rangeType === 'week') {
|
||||
const categories = dayTrend.map((item) => {
|
||||
const d = new Date(item.day)
|
||||
const dayIdx = d.getDay()
|
||||
return t(`dashboard.${weekdayKeys[dayIdx]}`)
|
||||
})
|
||||
return {
|
||||
categories,
|
||||
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
|
||||
passRateData: dayTrend.map((item) => item.passRate ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (rangeType === 'today') {
|
||||
const categories = dayTrend.map((item) => (item.day || '').substring(5))
|
||||
return {
|
||||
categories,
|
||||
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
|
||||
passRateData: dayTrend.map((item) => item.passRate ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
const categories = dayTrend.map((item) => (item.day || '').substring(5))
|
||||
return {
|
||||
categories,
|
||||
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
|
||||
passRateData: dayTrend.map((item) => item.passRate ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimestamp(ts) {
|
||||
const d = new Date(ts)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
function datetimeFormatter(type, value) {
|
||||
const unitMap = { year: '年', month: '月', day: '日', hour: '时', minute: '分' }
|
||||
return value + (unitMap[type] || '')
|
||||
}
|
||||
|
||||
function onFilterConfirm(e) {
|
||||
const val = e.value[0]?.value || e.value[0]
|
||||
currentFilter.value = val
|
||||
showFilterPicker.value = false
|
||||
if (val === 'product') {
|
||||
loadTrendData()
|
||||
} else if (val === 'task') {
|
||||
loadTaskTrendData()
|
||||
}
|
||||
}
|
||||
|
||||
function onRangeConfirm(e) {
|
||||
const val = e.value[0]?.value || e.value[0]
|
||||
currentRange.value = val
|
||||
showRangePicker.value = false
|
||||
if (val !== 'custom') {
|
||||
const range = getDateRange(val)
|
||||
dateRange.start = range.start
|
||||
dateRange.end = range.end
|
||||
if (currentFilter.value === 'product') {
|
||||
loadTrendData()
|
||||
} else if (currentFilter.value === 'task') {
|
||||
loadTaskTrendData()
|
||||
}
|
||||
} else {
|
||||
dateRange.start = getTodayZero()
|
||||
dateRange.end = getTodayZero()
|
||||
clearChartData()
|
||||
}
|
||||
}
|
||||
|
||||
function clearChartData() {
|
||||
trendData.baogongNum = 0
|
||||
trendData.passNum = 0
|
||||
trendData.noPassNum = 0
|
||||
trendData.passRate = 0
|
||||
baogongChartData.categories = []
|
||||
baogongChartData.series = [{ name: t('dashboard.baogongNum'), data: [] }]
|
||||
passRateChartData.categories = []
|
||||
passRateChartData.series = [{ name: t('dashboard.passRate'), data: [] }]
|
||||
|
||||
taskTrendData.totalNum = 0
|
||||
taskTrendData.issuedNum = 0
|
||||
taskTrendData.partialScheduledNum = 0
|
||||
taskTrendData.waitingProductionNum = 0
|
||||
taskTrendData.producingNum = 0
|
||||
taskTrendData.completedNum = 0
|
||||
taskChartData.categories = []
|
||||
taskChartData.series = [{ name: t('dashboard.taskTrend'), data: [] }]
|
||||
}
|
||||
|
||||
function onStartDateConfirm(e) {
|
||||
dateRange.start = formatTimestamp(e.value)
|
||||
if (dateRange.start && dateRange.end) {
|
||||
if (currentFilter.value === 'product') {
|
||||
loadTrendData()
|
||||
} else if (currentFilter.value === 'task') {
|
||||
loadTaskTrendData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onEndDateConfirm(e) {
|
||||
dateRange.end = formatTimestamp(e.value)
|
||||
if (dateRange.start && dateRange.end) {
|
||||
if (currentFilter.value === 'product') {
|
||||
loadTrendData()
|
||||
} else if (currentFilter.value === 'task') {
|
||||
loadTaskTrendData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPickerClose() {
|
||||
if (startPickerRef.value) startPickerRef.value.showByClickInput = false
|
||||
if (endPickerRef.value) endPickerRef.value.showByClickInput = false
|
||||
}
|
||||
|
||||
async function loadTrendData() {
|
||||
const trendTypeMap = { year: 1, month: 2, week: 3, today: 4, custom: 5 }
|
||||
const params = { trendType: trendTypeMap[currentRange.value] || 2 }
|
||||
if (currentRange.value === 'custom') {
|
||||
if (dateRange.start) params.beginBaogongTime = dateRange.start
|
||||
if (dateRange.end) params.endBaogongTime = dateRange.end
|
||||
}
|
||||
const res = await request({
|
||||
url: '/admin-api/mes/baogong-record/trend',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
const data = res?.data || {}
|
||||
trendData.baogongNum = data.baogongNum ?? 0
|
||||
trendData.passNum = data.passNum ?? 0
|
||||
trendData.noPassNum = data.noPassNum ?? 0
|
||||
trendData.passRate = data.passRate ?? 0
|
||||
|
||||
if (currentRange.value === 'custom') {
|
||||
baogongChartData.categories = []
|
||||
baogongChartData.series = [{ name: t('dashboard.baogongNum'), data: [] }]
|
||||
passRateChartData.categories = []
|
||||
passRateChartData.series = [{ name: t('dashboard.passRate'), data: [] }]
|
||||
return
|
||||
}
|
||||
|
||||
const dayTrend = data.dayTrend || []
|
||||
const transformed = transformChartData(dayTrend, currentRange.value)
|
||||
baogongChartData.categories = transformed.categories
|
||||
baogongChartData.series = [{ name: t('dashboard.baogongNum'), data: transformed.baogongData }]
|
||||
passRateChartData.categories = transformed.categories
|
||||
passRateChartData.series = [{ name: t('dashboard.passRate'), data: transformed.passRateData }]
|
||||
}
|
||||
|
||||
async function loadTaskTrendData() {
|
||||
const trendTypeMap = { year: 1, month: 2, week: 3, today: 4, custom: 5 }
|
||||
const params = { trendType: trendTypeMap[currentRange.value] || 2 }
|
||||
if (currentRange.value === 'custom') {
|
||||
if (dateRange.start) params.beginTime = dateRange.start
|
||||
if (dateRange.end) params.endTime = dateRange.end
|
||||
}
|
||||
const res = await request({
|
||||
url: '/admin-api/mes/task/trend',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
const data = res?.data || {}
|
||||
taskTrendData.totalNum = data.totalNum ?? 0
|
||||
taskTrendData.issuedNum = data.issuedNum ?? 0
|
||||
taskTrendData.partialScheduledNum = data.partialScheduledNum ?? 0
|
||||
taskTrendData.waitingProductionNum = data.waitingProductionNum ?? 0
|
||||
taskTrendData.producingNum = data.producingNum ?? 0
|
||||
taskTrendData.completedNum = data.completedNum ?? 0
|
||||
|
||||
if (currentRange.value === 'custom') {
|
||||
taskChartData.categories = []
|
||||
taskChartData.series = [{ name: t('dashboard.taskTrend'), data: [] }]
|
||||
return
|
||||
}
|
||||
|
||||
const dayTrend = data.dayTrend || []
|
||||
const categories = []
|
||||
const taskData = []
|
||||
if (currentRange.value === 'year') {
|
||||
const monthMap = new Map()
|
||||
dayTrend.forEach((item) => {
|
||||
const m = item.day ? item.day.substring(0, 7) : ''
|
||||
if (!m) return
|
||||
if (!monthMap.has(m)) {
|
||||
monthMap.set(m, 0)
|
||||
}
|
||||
monthMap.set(m, (monthMap.get(m) ?? 0) + (item.count ?? 0))
|
||||
})
|
||||
monthMap.forEach((val, key) => {
|
||||
categories.push(key)
|
||||
taskData.push(val)
|
||||
})
|
||||
} else if (currentRange.value === 'week') {
|
||||
dayTrend.forEach((item) => {
|
||||
const d = new Date(item.day)
|
||||
const dayIdx = d.getDay()
|
||||
categories.push(t(`dashboard.${weekdayKeys[dayIdx]}`))
|
||||
taskData.push(item.count ?? 0)
|
||||
})
|
||||
} else {
|
||||
dayTrend.forEach((item) => {
|
||||
const dayStr = item.day || ''
|
||||
categories.push(dayStr.substring(5))
|
||||
taskData.push(item.count ?? 0)
|
||||
})
|
||||
}
|
||||
taskChartData.categories = categories
|
||||
taskChartData.series = [{ name: t('dashboard.taskTrend'), data: taskData }]
|
||||
}
|
||||
|
||||
function viewMore() {
|
||||
if (currentFilter.value === 'task') {
|
||||
uni.navigateTo({ url: '/pages_function/pages/taskList/index' })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages_function/pages/planList/index' })
|
||||
}
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (currentFilter.value === 'task') {
|
||||
loadTaskTrendData()
|
||||
} else {
|
||||
loadTrendData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
defineExpose({ loadData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.plan-section {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.section-more {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f0f2f5;
|
||||
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;
|
||||
}
|
||||
|
||||
.filter-date-wrap {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.date-picker-item {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background: #f8fafc;
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border: 2rpx solid #e0e6ed;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #4a90c2;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:deep(.u-datetime-picker) {
|
||||
flex: 1;
|
||||
|
||||
.u-input {
|
||||
padding-left: 40rpx;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.up-icon {
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
z-index: 1;
|
||||
color: #4a90c2;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-content {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.trend-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.trend-stat-card {
|
||||
flex: 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 12rpx;
|
||||
margin: 0 6rpx;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-stat-value {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 6rpx;
|
||||
|
||||
&.pass {
|
||||
color: #18bc37;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.rate {
|
||||
color: #4a90c2;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-stat-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 450rpx;
|
||||
min-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="stats-section">
|
||||
<view class="section-title">{{ t('dashboard.productionOverview') }}</view>
|
||||
<view class="stats-grid">
|
||||
<view v-for="(stat, index) in statsData" :key="index" class="stat-card" :class="'stat-' + stat.type">
|
||||
<text class="stat-value">{{ formatNumber(stat.value) }}</text>
|
||||
<text class="stat-label">{{ t(`dashboard.${stat.labelKey}`) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import request from '@/utils/request'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const statsData = reactive([
|
||||
{ labelKey: 'all', type: 'total', value: 0 },
|
||||
{ labelKey: 'pending', type: 'pending', value: 0 },
|
||||
{ labelKey: 'running', type: 'running', value: 0 },
|
||||
{ labelKey: 'finished', type: 'finished', value: 0 }
|
||||
])
|
||||
|
||||
async function loadProductionStats() {
|
||||
const res = await request({ url: '/admin-api/mes/dashboard/getProduction', method: 'get' })
|
||||
const taskItems = (res?.data?.taskItems || []).map((i) => ({
|
||||
key: String(i.key),
|
||||
value: Number(i.value ?? 0)
|
||||
}))
|
||||
const byKey = taskItems.reduce((acc, cur) => {
|
||||
acc[cur.key] = cur.value
|
||||
return acc
|
||||
}, {})
|
||||
const keyOrder = ['1', '2', '3', '4']
|
||||
statsData.forEach((stat, index) => {
|
||||
const k = keyOrder[index]
|
||||
stat.value = byKey[k] ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProductionStats()
|
||||
})
|
||||
|
||||
defineExpose({ loadProductionStats })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stats-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 16rpx;
|
||||
margin: 0 8rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.stat-total {
|
||||
border-left: 6rpx solid #1a3a5c;
|
||||
|
||||
.stat-value {
|
||||
color: #1a3a5c;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-pending {
|
||||
border-left: 6rpx solid #ff8c00;
|
||||
|
||||
.stat-value {
|
||||
color: #ff8c00;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-running {
|
||||
border-left: 6rpx solid #18bc37;
|
||||
|
||||
.stat-value {
|
||||
color: #18bc37;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-finished {
|
||||
border-left: 6rpx solid #4a90c2;
|
||||
|
||||
.stat-value {
|
||||
color: #4a90c2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue