| | |
| | | </el-avatar> |
| | | <div class="welcome-text"> |
| | | <div class="welcome-title"> |
| | | 晚上好,{{ userStore.nickName || userStore.name || "超级管理员" }} 👋 |
| | | {{ greetingText }},{{ userStore.nickName || userStore.name || "超级管理员" }} 👋 |
| | | </div> |
| | | <div class="welcome-subtitle">专注工业数字化,助力智造升级</div> |
| | | </div> |
| | | </div> |
| | | <div class="welcome-meta"> |
| | | <div class="meta-time digital-number">{{ nowTime }}</div> |
| | | <div class="meta-extra"> |
| | | <span class="meta-date">{{ nowDate }}</span> |
| | | <span class="meta-weather">{{ weatherText }}</span> |
| | | </div> |
| | | <div class="meta-tip">MES / MOM 生产运营驾驶舱</div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="top-row"> |
| | | <section v-if="dashboardCards.length > 0" class="top-row"> |
| | | <div class="stats-grid"> |
| | | <article |
| | | v-for="card in dashboardCards" |
| | | :key="card.key" |
| | | class="stat-card" |
| | | :class="card.key" |
| | | v-for="card in dashboardCards" |
| | | :key="card.key" |
| | | class="stat-card" |
| | | :class="card.key" |
| | | > |
| | | <div class="stat-header"> |
| | | <div class="stat-title-wrap"> |
| | |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="main-grid"> |
| | | <div class="left-column"> |
| | | <div class="cockpit-panel process-panel"> |
| | | <section v-if="hasVisiblePanels" class="main-grid"> |
| | | <div v-if="hasLeftPanels" class="left-column"> |
| | | <div v-if="visiblePanels.process" class="cockpit-panel process-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">工序数据生产统计明细</div> |
| | | <div class="panel-actions"> |
| | |
| | | <div class="process-body"> |
| | | <div class="process-chart" :class="{ empty: !hasProcessData }"> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="{ width: '100%', height: '100%' }" |
| | | :grid="processGrid" |
| | | :series="processSeries" |
| | | :tooltip="processTooltip" |
| | | :xAxis="processXAxis" |
| | | :yAxis="processYAxis" |
| | | :style="{ height: hasProcessData ? '340px' : '280px' }" |
| | | @click="handleChartClick" |
| | | :options="chartBaseOptions" |
| | | :chartStyle="{ width: '100%', height: '100%' }" |
| | | :grid="processGrid" |
| | | :series="processSeries" |
| | | :tooltip="processTooltip" |
| | | :xAxis="processXAxis" |
| | | :yAxis="processYAxis" |
| | | :style="{ height: hasProcessData ? '340px' : '280px' }" |
| | | @click="handleChartClick" |
| | | /> |
| | | <div v-if="!hasProcessData" class="chart-empty"> |
| | | <el-icon><DataAnalysis /></el-icon> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel order-panel"> |
| | | <div v-if="visiblePanels.order" class="cockpit-panel order-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">生产订单进度</div> |
| | | <el-radio-group v-model="orderFilter" size="small"> |
| | | <el-radio-button label="all">全部</el-radio-button> |
| | | <el-radio-button label="in_progress">进行中</el-radio-button> |
| | | <el-radio-button label="completed">已完成</el-radio-button> |
| | | <el-radio-button label="paused">已暂停</el-radio-button> |
| | | <el-radio-button :value="'all'">全部({{ orderProgressMeta.total }})</el-radio-button> |
| | | <el-radio-button :value="'waiting'">待开始({{ orderProgressMeta.waitingCount }})</el-radio-button> |
| | | <el-radio-button :value="'inProgress'">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button> |
| | | <el-radio-button :value="'completed'">已完成({{ orderProgressMeta.completedCount }})</el-radio-button> |
| | | <el-radio-button :value="'paused'">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-table :data="filteredOrders" stripe> |
| | |
| | | <template #default="{ row }"> |
| | | <div class="table-progress"> |
| | | <el-progress |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </div> |
| | |
| | | <el-table-column label="状态" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getOrderStatusType(row.status)" effect="light"> |
| | | {{ getOrderStatusText(row.status) }} |
| | | {{ row.statusLabel || getOrderStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel contract-panel"> |
| | | <div class="panel-title">客户合同金额分析</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-card"> |
| | | <div class="contract-name">总合同金额(元)</div> |
| | | <div class="contract-main digital-number">{{ formatNumber(sum) }}</div> |
| | | <div class="contract-compare"> |
| | | 同比 |
| | | <span class="rise">{{ trendText(yny) }}</span> |
| | | 环比 |
| | | <span class="rise">{{ trendText(chain) }}</span> |
| | | </div> |
| | | <div v-if="visiblePanels.contract" class="cockpit-panel contract-panel"> |
| | | <div class="panel-title">客户合同金额分析</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-card"> |
| | | <div class="contract-name">总合同金额(元)</div> |
| | | <div class="contract-main digital-number">{{ formatNumber(sum) }}</div> |
| | | <div class="contract-compare"> |
| | | 同比 |
| | | <span class="rise">{{ trendText(yny) }}</span> |
| | | 环比 |
| | | <span class="rise">{{ trendText(chain) }}</span> |
| | | </div> |
| | | <div class="contract-chart-wrap"> |
| | | <Echarts |
| | | </div> |
| | | <div class="contract-chart-wrap"> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :legend="pieLegend" |
| | | :chartStyle="chartStylePie" |
| | | :series="materialPieSeries" |
| | | :tooltip="pieTooltip" |
| | | /> |
| | | </div> |
| | | /> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <span class="legend-dot" :style="{ backgroundColor: item.itemStyle?.color }"></span> |
| | | <span class="contract-item-name">{{ item.name }}</span> |
| | | <span class="contract-item-rate">{{ item.rate }}%</span> |
| | | <span class="contract-item-value digital-number">¥{{ formatNumber(item.value) }}</span> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <span class="legend-dot" :style="{ backgroundColor: item.itemStyle?.color }"></span> |
| | | <span class="contract-item-name">{{ item.name }}</span> |
| | | <span class="contract-item-rate">{{ item.rate }}%</span> |
| | | <span class="contract-item-value digital-number">¥{{ formatNumber(item.value) }}</span> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel quality-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">质量统计</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <el-radio-button :value="1">周</el-radio-button> |
| | | <el-radio-button :value="2">月</el-radio-button> |
| | | <el-radio-button :value="3">季度</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}件</span></div> |
| | | <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}件</span></div> |
| | | <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}件</span></div> |
| | | </div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="barLegend" |
| | | :series="barSeries1" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis1" |
| | | :yAxis="yAxis1" |
| | | style="height: 270px" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="right-column"> |
| | | <div class="cockpit-panel todo-panel"> |
| | | <div v-if="hasRightPanels" class="right-column"> |
| | | <div v-if="visiblePanels.todo" class="cockpit-panel todo-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">待办事项</div> |
| | | <span class="panel-more">更多</span> |
| | |
| | | <div v-else class="panel-empty">暂无数据</div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel realtime-panel"> |
| | | <div v-if="visiblePanels.realtime" class="cockpit-panel realtime-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">生产实时看板</div> |
| | | <span class="panel-more">更多</span> |
| | |
| | | <div class="realtime-grid"> |
| | | <div class="realtime-item" v-for="item in realtimeBoard" :key="item.key"> |
| | | <el-progress |
| | | type="circle" |
| | | :percentage="item.percent" |
| | | :stroke-width="10" |
| | | :width="94" |
| | | :color="item.color" |
| | | type="circle" |
| | | :percentage="item.percent" |
| | | :stroke-width="10" |
| | | :width="94" |
| | | :color="item.color" |
| | | > |
| | | <template #default> |
| | | <div class="realtime-value digital-number">{{ item.display }}</div> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel quick-panel"> |
| | | <div v-if="visiblePanels.quick" class="cockpit-panel quick-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">快捷功能</div> |
| | | </div> |
| | | <div class="quick-grid"> |
| | | <button |
| | | v-for="item in quickEntries" |
| | | :key="item.label" |
| | | class="quick-item" |
| | | type="button" |
| | | @click="goToQuick(item.path)" |
| | | v-for="item in quickEntries" |
| | | :key="item.label" |
| | | class="quick-item" |
| | | type="button" |
| | | @click="goToQuick(item.path)" |
| | | > |
| | | <span class="quick-icon"> |
| | | <el-icon> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel plan-panel"> |
| | | <div v-if="visiblePanels.plan" class="cockpit-panel plan-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">今日生产计划</div> |
| | | <span class="panel-more">{{ todayPlanList.length }}项</span> |
| | | <span class="panel-more">{{ todayPlanTotal }}项</span> |
| | | </div> |
| | | <ul class="plan-list"> |
| | | <li v-for="item in todayPlanList" :key="item.orderNo" class="plan-item"> |
| | |
| | | </ul> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel receipt-panel"> |
| | | <div class="panel-title">回款与开票分析</div> |
| | | <Echarts |
| | | </div> |
| | | |
| | | <!-- 质量统计 - 占满整行 --> |
| | | <div v-if="visiblePanels.quality" class="cockpit-panel quality-panel full-width"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">质量统计</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <el-radio-button :value="1">周</el-radio-button> |
| | | <el-radio-button :value="2">月</el-radio-button> |
| | | <el-radio-button :value="3">季度</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}吨</span></div> |
| | | <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}吨</span></div> |
| | | <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}吨</span></div> |
| | | </div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="lineLegend" |
| | | :series="lineSeries" |
| | | :tooltip="tooltipLine" |
| | | :xAxis="xAxis2" |
| | | :yAxis="yAxis2" |
| | | style="height: 300px" |
| | | /> |
| | | </div> |
| | | |
| | | :legend="barLegend" |
| | | :series="barSeries1" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis1" |
| | | :yAxis="yAxis1" |
| | | style="height: 270px" |
| | | /> |
| | | </div> |
| | | </section> |
| | | |
| | | <section v-else class="cockpit-panel empty-home-panel"> |
| | | 当前账号没有可展示的首页模块 |
| | | </section> |
| | | |
| | | <el-dialog v-model="processDialogVisible" title="选择工序" width="500px" append-to-body> |
| | | <div class="process-selection-wrapper"> |
| | | <el-checkbox-group v-model="tempProcessIds"> |
| | | <div class="process-grid"> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :value="item.id" border> |
| | | {{ item.name }} |
| | | </el-checkbox> |
| | | </div> |
| | |
| | | UserFilled, |
| | | } from "@element-plus/icons-vue"; |
| | | import Echarts from "@/components/Echarts/echarts.vue"; |
| | | import usePermissionStore from "@/store/modules/permission"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { |
| | | analysisCustomerContractAmounts, |
| | | getAmountHalfYear, |
| | | getBusiness, |
| | | homeTodos, |
| | | processDataProductionStatistics, |
| | | productionOrderProgress, |
| | | productionOverview, |
| | | productionRealtimeBoard, |
| | | qualityInspectionStatistics, |
| | | statisticsReceivablePayable, |
| | | todayProductionPlan, |
| | | } from "@/api/viewIndex.js"; |
| | | import { list } from "@/api/productionManagement/productionProcess"; |
| | | |
| | | const router = useRouter(); |
| | | const userStore = useUserStore(); |
| | | const permissionStore = usePermissionStore(); |
| | | const defaultWelcomeAvatar = new URL("../assets/images/profile.jpg", import.meta.url).href; |
| | | |
| | | const nowTime = ref(""); |
| | |
| | | |
| | | const nowDate = computed(() => (nowTime.value ? nowTime.value.slice(0, 10) : dayjs().format("YYYY-MM-DD"))); |
| | | |
| | | const greetingText = computed(() => { |
| | | const hour = dayjs().hour(); |
| | | if (hour < 6) return "凌晨好"; |
| | | if (hour < 9) return "早上好"; |
| | | if (hour < 12) return "上午好"; |
| | | if (hour < 14) return "中午好"; |
| | | if (hour < 18) return "下午好"; |
| | | return "晚上好"; |
| | | }); |
| | | |
| | | const welcomeAvatar = computed(() => |
| | | welcomeAvatarLoadFailed.value || !userStore.avatar ? defaultWelcomeAvatar : userStore.avatar |
| | | welcomeAvatarLoadFailed.value || !userStore.avatar ? defaultWelcomeAvatar : userStore.avatar |
| | | ); |
| | | |
| | | const handleWelcomeAvatarError = () => { |
| | |
| | | }; |
| | | |
| | | watch( |
| | | () => userStore.avatar, |
| | | () => { |
| | | welcomeAvatarLoadFailed.value = false; |
| | | } |
| | | () => userStore.avatar, |
| | | () => { |
| | | welcomeAvatarLoadFailed.value = false; |
| | | } |
| | | ); |
| | | |
| | | const axisTextColor = "#5f6f86"; |
| | |
| | | monthSaleHaveMoney: 0, |
| | | monthSaleMoney: 0, |
| | | todayInventoryNum: 0, |
| | | lastMonthSaleMoney: 0, |
| | | lastMonthPurchaseMoney: 0, |
| | | }); |
| | | |
| | | const qualityStatisticsObject = ref({ |
| | |
| | | processNum: 0, |
| | | factoryNum: 0, |
| | | }); |
| | | |
| | | const productionOverviewData = ref({ |
| | | totalOutput: 0, |
| | | totalScrap: 0, |
| | | yieldRate: 0, |
| | | }); |
| | | |
| | | const realtimeBoardData = ref({ |
| | | deviceOee: { value: 0, compareYesterday: 0 }, |
| | | orderAchievementRate: { value: 0, compareYesterday: 0 }, |
| | | defectRate: { value: 0, compareYesterday: 0 }, |
| | | }); |
| | | |
| | | const orderProgressMeta = ref({ |
| | | status: "all", |
| | | tab: "all", |
| | | bizDate: null, |
| | | total: 0, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | | }); |
| | | |
| | | const todayPlanList = ref([]); |
| | | const todayPlanTotal = ref(0); |
| | | |
| | | const sum = ref(0); |
| | | const yny = ref(0); |
| | |
| | | }, |
| | | ]); |
| | | |
| | | const lineLegend = { |
| | | show: true, |
| | | textStyle: { color: axisTextColor }, |
| | | data: ["开票", "回款"], |
| | | }; |
| | | |
| | | const xAxis2 = ref([ |
| | | { |
| | | type: "category", |
| | | data: [], |
| | | axisLine: { lineStyle: { color: axisLineColor } }, |
| | | axisLabel: { |
| | | color: axisTextColor, |
| | | interval: 0, |
| | | formatter: (value) => value.replace(/~/g, "\n"), |
| | | }, |
| | | }, |
| | | ]); |
| | | |
| | | const yAxis2 = ref([ |
| | | { |
| | | type: "value", |
| | | splitLine: { lineStyle: { color: splitLineColor } }, |
| | | axisLine: { lineStyle: { color: axisLineColor } }, |
| | | axisLabel: { color: axisTextColor }, |
| | | }, |
| | | ]); |
| | | |
| | | const tooltipLine = { |
| | | trigger: "axis", |
| | | backgroundColor: "rgba(255, 255, 255, 0.97)", |
| | | borderColor: "rgba(148, 163, 184, 0.26)", |
| | | textStyle: { color: "#334155" }, |
| | | }; |
| | | |
| | | const lineSeries = ref([ |
| | | { |
| | | name: "开票", |
| | | type: "line", |
| | | data: [], |
| | | smooth: true, |
| | | itemStyle: { color: "#2563eb" }, |
| | | lineStyle: { width: 2, color: "#2563eb" }, |
| | | showSymbol: true, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(37, 99, 235, 0.24)" }, |
| | | { offset: 1, color: "rgba(37, 99, 235, 0.02)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | { |
| | | name: "回款", |
| | | type: "line", |
| | | data: [], |
| | | smooth: true, |
| | | itemStyle: { color: "#14b8a6" }, |
| | | lineStyle: { width: 2, color: "#14b8a6" }, |
| | | showSymbol: true, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(20, 184, 166, 0.2)" }, |
| | | { offset: 1, color: "rgba(20, 184, 166, 0.02)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | ]); |
| | | |
| | | const pieTooltip = reactive({ |
| | | trigger: "item", |
| | | backgroundColor: "rgba(255, 255, 255, 0.97)", |
| | |
| | | const name = params?.[0]?.name ?? ""; |
| | | const list = Array.isArray(params) ? params : []; |
| | | const lines = list |
| | | .map((p) => { |
| | | const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`; |
| | | return `${colorBox}${p.seriesName}<b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`; |
| | | }) |
| | | .join("<br/>"); |
| | | .map((p) => { |
| | | const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`; |
| | | return `${colorBox}${p.seriesName}<b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`; |
| | | }) |
| | | .join("<br/>"); |
| | | return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>`; |
| | | }, |
| | | }); |
| | |
| | | }); |
| | | |
| | | const processTotals = computed(() => |
| | | processChartData.value.reduce( |
| | | (acc, cur) => { |
| | | acc.input += Number(cur.input || 0); |
| | | acc.scrap += Number(cur.scrap || 0); |
| | | acc.output += Number(cur.output || 0); |
| | | return acc; |
| | | }, |
| | | { input: 0, scrap: 0, output: 0 } |
| | | ) |
| | | processChartData.value.reduce( |
| | | (acc, cur) => { |
| | | acc.input += Number(cur.input || 0); |
| | | acc.scrap += Number(cur.scrap || 0); |
| | | acc.output += Number(cur.output || 0); |
| | | return acc; |
| | | }, |
| | | { input: 0, scrap: 0, output: 0 } |
| | | ) |
| | | ); |
| | | |
| | | const hasProcessData = computed(() => { |
| | |
| | | }); |
| | | }); |
| | | |
| | | // 计算环比增长率 |
| | | const calculateGrowthRate = (current, last) => { |
| | | const currentVal = parseFloat(current) || 0; |
| | | const lastVal = parseFloat(last) || 0; |
| | | if (lastVal === 0) return currentVal > 0 ? 100 : 0; |
| | | return ((currentVal - lastVal) / lastVal * 100).toFixed(1); |
| | | }; |
| | | |
| | | const dashboardCards = computed(() => [ |
| | | { |
| | | key: "sales", |
| | | title: "销售数据", |
| | | desc: "本月销售额(元)", |
| | | value: formatNumber(businessInfo.value.monthSaleMoney), |
| | | subLabel: "未开票金额", |
| | | subValue: formatNumber(businessInfo.value.monthSaleHaveMoney), |
| | | trend: `占比 ${ratioText(businessInfo.value.monthSaleHaveMoney, businessInfo.value.monthSaleMoney)}`, |
| | | subLabel: "上月销售", |
| | | subValue: formatNumber(businessInfo.value.lastMonthSaleMoney), |
| | | trend: (() => { |
| | | const rate = calculateGrowthRate(businessInfo.value.monthSaleMoney, businessInfo.value.lastMonthSaleMoney); |
| | | const num = parseFloat(rate); |
| | | if (num > 0) return `环比 ↑ ${rate}%`; |
| | | if (num < 0) return `环比 ↓ ${Math.abs(num)}%`; |
| | | return "环比持平"; |
| | | })(), |
| | | icon: DataLine, |
| | | visible: visibleModules.value.sales, |
| | | }, |
| | | { |
| | | key: "purchase", |
| | | title: "采购数据", |
| | | desc: "本月采购额(元)", |
| | | value: formatNumber(businessInfo.value.monthPurchaseMoney), |
| | | subLabel: "待付款金额", |
| | | subValue: formatNumber(businessInfo.value.monthPurchaseHaveMoney), |
| | | trend: `占比 ${ratioText( |
| | | businessInfo.value.monthPurchaseHaveMoney, |
| | | businessInfo.value.monthPurchaseMoney |
| | | )}`, |
| | | subLabel: "上月采购", |
| | | subValue: formatNumber(businessInfo.value.lastMonthPurchaseMoney), |
| | | trend: (() => { |
| | | const rate = calculateGrowthRate(businessInfo.value.monthPurchaseMoney, businessInfo.value.lastMonthPurchaseMoney); |
| | | const num = parseFloat(rate); |
| | | if (num > 0) return `环比 ↑ ${rate}%`; |
| | | if (num < 0) return `环比 ↓ ${Math.abs(num)}%`; |
| | | return "环比持平"; |
| | | })(), |
| | | icon: ShoppingCartFull, |
| | | visible: visibleModules.value.procurement, |
| | | }, |
| | | { |
| | | key: "inventory", |
| | |
| | | subValue: formatNumber(businessInfo.value.todayInventoryNum), |
| | | trend: "库存结构持续优化", |
| | | icon: Box, |
| | | visible: visibleModules.value.inventory, |
| | | }, |
| | | { |
| | | key: "production", |
| | | title: "生产总览", |
| | | desc: "累计产出(件)", |
| | | value: formatNumber(processTotals.value.output), |
| | | subLabel: "累计报废", |
| | | subValue: formatNumber(processTotals.value.scrap), |
| | | trend: `良率 ${ratioText(processTotals.value.output, processTotals.value.input)}`, |
| | | icon: Operation, |
| | | }, |
| | | ]); |
| | | ].filter((item) => item.visible)); |
| | | |
| | | const productionOrders = ref([ |
| | | { |
| | | orderNo: "MO-20260518-001", |
| | | productName: "智能控制器", |
| | | planQty: 1000, |
| | | completedQty: 860, |
| | | completionRate: 86, |
| | | deliveryDate: "2026-05-20", |
| | | status: "in_progress", |
| | | }, |
| | | { |
| | | orderNo: "MO-20260518-002", |
| | | productName: "电源模块", |
| | | planQty: 800, |
| | | completedQty: 640, |
| | | completionRate: 80, |
| | | deliveryDate: "2026-05-22", |
| | | status: "in_progress", |
| | | }, |
| | | { |
| | | orderNo: "MO-20260518-003", |
| | | productName: "传感器组件", |
| | | planQty: 500, |
| | | completedQty: 150, |
| | | completionRate: 30, |
| | | deliveryDate: "2026-05-25", |
| | | status: "paused", |
| | | }, |
| | | { |
| | | orderNo: "MO-20260518-004", |
| | | productName: "结构件A", |
| | | planQty: 1200, |
| | | completedQty: 1200, |
| | | completionRate: 100, |
| | | deliveryDate: "2026-05-15", |
| | | status: "completed", |
| | | }, |
| | | ]); |
| | | const productionOrders = ref([]); |
| | | |
| | | const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "paused"]; |
| | | const orderFilterAliasMap = { |
| | | 1: "waiting", |
| | | 2: "inProgress", |
| | | 3: "completed", |
| | | 4: "paused", |
| | | }; |
| | | const orderFilter = ref("all"); |
| | | const filteredOrders = computed(() => { |
| | | if (orderFilter.value === "all") return productionOrders.value; |
| | | return productionOrders.value.filter((item) => item.status === orderFilter.value); |
| | | }); |
| | | const filteredOrders = computed(() => productionOrders.value); |
| | | |
| | | const todayPlanList = computed(() => |
| | | productionOrders.value |
| | | .slice() |
| | | .sort((a, b) => dayjs(a.deliveryDate).valueOf() - dayjs(b.deliveryDate).valueOf()) |
| | | .slice(0, 5) |
| | | ); |
| | | const normalizeOrderFilter = (value, fallback = "all") => { |
| | | const safeFallback = orderFilterOptions.includes(fallback) ? fallback : "all"; |
| | | const text = String(value ?? "").trim(); |
| | | if (orderFilterAliasMap[text]) { |
| | | return orderFilterAliasMap[text]; |
| | | } |
| | | return orderFilterOptions.includes(text) ? text : safeFallback; |
| | | }; |
| | | |
| | | const avgCompletionRate = computed(() => { |
| | | if (!productionOrders.value.length) return 0; |
| | | const total = productionOrders.value.reduce((acc, cur) => acc + Number(cur.completionRate || 0), 0); |
| | | return Number((total / productionOrders.value.length).toFixed(1)); |
| | | }); |
| | | const parseCount = (value) => { |
| | | if (value === null || value === undefined || value === "") return null; |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : null; |
| | | }; |
| | | |
| | | const resolveProgressCount = (rawValue, currentStatus, targetStatus, total) => { |
| | | const count = parseCount(rawValue); |
| | | if (count !== null) return count; |
| | | return currentStatus === targetStatus ? total : 0; |
| | | }; |
| | | |
| | | const getCompareTrend = (value) => { |
| | | const num = Number(value || 0); |
| | | if (num > 0) return "up"; |
| | | if (num < 0) return "down"; |
| | | return "flat"; |
| | | }; |
| | | |
| | | const getCompareText = (value) => { |
| | | const num = Number(value || 0); |
| | | const abs = Math.abs(num).toFixed(2); |
| | | if (num > 0) return `较昨日 ↑ ${abs}%`; |
| | | if (num < 0) return `较昨日 ↓ ${abs}%`; |
| | | return "较昨日 持平"; |
| | | }; |
| | | |
| | | const realtimeBoard = computed(() => { |
| | | const oee = ratioNumber(processTotals.value.output, processTotals.value.input); |
| | | const defectRate = ratioNumber(processTotals.value.scrap, processTotals.value.input); |
| | | const oee = Number(realtimeBoardData.value.deviceOee?.value || 0); |
| | | const orderAchievement = Number(realtimeBoardData.value.orderAchievementRate?.value || 0); |
| | | const defectRate = Number(realtimeBoardData.value.defectRate?.value || 0); |
| | | const oeeCompare = Number(realtimeBoardData.value.deviceOee?.compareYesterday || 0); |
| | | const orderCompare = Number(realtimeBoardData.value.orderAchievementRate?.compareYesterday || 0); |
| | | const defectCompare = Number(realtimeBoardData.value.defectRate?.compareYesterday || 0); |
| | | return [ |
| | | { |
| | | key: "oee", |
| | | label: "设备 OEE", |
| | | percent: clampPercent(oee), |
| | | display: `${oee.toFixed(1)}%`, |
| | | delta: "较昨日 ↑ 4.0%", |
| | | trend: "up", |
| | | display: `${oee.toFixed(2)}%`, |
| | | delta: getCompareText(oeeCompare), |
| | | trend: getCompareTrend(oeeCompare), |
| | | color: "#2d8cff", |
| | | }, |
| | | { |
| | | key: "order", |
| | | label: "订单达成率", |
| | | percent: clampPercent(avgCompletionRate.value), |
| | | display: `${avgCompletionRate.value.toFixed(1)}%`, |
| | | delta: "较昨日 ↑ 2.6%", |
| | | trend: "up", |
| | | percent: clampPercent(orderAchievement), |
| | | display: `${orderAchievement.toFixed(2)}%`, |
| | | delta: getCompareText(orderCompare), |
| | | trend: getCompareTrend(orderCompare), |
| | | color: "#31d2ff", |
| | | }, |
| | | { |
| | | key: "defect", |
| | | label: "不良率", |
| | | percent: clampPercent(defectRate), |
| | | display: `${defectRate.toFixed(1)}%`, |
| | | delta: "较昨日 ↓ 0.5%", |
| | | trend: "down", |
| | | display: `${defectRate.toFixed(2)}%`, |
| | | delta: getCompareText(defectCompare), |
| | | trend: getCompareTrend(defectCompare), |
| | | color: "#f6a23f", |
| | | }, |
| | | ]; |
| | |
| | | }, |
| | | ]); |
| | | |
| | | const quickEntries = [ |
| | | { label: "生产排产", icon: Calendar, path: "/productionManagement/productionDispatching" }, |
| | | { label: "工序报工", icon: EditPen, path: "/productionManagement/productionReporting" }, |
| | | { label: "生产订单", icon: Tickets, path: "/productionManagement/productionOrder" }, |
| | | { label: "物料齐套", icon: Box, path: "/productionManagement/workOrderManagement" }, |
| | | { label: "质量检验", icon: Checked, path: "/qualityManagement/processInspection" }, |
| | | { label: "设备管理", icon: Tools, path: "/equipmentManagement/deviceInfo" }, |
| | | { label: "库存查询", icon: Search, path: "/inventoryManagement/stockManage" }, |
| | | { label: "数据报表", icon: DataAnalysis, path: "/reportAnalysis/reportManagement" }, |
| | | const quickEntryConfigs = [ |
| | | { label: "主生产计划", icon: Calendar, titles: ["主生产计划"], fallbackPath: "/productionPlan/productionPlan" }, |
| | | { label: "生产订单", icon: Tickets, titles: ["生产订单"], fallbackPath: "/productionManagement/productionOrder" }, |
| | | { label: "生产报工", icon: EditPen, titles: ["生产报工"], fallbackPath: "/productionManagement/productionReporting" }, |
| | | { label: "设备台账", icon: Tools, titles: ["设备台账"], fallbackPath: "/equipmentManagement/ledger" }, |
| | | { label: "销售台账", icon: DataLine, titles: ["销售台账"], fallbackPath: "/salesManagement/salesLedger" }, |
| | | { label: "采购台账", icon: ShoppingCartFull, titles: ["采购台账"], fallbackPath: "/procurementManagement/procurementLedger" }, |
| | | { label: "员工台账", icon: UserFilled, titles: ["员工台账", "在职员工台账"], fallbackPath: "/personnelManagement/employeeRecord" }, |
| | | { label: "库存管理", icon: Box, titles: ["库存管理"], fallbackPath: "/inventoryManagement/stockManage" }, |
| | | ]; |
| | | |
| | | const normalizeMenuTitle = (title) => String(title || "").replace(/\s+/g, "").trim(); |
| | | const normalizeRoutePath = (path) => |
| | | String(path || "") |
| | | .trim() |
| | | .replace(/\/+/g, "/") |
| | | .replace(/\/$/, "") |
| | | .toLowerCase(); |
| | | |
| | | const resolveRoutePath = (route, parentPath = "") => { |
| | | const currentPath = String(route?.path || "").trim(); |
| | | if (!currentPath) return parentPath || ""; |
| | | if (/^(https?:)?\/\//.test(currentPath)) return currentPath; |
| | | if (currentPath.startsWith("/")) return currentPath; |
| | | const basePath = parentPath && parentPath !== "/" ? parentPath.replace(/\/$/, "") : ""; |
| | | return `${basePath}/${currentPath}`.replace(/\/+/g, "/"); |
| | | }; |
| | | |
| | | const collectAccessibleRoutes = (routes = [], parentPath = "") => { |
| | | const items = []; |
| | | (routes || []).forEach((route) => { |
| | | if (!route) return; |
| | | const fullPath = resolveRoutePath(route, parentPath); |
| | | const title = route.meta?.title ?? route.title ?? ""; |
| | | if (title && fullPath && !String(route.redirect || "").includes("noRedirect")) { |
| | | items.push({ |
| | | title: normalizeMenuTitle(title), |
| | | path: normalizeRoutePath(fullPath), |
| | | }); |
| | | } |
| | | if (Array.isArray(route.children) && route.children.length > 0) { |
| | | items.push(...collectAccessibleRoutes(route.children, fullPath)); |
| | | } |
| | | }); |
| | | return items; |
| | | }; |
| | | |
| | | const accessibleMenuRoutes = computed(() => { |
| | | const routePool = |
| | | permissionStore.defaultRoutes?.length > 0 |
| | | ? permissionStore.defaultRoutes |
| | | : permissionStore.sidebarRouters?.length > 0 |
| | | ? permissionStore.sidebarRouters |
| | | : permissionStore.routes; |
| | | return collectAccessibleRoutes(routePool || []); |
| | | }); |
| | | |
| | | const moduleAccessConfig = { |
| | | sales: { |
| | | titles: ["销售管理", "销售台账"], |
| | | pathPrefixes: ["/salesmanagement"], |
| | | }, |
| | | procurement: { |
| | | titles: ["采购管理", "采购台账"], |
| | | pathPrefixes: ["/procurementmanagement"], |
| | | }, |
| | | inventory: { |
| | | titles: ["库存管理"], |
| | | pathPrefixes: ["/inventorymanagement"], |
| | | }, |
| | | production: { |
| | | titles: ["生产管理", "主生产计划", "生产订单", "生产报工"], |
| | | pathPrefixes: ["/productionmanagement", "/productionplan"], |
| | | }, |
| | | quality: { |
| | | titles: ["质量管理"], |
| | | pathPrefixes: ["/qualitymanagement"], |
| | | }, |
| | | equipment: { |
| | | titles: ["设备管理", "设备台账"], |
| | | pathPrefixes: ["/equipmentmanagement"], |
| | | }, |
| | | personnel: { |
| | | titles: ["人事管理", "员工台账", "在职员工台账"], |
| | | pathPrefixes: ["/personnelmanagement"], |
| | | }, |
| | | approval: { |
| | | titles: ["协同审批", "待办事项"], |
| | | pathPrefixes: ["/collaborativeapproval"], |
| | | }, |
| | | finance: { |
| | | titles: ["财务管理", "财务分析", "回款管理", "开票管理"], |
| | | pathPrefixes: ["/financesuite", "/financialmanagement"], |
| | | }, |
| | | }; |
| | | |
| | | const hasModuleAccess = (config) => |
| | | accessibleMenuRoutes.value.some((route) => { |
| | | const matchedTitle = (config.titles || []).some((title) => route.title === normalizeMenuTitle(title)); |
| | | const matchedPath = (config.pathPrefixes || []).some( |
| | | (prefix) => route.path === prefix || route.path.startsWith(`${prefix}/`) |
| | | ); |
| | | return matchedTitle || matchedPath; |
| | | }); |
| | | |
| | | const visibleModules = computed(() => ({ |
| | | sales: hasModuleAccess(moduleAccessConfig.sales), |
| | | procurement: hasModuleAccess(moduleAccessConfig.procurement), |
| | | inventory: hasModuleAccess(moduleAccessConfig.inventory), |
| | | production: hasModuleAccess(moduleAccessConfig.production), |
| | | quality: hasModuleAccess(moduleAccessConfig.quality), |
| | | equipment: hasModuleAccess(moduleAccessConfig.equipment), |
| | | personnel: hasModuleAccess(moduleAccessConfig.personnel), |
| | | approval: hasModuleAccess(moduleAccessConfig.approval), |
| | | finance: hasModuleAccess(moduleAccessConfig.finance), |
| | | })); |
| | | |
| | | const visiblePanels = computed(() => ({ |
| | | process: visibleModules.value.production, |
| | | order: visibleModules.value.production, |
| | | contract: visibleModules.value.sales, |
| | | quality: visibleModules.value.quality, |
| | | todo: visibleModules.value.approval, |
| | | realtime: visibleModules.value.production, |
| | | quick: quickEntries.value.length > 0, |
| | | plan: visibleModules.value.production, |
| | | |
| | | })); |
| | | |
| | | const hasLeftPanels = computed( |
| | | () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract |
| | | ); |
| | | const hasRightPanels = computed( |
| | | () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan |
| | | ); |
| | | const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value || visiblePanels.value.quality); |
| | | |
| | | const quickEntries = computed(() => |
| | | quickEntryConfigs |
| | | .map((item) => { |
| | | const targetRoute = accessibleMenuRoutes.value.find((route) => |
| | | item.titles.some((title) => route.title === normalizeMenuTitle(title)) |
| | | ); |
| | | const resolvedPath = targetRoute?.path || ""; |
| | | return resolvedPath |
| | | ? { |
| | | label: item.label, |
| | | icon: item.icon, |
| | | path: resolvedPath, |
| | | } |
| | | : null; |
| | | }) |
| | | .filter(Boolean) |
| | | ); |
| | | |
| | | const updateNowTime = () => { |
| | | nowTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | |
| | | |
| | | const getOrderStatusText = (status) => { |
| | | const mapping = { |
| | | in_progress: "进行中", |
| | | completed: "已完成", |
| | | paused: "暂停", |
| | | 1: "待开始", |
| | | 2: "进行中", |
| | | 3: "已完成", |
| | | 4: "已暂停", |
| | | }; |
| | | return mapping[status] || "未知"; |
| | | }; |
| | | |
| | | const getOrderStatusType = (status) => { |
| | | const mapping = { |
| | | in_progress: "success", |
| | | completed: "primary", |
| | | paused: "warning", |
| | | 1: "info", |
| | | 2: "success", |
| | | 3: "primary", |
| | | 4: "warning", |
| | | }; |
| | | return mapping[status] || "info"; |
| | | }; |
| | | |
| | | const formatDueDate = (value) => { |
| | | if (!value) return "--"; |
| | | const date = dayjs(value); |
| | | return date.isValid() ? date.format("YYYY-MM-DD") : "--"; |
| | | }; |
| | | |
| | | const mapOrderProgressRecord = (item = {}) => ({ |
| | | orderNo: item.orderNo || "--", |
| | | productName: item.productName || "--", |
| | | planQty: Number(item.plannedQuantity || 0), |
| | | completedQty: Number(item.completedQuantity || 0), |
| | | completionRate: clampPercent(Number(item.completionRate || 0)), |
| | | deliveryDate: formatDueDate(item.dueDate), |
| | | status: Number(item.status || 0), |
| | | statusLabel: item.statusLabel || "", |
| | | }); |
| | | |
| | | const mapTodayPlanRecord = (item = {}) => ({ |
| | | orderNo: item.orderNo || "--", |
| | | productName: item.productName || "--", |
| | | planQty: Number(item.plannedQuantity || 0), |
| | | deliveryDate: formatDueDate(item.dueDate), |
| | | status: Number(item.status || 0), |
| | | statusLabel: item.statusLabel || "", |
| | | }); |
| | | |
| | | const refreshProductionOverview = async () => { |
| | | try { |
| | | const res = await productionOverview(); |
| | | const data = res?.data || {}; |
| | | productionOverviewData.value = { |
| | | totalOutput: Number(data.totalOutput || 0), |
| | | totalScrap: Number(data.totalScrap || 0), |
| | | yieldRate: Number(data.yieldRate || 0), |
| | | }; |
| | | } catch { |
| | | productionOverviewData.value = { |
| | | totalOutput: 0, |
| | | totalScrap: 0, |
| | | yieldRate: 0, |
| | | }; |
| | | } |
| | | }; |
| | | |
| | | const refreshProductionRealtimeBoard = async () => { |
| | | try { |
| | | const res = await productionRealtimeBoard(); |
| | | const data = res?.data || {}; |
| | | realtimeBoardData.value = { |
| | | deviceOee: { |
| | | value: Number(data.deviceOee?.value || 0), |
| | | compareYesterday: Number(data.deviceOee?.compareYesterday || 0), |
| | | }, |
| | | orderAchievementRate: { |
| | | value: Number(data.orderAchievementRate?.value || 0), |
| | | compareYesterday: Number(data.orderAchievementRate?.compareYesterday || 0), |
| | | }, |
| | | defectRate: { |
| | | value: Number(data.defectRate?.value || 0), |
| | | compareYesterday: Number(data.defectRate?.compareYesterday || 0), |
| | | }, |
| | | }; |
| | | } catch { |
| | | realtimeBoardData.value = { |
| | | deviceOee: { value: 0, compareYesterday: 0 }, |
| | | orderAchievementRate: { value: 0, compareYesterday: 0 }, |
| | | defectRate: { value: 0, compareYesterday: 0 }, |
| | | }; |
| | | } |
| | | }; |
| | | |
| | | const refreshProductionOrderProgress = async () => { |
| | | try { |
| | | const res = await productionOrderProgress({ |
| | | status: orderFilter.value, |
| | | tab: orderFilter.value, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }); |
| | | const data = res?.data || {}; |
| | | const statusValue = normalizeOrderFilter(data.status, orderFilter.value); |
| | | const total = Number(data.total || 0); |
| | | orderProgressMeta.value = { |
| | | status: statusValue, |
| | | tab: data.tab || orderFilter.value, |
| | | bizDate: data.bizDate || null, |
| | | total, |
| | | pageNum: Number(data.pageNum || 1), |
| | | pageSize: Number(data.pageSize || 10), |
| | | waitingCount: resolveProgressCount(data.waitingCount, statusValue, "waiting", total), |
| | | inProgressCount: resolveProgressCount(data.inProgressCount, statusValue, "inProgress", total), |
| | | completedCount: resolveProgressCount(data.completedCount, statusValue, "completed", total), |
| | | pausedCount: resolveProgressCount(data.pausedCount, statusValue, "paused", total), |
| | | }; |
| | | productionOrders.value = (data.records || []).map(mapOrderProgressRecord); |
| | | } catch { |
| | | orderProgressMeta.value = { |
| | | status: orderFilter.value, |
| | | tab: orderFilter.value, |
| | | bizDate: null, |
| | | total: 0, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | | }; |
| | | productionOrders.value = []; |
| | | } |
| | | }; |
| | | |
| | | const refreshTodayProductionPlan = async () => { |
| | | try { |
| | | const res = await todayProductionPlan({ |
| | | limit: 4, |
| | | planDate: nowDate.value, |
| | | }); |
| | | const data = res?.data || {}; |
| | | todayPlanTotal.value = Number(data.total || 0); |
| | | todayPlanList.value = (data.records || []).map(mapTodayPlanRecord); |
| | | } catch { |
| | | todayPlanTotal.value = 0; |
| | | todayPlanList.value = []; |
| | | } |
| | | }; |
| | | |
| | | const getBusinessData = async () => { |
| | |
| | | todoList.value = res.data || []; |
| | | }; |
| | | |
| | | const statisticsReceivable = async () => { |
| | | const res = await statisticsReceivablePayable({ type: 1 }); |
| | | barSeries.value[0].data = [ |
| | | { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } }, |
| | | ]; |
| | | }; |
| | | |
| | | const qualityStatisticsInfo = async () => { |
| | | const res = await qualityInspectionStatistics({ type: qualityRange.value }); |
| | | xAxis1.value[0].data = []; |
| | | barSeries1.value[0].data = []; |
| | | barSeries1.value[1].data = []; |
| | | barSeries1.value[2].data = []; |
| | | (res.data.item || []).forEach((item) => { |
| | | const data = res.data || {}; |
| | | (data.item || []).forEach((item) => { |
| | | xAxis1.value[0].data.push(item.date); |
| | | barSeries1.value[0].data.push(item.supplierNum); |
| | | barSeries1.value[1].data.push(item.processNum); |
| | | barSeries1.value[2].data.push(item.factoryNum); |
| | | }); |
| | | qualityStatisticsObject.value.supplierNum = res.data.supplierNum; |
| | | qualityStatisticsObject.value.processNum = res.data.processNum; |
| | | qualityStatisticsObject.value.factoryNum = res.data.factoryNum; |
| | | }; |
| | | |
| | | const getAmountHalfYearNum = async () => { |
| | | const res = await getAmountHalfYear(); |
| | | const monthName = []; |
| | | const receiptAmount = []; |
| | | const invoiceAmount = []; |
| | | (res.data || []).forEach((item) => { |
| | | monthName.push(item.month); |
| | | receiptAmount.push(item.receiptAmount); |
| | | invoiceAmount.push(item.invoiceAmount); |
| | | }); |
| | | |
| | | xAxis2.value[0].data = monthName.map((item) => item.replace(/~/g, "\n~")); |
| | | lineSeries.value[0].data = invoiceAmount; |
| | | lineSeries.value[1].data = receiptAmount; |
| | | qualityStatisticsObject.value.supplierNum = data.supplierNum || 0; |
| | | qualityStatisticsObject.value.processNum = data.processNum || 0; |
| | | qualityStatisticsObject.value.factoryNum = data.factoryNum || 0; |
| | | }; |
| | | |
| | | const getProcessList = async () => { |
| | |
| | | }; |
| | | |
| | | const goToQuick = (path) => { |
| | | if (!path) return; |
| | | router.push(path).catch(() => {}); |
| | | }; |
| | | |
| | | watch(orderFilter, () => { |
| | | if (visiblePanels.value.order) { |
| | | refreshProductionOrderProgress(); |
| | | } |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | updateNowTime(); |
| | | clockTimer = setInterval(updateNowTime, 1000); |
| | | getBusinessData(); |
| | | analysisCustomer(); |
| | | todoInfoS(); |
| | | statisticsReceivable(); |
| | | qualityStatisticsInfo(); |
| | | getAmountHalfYearNum(); |
| | | getProcessList(); |
| | | refreshProcessStats(); |
| | | if (dashboardCards.value.length > 0) { |
| | | getBusinessData(); |
| | | } |
| | | if (visibleModules.value.production) { |
| | | refreshProductionOverview(); |
| | | } |
| | | if (visiblePanels.value.contract) { |
| | | analysisCustomer(); |
| | | } |
| | | if (visiblePanels.value.todo) { |
| | | todoInfoS(); |
| | | } |
| | | if (visiblePanels.value.quality) { |
| | | qualityStatisticsInfo(); |
| | | } |
| | | if (visiblePanels.value.process) { |
| | | getProcessList(); |
| | | refreshProcessStats(); |
| | | } |
| | | if (visiblePanels.value.order) { |
| | | refreshProductionOrderProgress(); |
| | | } |
| | | if (visiblePanels.value.realtime) { |
| | | refreshProductionRealtimeBoard(); |
| | | } |
| | | if (visiblePanels.value.plan) { |
| | | refreshTodayProductionPlan(); |
| | | } |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | overflow-x: clip; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .digital-number { |
| | |
| | | min-height: 92px; |
| | | padding: 18px 22px; |
| | | background: |
| | | radial-gradient(circle at 8% 20%, rgba(59, 130, 246, 0.12), transparent 32%), |
| | | linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(239, 246, 255, 0.88)); |
| | | radial-gradient(circle at 8% 20%, rgba(59, 130, 246, 0.12), transparent 32%), |
| | | linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(239, 246, 255, 0.88)); |
| | | } |
| | | |
| | | .welcome-user { |
| | |
| | | |
| | | .stats-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 14px; |
| | | } |
| | | |
| | |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | background: |
| | | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 340 40' preserveAspectRatio='none'%3E%3Cpath d='M0 31C20 16 44 36 66 24C87 12 107 31 129 18C148 8 169 28 193 16C214 5 237 25 259 14C280 3 306 19 340 8' fill='none' stroke='%236ea4ee' stroke-width='1.5'/%3E%3C/svg%3E") |
| | | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 340 40' preserveAspectRatio='none'%3E%3Cpath d='M0 31C20 16 44 36 66 24C87 12 107 31 129 18C148 8 169 28 193 16C214 5 237 25 259 14C280 3 306 19 340 8' fill='none' stroke='%236ea4ee' stroke-width='1.5'/%3E%3C/svg%3E") |
| | | center / 100% 100% no-repeat; |
| | | } |
| | | |
| | |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | max-height: 230px; |
| | | max-height: 380px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | |
| | | color: var(--text-tertiary); |
| | | } |
| | | |
| | | .empty-home-panel { |
| | | min-height: 220px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #64748b; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .main-grid { |
| | | display: grid; |
| | | grid-template-columns: minmax(0, 1fr) 400px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | align-items: start; |
| | | } |
| | | |
| | | .left-column, |
| | | .left-column { |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .right-column { |
| | | width: 400px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .full-width { |
| | | width: 100%; |
| | | flex-basis: 100%; |
| | | } |
| | | |
| | | .process-panel { |
| | |
| | | border: 1px solid rgba(148, 163, 184, 0.24); |
| | | border-radius: 14px; |
| | | background: |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.9)), |
| | | repeating-linear-gradient( |
| | | to right, |
| | | rgba(148, 163, 184, 0.07) 0, |
| | | rgba(148, 163, 184, 0.07) 1px, |
| | | transparent 1px, |
| | | transparent 48px |
| | | ), |
| | | repeating-linear-gradient( |
| | | to bottom, |
| | | rgba(148, 163, 184, 0.06) 0, |
| | | rgba(148, 163, 184, 0.06) 1px, |
| | | transparent 1px, |
| | | transparent 34px |
| | | ); |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.9)), |
| | | repeating-linear-gradient( |
| | | to right, |
| | | rgba(148, 163, 184, 0.07) 0, |
| | | rgba(148, 163, 184, 0.07) 1px, |
| | | transparent 1px, |
| | | transparent 48px |
| | | ), |
| | | repeating-linear-gradient( |
| | | to bottom, |
| | | rgba(148, 163, 184, 0.06) 0, |
| | | rgba(148, 163, 184, 0.06) 1px, |
| | | transparent 1px, |
| | | transparent 34px |
| | | ); |
| | | overflow: hidden; |
| | | padding: 10px; |
| | | } |
| | |
| | | color: #f59e0b; |
| | | } |
| | | |
| | | .realtime-delta.flat { |
| | | color: #64748b; |
| | | } |
| | | |
| | | .warning-list { |
| | | margin-top: 10px; |
| | | display: flex; |
| | |
| | | } |
| | | |
| | | .quick-panel { |
| | | min-height: 0; |
| | | min-height: 280px; |
| | | } |
| | | |
| | | .table-progress { |
| | |
| | | |
| | | .quality-panel { |
| | | min-height: 0; |
| | | } |
| | | |
| | | .receipt-panel { |
| | | min-height: 340px; |
| | | } |
| | | |
| | | .plan-list { |
| | |
| | | } |
| | | |
| | | @media (max-width: 1600px) { |
| | | .main-grid { |
| | | grid-template-columns: minmax(0, 1fr) 380px; |
| | | .right-column { |
| | | width: 380px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1366px) { |
| | | .main-grid { |
| | | grid-template-columns: minmax(0, 1fr) 340px; |
| | | .right-column { |
| | | width: 340px; |
| | | } |
| | | |
| | | .stats-grid { |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 992px) { |
| | | .stats-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .main-grid { |
| | | grid-template-columns: 1fr; |
| | | .left-column, |
| | | .right-column { |
| | | width: 100%; |
| | | } |
| | | |
| | | .right-column { |