|
|
|
|
@ -23,6 +23,7 @@
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view class="content-section">
|
|
|
|
|
<!-- 功能导航 -->
|
|
|
|
|
<view class="nav-section">
|
|
|
|
|
<view class="section-title">{{ t('dashboard.functionNav') }}</view>
|
|
|
|
|
<view class="nav-grid">
|
|
|
|
|
@ -35,6 +36,7 @@
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 生产概览统计 -->
|
|
|
|
|
<view class="stats-section">
|
|
|
|
|
<view class="section-title">{{ t('dashboard.productionOverview') }}</view>
|
|
|
|
|
<view class="stats-grid">
|
|
|
|
|
@ -45,45 +47,104 @@
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 生产计划 -->
|
|
|
|
|
<view class="plan-section">
|
|
|
|
|
<view class="section-header">
|
|
|
|
|
<text class="section-title">{{ t('dashboard.productionPlan') }}</text>
|
|
|
|
|
<text v-if="hasMorePlans || isShowAllPlans" class="section-more" @click="viewMorePlans">
|
|
|
|
|
{{ isShowAllPlans ? t('dashboard.collapseList') : t('dashboard.viewMore') }}
|
|
|
|
|
<text class="section-more" @click="viewMorePlans">
|
|
|
|
|
{{ t('dashboard.viewMore') }}
|
|
|
|
|
</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="plan-list">
|
|
|
|
|
<view v-for="(plan, index) in displayPlanList" :key="index" class="plan-card"
|
|
|
|
|
@click="handlePlanClick(plan)">
|
|
|
|
|
<view class="plan-header">
|
|
|
|
|
<text class="plan-code">{{ plan.code }}</text>
|
|
|
|
|
<view class="plan-status" :class="'status-' + plan.statusType">
|
|
|
|
|
<text>{{ plan.status }}</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="plan-body">
|
|
|
|
|
<view class="plan-row">
|
|
|
|
|
<text class="plan-label">{{ t('dashboard.productName') }}</text>
|
|
|
|
|
<text class="plan-value">{{ plan.productName }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="plan-row">
|
|
|
|
|
<text class="plan-label">{{ t('dashboard.pipeline') }}</text>
|
|
|
|
|
<text class="plan-value">{{ plan.feedingPipelineName }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="plan-row">
|
|
|
|
|
<text class="plan-label">{{ t('dashboard.planNumber') }}</text>
|
|
|
|
|
<text class="plan-value plan-num">{{ plan.planNumber }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="plan-row">
|
|
|
|
|
<text class="plan-label">{{ t('dashboard.planStart') }}</text>
|
|
|
|
|
<text class="plan-value">{{ plan.planStartTimeText }}</text>
|
|
|
|
|
<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>
|
|
|
|
|
<scroll-view scroll-x class="chart-scroll">
|
|
|
|
|
<view class="chart-box" :style="{ width: chartWidth }">
|
|
|
|
|
<qiun-data-charts type="line" :chartData="baogongChartData" :canvas2d="false"
|
|
|
|
|
:opts="{ legend: { show: false }, xAxis: { disableGrid: true }, yAxis: { gridType: 'dash', dashLength: 2 }, extra: { line: { type: 'straight', width: 2, activeType: 'hollow' } } }" />
|
|
|
|
|
</view>
|
|
|
|
|
<view class="plan-row">
|
|
|
|
|
<text class="plan-label">{{ t('dashboard.planEnd') }}</text>
|
|
|
|
|
<text class="plan-value">{{ plan.planEndTimeText }}</text>
|
|
|
|
|
</scroll-view>
|
|
|
|
|
</view>
|
|
|
|
|
<view v-if="currentRange !== 'custom'" class="trend-chart">
|
|
|
|
|
<text class="chart-title">{{ t('dashboard.passRate') }}</text>
|
|
|
|
|
<scroll-view scroll-x class="chart-scroll">
|
|
|
|
|
<view class="chart-box" :style="{ width: chartWidth }">
|
|
|
|
|
<qiun-data-charts type="line" :chartData="passRateChartData" :canvas2d="false"
|
|
|
|
|
:opts="{ legend: { show: false }, xAxis: { disableGrid: true }, yAxis: { gridType: 'dash', dashLength: 2, data: [{ min: 0, max: 100 }] }, extra: { line: { type: 'straight', width: 2, activeType: 'hollow' } } }" />
|
|
|
|
|
</view>
|
|
|
|
|
</scroll-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>
|
|
|
|
|
<scroll-view scroll-x class="chart-scroll">
|
|
|
|
|
<view class="chart-box" :style="{ width: chartWidth }">
|
|
|
|
|
<qiun-data-charts type="line" :chartData="taskChartData" :canvas2d="false"
|
|
|
|
|
:opts="{ legend: { show: false }, xAxis: { disableGrid: true }, yAxis: { gridType: 'dash', dashLength: 2 }, extra: { line: { type: 'straight', width: 2, activeType: 'hollow' } } }" />
|
|
|
|
|
</view>
|
|
|
|
|
</scroll-view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
@ -93,6 +154,12 @@
|
|
|
|
|
<text class="go-top-icon">↑</text>
|
|
|
|
|
</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 />
|
|
|
|
|
|
|
|
|
|
<uni-popup ref="todoPopup" type="right" background-color="#fff">
|
|
|
|
|
<view class="todo-popup">
|
|
|
|
|
<view class="todo-header">
|
|
|
|
|
@ -130,6 +197,7 @@ import { onShow } from '@dcloudio/uni-app'
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import request from '@/utils/request'
|
|
|
|
|
import { onLocaleChange, offLocaleChange, setNavigationTitle } from '@/locales'
|
|
|
|
|
import { formatNumber, formatPercent } from '@/utils/format'
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
|
|
|
@ -138,7 +206,6 @@ const todoCount = ref(0);
|
|
|
|
|
const scrollTop = ref(0)
|
|
|
|
|
const currentScrollTop = ref(0)
|
|
|
|
|
const showGoTop = ref(false)
|
|
|
|
|
const isShowAllPlans = ref(false)
|
|
|
|
|
const badgeVisible = computed(() => Number(todoCount.value) > 0)
|
|
|
|
|
const badgeText = computed(() => {
|
|
|
|
|
const count = Number(todoCount.value)
|
|
|
|
|
@ -162,14 +229,349 @@ const statsData = reactive([
|
|
|
|
|
{ labelKey: 'finished', type: 'finished' }
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const planList = reactive([]);
|
|
|
|
|
const hasMorePlans = computed(() => planList.length > 3)
|
|
|
|
|
const displayPlanList = computed(() => {
|
|
|
|
|
if (isShowAllPlans.value) return planList
|
|
|
|
|
return planList.slice(0, 3)
|
|
|
|
|
const todoList = reactive([]);
|
|
|
|
|
|
|
|
|
|
const showFilterPicker = ref(false)
|
|
|
|
|
const showRangePicker = ref(false)
|
|
|
|
|
const startPickerRef = ref(null)
|
|
|
|
|
const endPickerRef = ref(null)
|
|
|
|
|
const currentFilter = ref('task')
|
|
|
|
|
const currentRange = ref('month')
|
|
|
|
|
|
|
|
|
|
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 todoList = reactive([]);
|
|
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const dateRange = reactive({ start: '', end: '' })
|
|
|
|
|
|
|
|
|
|
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 chartWidth = computed(() => {
|
|
|
|
|
const count = baogongChartData.categories.length
|
|
|
|
|
if (count <= 7) return '100%'
|
|
|
|
|
return Math.max(count * 50, 300) + 'rpx'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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: '' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const weekdayKeys = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
|
|
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
|
const dayStr = item.day || ''
|
|
|
|
|
return dayStr.substring(5)
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
categories,
|
|
|
|
|
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
|
|
|
|
|
passRateData: dayTrend.map((item) => item.passRate ?? 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const categories = dayTrend.map((item) => {
|
|
|
|
|
const dayStr = item.day || ''
|
|
|
|
|
return dayStr.substring(5)
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
categories,
|
|
|
|
|
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
|
|
|
|
|
passRateData: dayTrend.map((item) => item.passRate ?? 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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') {
|
|
|
|
|
const weekdayKeys = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
|
|
|
|
|
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 showTodoList() {
|
|
|
|
|
todoPopup.value.open();
|
|
|
|
|
@ -199,16 +601,8 @@ function handleNavClick(item) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePlanClick(plan) {
|
|
|
|
|
uni.showToast({
|
|
|
|
|
title: t('dashboard.viewPlan', { code: plan.code }),
|
|
|
|
|
icon: 'none'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function viewMorePlans() {
|
|
|
|
|
if (!hasMorePlans.value && !isShowAllPlans.value) return
|
|
|
|
|
isShowAllPlans.value = !isShowAllPlans.value
|
|
|
|
|
uni.navigateTo({ url: '/pages_function/pages/planList/index' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onScroll(e) {
|
|
|
|
|
@ -235,26 +629,6 @@ function formatDate(ms) {
|
|
|
|
|
return `${y}-${m}-${d}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getPlanStatusLabel = (value) => {
|
|
|
|
|
const v = value === '' || value === null || value === undefined ? undefined : String(value)
|
|
|
|
|
if (v == '1') return t('dashboard.statusScheduled')
|
|
|
|
|
if (v == '6') return t('dashboard.statusTrial')
|
|
|
|
|
if (v == '2') return t('dashboard.statusMass')
|
|
|
|
|
if (v == '3') return t('dashboard.statusPause')
|
|
|
|
|
if (v == '4') return t('dashboard.statusWaitStockIn')
|
|
|
|
|
return '-'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mapPlanStatus(status) {
|
|
|
|
|
const v = status === '' || status === null || status === undefined ? undefined : String(status)
|
|
|
|
|
if (v == '1') return { status: getPlanStatusLabel(v), statusType: 'pending' }
|
|
|
|
|
if (v == '6') return { status: getPlanStatusLabel(v), statusType: 'running' }
|
|
|
|
|
if (v == '2') return { status: getPlanStatusLabel(v), statusType: 'running' }
|
|
|
|
|
if (v == '3') return { status: getPlanStatusLabel(v), statusType: 'pending' }
|
|
|
|
|
if (v == '4') return { status: getPlanStatusLabel(v), statusType: 'finished' }
|
|
|
|
|
return { status: getPlanStatusLabel(v), statusType: 'pending' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadProductionStats() {
|
|
|
|
|
const res = await request({ url: '/admin-api/mes/dashboard/getProduction', method: 'get' })
|
|
|
|
|
const taskItems = (res?.data?.taskItems || []).map((i) => ({
|
|
|
|
|
@ -272,29 +646,6 @@ async function loadProductionStats() {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadPlanList() {
|
|
|
|
|
const res = await request({ url: '/admin-api/mes/dashboard/getPlan', method: 'get' })
|
|
|
|
|
const raw = Array.isArray(res?.data) ? res.data : (res?.data ? [res.data] : [])
|
|
|
|
|
const mapped = raw.map((p) => {
|
|
|
|
|
const statusInfo = mapPlanStatus(p?.status)
|
|
|
|
|
return {
|
|
|
|
|
id: p?.id,
|
|
|
|
|
code: p?.code ?? '-',
|
|
|
|
|
status: statusInfo.status,
|
|
|
|
|
statusType: statusInfo.statusType,
|
|
|
|
|
productName: p?.productName ?? '-',
|
|
|
|
|
feedingPipelineName: p?.feedingPipelineName ?? '-',
|
|
|
|
|
planNumber: p?.planNumber ?? 0,
|
|
|
|
|
planStartTimeText: formatDate(p?.planStartTime),
|
|
|
|
|
planEndTimeText: formatDate(p?.planEndTime),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
planList.splice(0, planList.length, ...mapped)
|
|
|
|
|
if (mapped.length <= 3) {
|
|
|
|
|
isShowAllPlans.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadTodoList() {
|
|
|
|
|
const res = await request({ url: '/admin-api/mes/dashboard/getTodoList', method: 'get' })
|
|
|
|
|
const data = res?.data || []
|
|
|
|
|
@ -303,7 +654,7 @@ async function loadTodoList() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadDashboard() {
|
|
|
|
|
await Promise.allSettled([loadProductionStats(), loadPlanList(), loadTodoList()])
|
|
|
|
|
await Promise.allSettled([loadProductionStats(), loadTodoList(), loadTrendData()])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
@ -609,89 +960,165 @@ onUnmounted(() => {
|
|
|
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-list {
|
|
|
|
|
.filter-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-card {
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
.filter-select {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 12rpx 24rpx;
|
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
margin-right: 20rpx;
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
background: #e8f4ff;
|
|
|
|
|
background: #e8ecf0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-code {
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
.filter-text {
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
color: #1a3a5c;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-right: 8rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-status {
|
|
|
|
|
padding: 8rpx 20rpx;
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
.filter-arrow {
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
color: #999999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-pending {
|
|
|
|
|
background: rgba(255, 140, 0, 0.15);
|
|
|
|
|
color: #ff8c00;
|
|
|
|
|
.filter-date-wrap {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-running {
|
|
|
|
|
background: rgba(24, 188, 55, 0.15);
|
|
|
|
|
color: #18bc37;
|
|
|
|
|
}
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-finished {
|
|
|
|
|
background: rgba(74, 144, 194, 0.15);
|
|
|
|
|
color: #4a90c2;
|
|
|
|
|
.up-icon {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 12rpx;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
color: #4a90c2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-body {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
.trend-content {
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-row {
|
|
|
|
|
.trend-stats {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.trend-stat-card {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
padding: 20rpx 12rpx;
|
|
|
|
|
margin: 0 6rpx;
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
&:first-child {
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-label {
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-value {
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
color: #333333;
|
|
|
|
|
.trend-chart {
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.plan-num {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
.chart-title {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #1a3a5c;
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-scroll {
|
|
|
|
|
width: 100%;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-box {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 450rpx;
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.task-placeholder {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 300rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.placeholder-text {
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
color: #999999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.todo-popup {
|
|
|
|
|
|