You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

673 lines
19 KiB
Vue

<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">
<picker mode="selector" :range="filterRange" range-key="text" :value="filterIndex" @change="onFilterChange">
<view class="filter-select">
<text class="filter-text">{{ currentFilterLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
<picker mode="selector" :range="rangeRange" range-key="text" :value="rangeIndex" @change="onRangeChange">
<view class="filter-select">
<text class="filter-text">{{ currentRangeLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<view v-if="currentRange === 'custom'" class="filter-date-wrap">
<view class="date-picker-item">
<uni-datetime-picker v-model="dateRange.start" type="datetime"
:placeholder="t('dashboard.startDate')" @change="onStartDateChange" />
</view>
<view class="date-picker-item">
<uni-datetime-picker v-model="dateRange.end" type="datetime"
:placeholder="t('dashboard.endDate')" @change="onEndDateChange" />
</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">
<scroll-view scroll-x enable-flex class="trend-stats-scroll">
<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">{{ formatNumber(taskTrendData.issuedNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.issuedNum') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.partialScheduledNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.partialScheduledNum') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.waitingProductionNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.waitingProduction') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.producingNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.producing') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.completedNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.completed') }}</text>
</view>
</view>
</scroll-view>
<view v-if="currentRange !== 'custom'" class="trend-chart">
<text class="chart-title">{{ t('dashboard.totalTask') }}</text>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="taskChartData" :canvas2d="false"
:opts="chartOpts" />
</view>
</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'
const { t } = useI18n()
const chartOpts = {
color: ['#1a3a5c'],
dataLabel: false,
dataPointShape: false,
legend: { show: false },
xAxis: { disableGrid: true, labelCount: 4 },
yAxis: { gridType: 'dash', dashLength: 2 },
extra: { line: { type: 'curve', width: 1, activeType: 'hollow' } }
}
const passRateChartOpts = {
color: ['#1a3a5c'],
dataLabel: false,
dataPointShape: false,
legend: { show: false },
xAxis: { disableGrid: true, labelCount: 4 },
yAxis: { gridType: 'dash', dashLength: 2, data: [{ min: 0, max: 100 }] },
extra: { line: { type: 'curve', width: 1, activeType: 'hollow' } }
}
const currentFilter = ref('task')
const currentRange = ref('year')
const isInitialLoad = ref(true)
const filterRange = computed(() => [
{ text: t('dashboard.filterTask'), value: 'task' },
{ text: t('dashboard.filterProduct'), value: 'product' }
])
const filterIndex = computed(() => {
const idx = filterRange.value.findIndex(item => item.value === currentFilter.value)
return idx >= 0 ? idx : 0
})
const currentFilterLabel = computed(() => {
return currentFilter.value === 'task' ? t('dashboard.filterTask') : t('dashboard.filterProduct')
})
const rangeRange = 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 rangeIndex = computed(() => {
const idx = rangeRange.value.findIndex(item => item.value === currentRange.value)
return idx >= 0 ? idx : 0
})
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 onFilterChange(e) {
const idx = e.detail.value
const val = filterRange.value[idx]?.value
if (!val) return
currentFilter.value = val
if (val === 'product') {
loadTrendData()
} else if (val === 'task') {
loadTaskTrendData()
}
}
function onRangeChange(e) {
const idx = e.detail.value
const val = rangeRange.value[idx]?.value
if (!val) return
currentRange.value = val
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 onStartDateChange(val) {
dateRange.start = val
if (dateRange.start && dateRange.end) {
loadData()
}
}
function onEndDateChange(val) {
dateRange.end = val
if (dateRange.start && dateRange.end) {
loadData()
}
}
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,
showLoading: !isInitialLoad.value
})
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,
showLoading: !isInitialLoad.value
})
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(async () => {
await loadData()
isInitialLoad.value = false
})
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(.uni-date) {
flex: 1;
width: 100%;
.uni-date-editor--x {
border: none;
background: transparent;
padding: 0;
}
.uni-date__x-input {
background: transparent;
font-size: 24rpx;
}
}
}
.trend-content {
margin-top: 8rpx;
}
.trend-stats-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 24rpx;
}
.trend-stats {
display: inline-flex;
flex-wrap: nowrap;
}
.trend-stat-card {
flex-shrink: 0;
width: 164rpx;
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%;
border-radius: 12rpx;
}
</style>