perf:首页-组件化

master
黄伟杰 3 weeks ago
parent b1e6d3af72
commit e41283295b

@ -0,0 +1,324 @@
<template>
<view class="banner-section">
<view class="banner-bg">
<view class="banner-content">
<text class="banner-title">{{ t('dashboard.welcome') }}</text>
<text class="banner-subtitle">{{ t('dashboard.subtitle') }}</text>
</view>
<view class="banner-decoration">
<view class="deco-line"></view>
<view class="deco-dot"></view>
</view>
</view>
<view class="bell-wrapper" @click="openTodo">
<view class="bell-icon">
<view class="bell-badge" v-show="badgeVisible">
<text class="bell-badge-text">{{ badgeText }}</text>
</view>
<image src="/static/logo/bell.png" mode="aspectFit" style="width: 48rpx; height: 48rpx;" />
</view>
</view>
<uni-popup ref="todoPopup" type="right" background-color="#fff">
<view class="todo-popup">
<view class="todo-header">
<view class="todo-back" @click="closeTodo">
<text class="back-icon"></text>
<text class="back-text">{{ t('dashboard.back') }}</text>
</view>
<text class="todo-title-text">{{ t('dashboard.todoTitle') }}</text>
</view>
<scroll-view scroll-y class="todo-scroll">
<view v-if="todoList.length === 0" class="todo-empty">
<text class="empty-text">{{ t('dashboard.noTodo') }}</text>
</view>
<view v-else>
<view v-for="(item, index) in todoList" :key="index" class="todo-item">
<view class="todo-dot"></view>
<view class="todo-content">
<view class="todo-item-title">{{ item.name }}</view>
<view class="todo-sub">{{ t('dashboard.taskCode', { value: item.code }) }}</view>
<view class="todo-sub">{{ t('dashboard.taskType', { value: item.type }) }}</view>
<view class="todo-sub">{{ t('dashboard.taskTarget', { value: item.deviceName }) }}</view>
<view class="todo-sub">{{ t('dashboard.createTime', { value: formatDate(item.createTime) }) }}</view>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
const { t } = useI18n()
const todoPopup = ref(null)
const todoCount = ref(0)
const todoList = reactive([])
const badgeVisible = computed(() => Number(todoCount.value) > 0)
const badgeText = computed(() => {
const count = Number(todoCount.value)
if (!Number.isFinite(count) || count <= 0) return ''
if (count > 99) return '99+'
return String(count)
})
function formatDate(ms) {
if (!ms) return '-'
const date = new Date(ms)
if (Number.isNaN(date.getTime())) return '-'
const pad2 = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
}
function openTodo() {
todoPopup.value?.open()
}
function closeTodo() {
todoPopup.value?.close()
}
async function loadTodoList() {
const res = await request({ url: '/admin-api/mes/dashboard/getTodoList', method: 'get' })
const data = res?.data || []
todoList.splice(0, todoList.length, ...data)
todoCount.value = data.length
}
onMounted(() => {
loadTodoList()
})
defineExpose({ loadTodoList })
</script>
<style lang="scss" scoped>
.banner-section {
position: relative;
height: 320rpx;
}
.banner-bg {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 50%, #3d7ab5 100%);
position: relative;
overflow: visible;
&::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 300rpx;
height: 300rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
bottom: -30%;
left: 10%;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 50%;
}
}
.banner-content {
position: relative;
z-index: 2;
padding: 60rpx 40rpx;
.banner-title {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
}
.banner-subtitle {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #ffffff;
line-height: 1.4;
}
}
.banner-decoration {
position: absolute;
bottom: 40rpx;
left: 40rpx;
display: flex;
align-items: center;
.deco-line {
width: 60rpx;
height: 4rpx;
background: #ff8c00;
border-radius: 2rpx;
}
.deco-dot {
width: 12rpx;
height: 12rpx;
background: #ff8c00;
border-radius: 50%;
margin-left: 16rpx;
}
}
.bell-wrapper {
position: absolute;
top: 30rpx;
right: 30rpx;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.bell-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.bell-badge {
position: absolute;
top: -10rpx;
right: -12rpx;
z-index: 2;
min-width: 34rpx;
height: 34rpx;
padding: 0 8rpx;
border-radius: 18rpx;
background: #ff4d4f;
border: 2rpx solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.bell-badge-text {
color: #ffffff;
font-size: 20rpx;
line-height: 1;
font-weight: 600;
}
.todo-popup {
width: 600rpx;
height: 100vh;
background: #ffffff;
}
.todo-header {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.todo-back {
display: flex;
align-items: center;
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.todo-title-text {
flex: 1;
text-align: center;
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
margin-right: 80rpx;
}
.todo-scroll {
height: calc(100vh - 140rpx);
background: #f5f7fa;
}
.todo-empty {
display: flex;
align-items: center;
justify-content: center;
height: 400rpx;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
.todo-item {
display: flex;
align-items: center;
background: #ffffff;
padding: 28rpx 30rpx;
margin-bottom: 2rpx;
&:active {
background: #f5f7fa;
}
}
.todo-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #1a3a5c;
margin-right: 20rpx;
}
.todo-content {
flex: 1;
.todo-item-title {
display: block;
font-size: 28rpx;
color: #333333;
font-weight: bold;
margin-bottom: 12rpx;
}
.todo-sub {
display: block;
font-size: 24rpx;
color: #666666;
margin-bottom: 6rpx;
line-height: 1.4;
}
}
</style>

@ -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…
Cancel
Save