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.

744 lines
16 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<scroll-view scroll-y class="main-scroll" @scroll="onScroll">
<view class="banner-section">
<view class="banner-bg">
<view class="banner-content">
<text class="banner-title">欢迎您使用</text>
<text class="banner-subtitle">必硕数字化智能中控平台</text>
</view>
<view class="banner-decoration">
<view class="deco-line"></view>
<view class="deco-dot"></view>
</view>
</view>
<view class="bell-wrapper" @click="showTodoList">
<text class="bell-icon">
<el-badge :value="todoCount" :hidden="todoCount === 0" class="item">
<image src="/static/logo/bell.png" mode="aspectFit" style="width: 48rpx; height: 48rpx;" />
</el-badge>
</text>
</view>
</view>
<view class="content-section">
<view class="nav-section">
<view class="section-title">功能导航</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">{{ item.name }}</text>
</view>
</view>
</view>
<view class="stats-section">
<view class="section-title">生产整体概况</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">{{ stat.value }}</text>
<text class="stat-label">{{ stat.label }}</text>
</view>
</view>
</view>
<view class="plan-section">
<view class="section-header">
<text class="section-title">生产计划</text>
<text class="section-more" @click="viewMorePlans">查看更多 </text>
</view>
<view class="plan-list">
<view v-for="(plan, index) in planList" :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>
<view class="plan-body">
<view class="plan-row">
<text class="plan-label">产品名称</text>
<text class="plan-value">{{ plan.productName }}</text>
</view>
<view class="plan-row">
<text class="plan-label">生产线</text>
<text class="plan-value">{{ plan.feedingPipelineName }}</text>
</view>
<view class="plan-row">
<text class="plan-label">计划数量</text>
<text class="plan-value plan-num">{{ plan.planNumber }}</text>
</view>
<view class="plan-row">
<text class="plan-label">计划开始</text>
<text class="plan-value">{{ plan.planStartTimeText }}</text>
</view>
<view class="plan-row">
<text class="plan-label">计划结束</text>
<text class="plan-value">{{ plan.planEndTimeText }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<uni-popup ref="todoPopup" type="right" background-color="#fff">
<view class="todo-popup">
<view class="todo-header">
<view class="todo-back" @click="closeTodoList">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="todo-title">待办任务</text>
</view>
<scroll-view scroll-y class="todo-scroll">
<view v-if="todoList.length === 0" class="todo-empty">
<text class="empty-text">暂无待办任务</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-title" style="text-align: left;margin-right: 0;">{{ item.name }}</view>
<view class="todo-sub">任务编号:{{ item.code }}</view>
<view class="todo-sub">任务类型:{{ item.type }}</view>
<view class="todo-sub">目标:{{ item.deviceName }}</view>
<view class="todo-sub">{{ formatDate(item.createTime) }}</view>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { onMounted, ref, reactive } from 'vue';
import request from '@/utils/request'
const todoPopup = ref(null);
const todoCount = ref(0);
const navList = reactive([
{ name: '模具', icon: '🔧', bgColor: '#1a3a5c', path: '/pages_function/mold' },
{ name: '设备', icon: '⚙️', bgColor: '#2d5a87', path: '/pages_function/equipment' },
{ name: '关键件', icon: '🔩', bgColor: '#3d7ab5', path: '/pages_function/keypart' },
{ name: '备件', icon: '📦', bgColor: '#4a90c2', path: '/pages_function/spare' },
{ name: '出入库', icon: '📊', bgColor: '#5aa0d2', path: '/pages_function/warehouse' }
]);
const statsData = reactive([
{ label: '总数', type: 'total' },
{ label: '未开工', type: 'pending' },
{ label: '生产中', type: 'running' },
{ label: '完工', type: 'finished' }
]);
const planList = reactive([]);
const todoList = reactive([]);
function showTodoList() {
todoPopup.value.open();
}
function closeTodoList() {
todoPopup.value.close();
}
function handleNavClick(item) {
const navMap = {
'模具': '/pages_function/pages/mold/index',
'设备': '/pages_function/pages/equipment/index',
'备件': '/pages_function/pages/spare/index',
'关键件': '/pages_function/pages/keypart/index'
};
const url = navMap[item.name];
if (url) {
uni.navigateTo({ url });
} else {
uni.showToast({
title: `进入${item.name}模块`,
icon: 'none'
});
}
}
function handlePlanClick(plan) {
uni.showToast({
title: `查看计划: ${plan.code}`,
icon: 'none'
});
}
function viewMorePlans() {
uni.showToast({
title: '查看更多计划',
icon: 'none'
});
}
function onScroll(e) {
}
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')
const y = date.getFullYear()
const m = pad2(date.getMonth() + 1)
const d = pad2(date.getDate())
return `${y}-${m}-${d}`
}
const getPlanStatusLabel = (value) => {
const v = value === '' || value === null || value === undefined ? undefined : String(value)
if (v == '1') return '已排产'
if (v == '6') return '试产'
if (v == '2') return '量产'
if (v == '3') return '暂停'
if (v == '4') return '待入库'
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) => ({
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
})
}
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),
}
})
if (mapped.length) {
planList.splice(0, planList.length, ...mapped)
}
}
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
}
async function loadDashboard() {
await Promise.allSettled([loadProductionStats(), loadPlanList(), loadTodoList()])
}
onMounted(() => {
loadDashboard()
})
</script>
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f0f2f5;
}
.main-scroll {
flex: 1;
height: 100%;
}
.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 {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.content-section {
padding: 0 24rpx 24rpx;
margin-top: -40rpx;
position: relative;
z-index: 5;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.section-more {
font-size: 26rpx;
color: #666666;
}
}
.nav-section {
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.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;
}
.stats-section {
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;
}
}
.plan-section {
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.plan-list {
display: flex;
flex-direction: column;
}
.plan-card {
background: #f8fafc;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background: #e8f4ff;
}
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.plan-code {
font-size: 28rpx;
font-weight: 600;
color: #1a3a5c;
}
.plan-status {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
}
.status-pending {
background: rgba(255, 140, 0, 0.15);
color: #ff8c00;
}
.status-running {
background: rgba(24, 188, 55, 0.15);
color: #18bc37;
}
.status-finished {
background: rgba(74, 144, 194, 0.15);
color: #4a90c2;
}
.plan-body {
display: flex;
flex-direction: column;
}
.plan-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
}
.plan-label {
font-size: 26rpx;
color: #999999;
}
.plan-value {
font-size: 26rpx;
color: #333333;
}
.plan-num {
font-weight: 600;
color: #1a3a5c;
}
.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 {
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-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;
}
}
.todo-priority {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
.priority-high {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
.priority-medium {
background: rgba(255, 140, 0, 0.1);
color: #ff8c00;
}
.priority-low {
background: rgba(24, 188, 55, 0.1);
color: #18bc37;
}
</style>