|
|
@@ -1,17 +1,15 @@
|
|
|
-<template>
|
|
|
+<template>
|
|
|
<div class="semi-product">
|
|
|
- <!-- 半成品概览 -->
|
|
|
<el-row :gutter="20" class="overview-row">
|
|
|
<el-col :span="6">
|
|
|
<el-card class="stat-card">
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-icon" style="background-color: #409eff;">
|
|
|
- <!-- Replace Box icon with text symbol -->
|
|
|
- <span style="font-size:28px; color:#fff;">📦</span>
|
|
|
+ <i class="el-icon-box" style="font-size: 28px;"></i>
|
|
|
</div>
|
|
|
<div class="stat-info">
|
|
|
<div class="stat-label">半成品总数</div>
|
|
|
- <div class="stat-value">-</div>
|
|
|
+ <div class="stat-value">{{ stats.totalSemiProducts }}</div>
|
|
|
<div class="stat-unit">种</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -21,12 +19,11 @@
|
|
|
<el-card class="stat-card">
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-icon" style="background-color: #67c23a;">
|
|
|
- <!-- Replace Goods icon with text symbol -->
|
|
|
- <span style="font-size:28px; color:#fff;">📦</span>
|
|
|
+ <i class="el-icon-goods" style="font-size: 28px;"></i>
|
|
|
</div>
|
|
|
<div class="stat-info">
|
|
|
<div class="stat-label">库存数量</div>
|
|
|
- <div class="stat-value">-</div>
|
|
|
+ <div class="stat-value">{{ stats.totalQuantity }}</div>
|
|
|
<div class="stat-unit">件</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -36,12 +33,11 @@
|
|
|
<el-card class="stat-card">
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-icon" style="background-color: #909399;">
|
|
|
- <!-- Replace Goods icon with text symbol -->
|
|
|
- <span style="font-size:28px; color:#fff;">📦</span>
|
|
|
+ <i class="el-icon-s-promotion" style="font-size: 28px;"></i>
|
|
|
</div>
|
|
|
<div class="stat-info">
|
|
|
<div class="stat-label">预计可生产成品</div>
|
|
|
- <div class="stat-value">-</div>
|
|
|
+ <div class="stat-value">{{ stats.estimatedFinished }}</div>
|
|
|
<div class="stat-unit">件</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -49,22 +45,22 @@
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
- <!-- 搜索与操作栏 -->
|
|
|
<el-card class="search-card">
|
|
|
<el-row :gutter="20" align="middle">
|
|
|
<el-col :span="6">
|
|
|
<el-input
|
|
|
+ v-model="searchKeyword"
|
|
|
placeholder="搜索半成品名称/编码"
|
|
|
clearable
|
|
|
+ @clear="handleSearch"
|
|
|
>
|
|
|
- <template #prefix>
|
|
|
- <!-- Replace Search icon with text symbol -->
|
|
|
- <span style="font-size:16px; color:#909399;">🔍</span>
|
|
|
+ <template slot="prefix">
|
|
|
+ <i class="el-icon-search"></i>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-col>
|
|
|
<el-col :span="4">
|
|
|
- <el-select placeholder="状态筛选" clearable>
|
|
|
+ <el-select v-model="filterStatus" placeholder="状态筛选" clearable>
|
|
|
<el-option label="全部" value="" />
|
|
|
<el-option label="在库" value="in-stock" />
|
|
|
<el-option label="在制" value="in-production" />
|
|
|
@@ -73,210 +69,713 @@
|
|
|
</el-select>
|
|
|
</el-col>
|
|
|
<el-col :span="4">
|
|
|
- <el-select placeholder="类别筛选" clearable>
|
|
|
+ <el-select v-model="filterCategory" placeholder="类别筛选" clearable>
|
|
|
<el-option label="全部" value="" />
|
|
|
<el-option label="电子元件" value="electronics" />
|
|
|
<el-option label="机械部件" value="mechanics" />
|
|
|
- <el-option label="外壳模组" value="housing" />
|
|
|
+ <el-option label="外壳模块" value="housing" />
|
|
|
<el-option label="电路板" value="pcb" />
|
|
|
</el-select>
|
|
|
</el-col>
|
|
|
<el-col :span="6">
|
|
|
- <el-button type="primary">
|
|
|
- <!-- Replace Search icon with text -->
|
|
|
- 🔍 搜索
|
|
|
+ <el-button type="primary" @click="handleSearch">
|
|
|
+ <i class="el-icon-search"></i> 搜索
|
|
|
</el-button>
|
|
|
- <el-button>
|
|
|
- <!-- Replace RefreshLeft icon with text -->
|
|
|
- 🔄 重置
|
|
|
+ <el-button @click="handleReset">
|
|
|
+ <i class="el-icon-refresh-left"></i> 重置
|
|
|
</el-button>
|
|
|
</el-col>
|
|
|
<el-col :span="4" style="text-align: right;">
|
|
|
- <el-button type="success">
|
|
|
- <!-- Replace Plus icon with text -->
|
|
|
- ➕ 新增半成品
|
|
|
+ <el-button type="success" @click="handleAdd">
|
|
|
+ <i class="el-icon-plus"></i> 新增半成品
|
|
|
</el-button>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-card>
|
|
|
|
|
|
- <!-- 可组装成品能力 -->
|
|
|
<el-card class="assembly-card" style="margin-bottom: 20px;">
|
|
|
- <template #header>
|
|
|
+ <template slot="header">
|
|
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
|
<span>可组装成品能力(基于入库库存)</span>
|
|
|
- <el-button type="primary" size="small">刷新</el-button>
|
|
|
+ <el-button type="primary" size="small" @click="fetchAssemblyCapacities">刷新</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <el-table stripe style="width: 100%">
|
|
|
- <el-table-column label="成品编码" width="180" />
|
|
|
- <el-table-column label="名称" width="220" />
|
|
|
- <el-table-column label="可组装数量" width="140" />
|
|
|
- <el-table-column label="所需半成品及库存" />
|
|
|
+ <el-table :data="assemblyCapacities" stripe style="width: 100%">
|
|
|
+ <el-table-column prop="product_code" label="成品编码" width="180" />
|
|
|
+ <el-table-column prop="product_name" label="名称" width="220" />
|
|
|
+ <el-table-column prop="capacity" label="可组装数量" width="140" />
|
|
|
+ <el-table-column label="所需半成品及库存">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div>
|
|
|
+ <el-tag
|
|
|
+ v-for="c in scope.row.components"
|
|
|
+ :key="scope.row.product_code + c.code"
|
|
|
+ size="small"
|
|
|
+ style="margin: 2px 4px;"
|
|
|
+ >
|
|
|
+ {{ c.code }}: {{ c.available }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
|
|
|
- <!-- 半成品列表 -->
|
|
|
<el-card class="table-card">
|
|
|
- <el-table stripe style="width: 100%">
|
|
|
+ <el-table :data="semiProductList" stripe style="width: 100%">
|
|
|
<el-table-column type="selection" width="55" />
|
|
|
- <el-table-column label="编码" width="120" fixed />
|
|
|
- <el-table-column label="半成品名称" width="200" />
|
|
|
- <el-table-column label="类别" width="120" />
|
|
|
- <el-table-column label="库存数量" width="100" />
|
|
|
- <el-table-column label="安全库存" width="100" />
|
|
|
- <el-table-column label="状态" width="100" />
|
|
|
- <el-table-column label="可用于组装" width="150" />
|
|
|
- <el-table-column label="供应商" width="150" />
|
|
|
- <el-table-column label="采购周期" width="100" />
|
|
|
- <el-table-column label="单价(元)" width="100" />
|
|
|
- <el-table-column label="库存价值(元)" width="120" />
|
|
|
+ <el-table-column prop="code" label="编码" width="120" fixed />
|
|
|
+ <el-table-column prop="name" label="半成品名称" width="200" />
|
|
|
+ <el-table-column label="类别" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag>{{ getCategoryText(scope.row.category) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="quantity" label="库存数量" width="100" sortable />
|
|
|
+ <el-table-column prop="safeStock" label="安全库存" width="100" />
|
|
|
+ <el-table-column label="状态" width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag :type="getStatusType(scope.row.status)">
|
|
|
+ {{ getStatusText(scope.row.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="可用于组装" width="150">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-popover placement="top" :width="300" trigger="hover">
|
|
|
+ <template slot="reference">
|
|
|
+ <el-tag type="info" style="cursor: pointer;">
|
|
|
+ {{ scope.row.usedForProducts.length }} 种成品
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <div>
|
|
|
+ <p style="font-weight: bold; margin-bottom: 8px;">可组装成品</p>
|
|
|
+ <el-tag
|
|
|
+ v-for="product in scope.row.usedForProducts"
|
|
|
+ :key="product"
|
|
|
+ size="small"
|
|
|
+ style="margin: 2px 4px;"
|
|
|
+ >
|
|
|
+ {{ product }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </el-popover>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="supplier" label="供应商" width="150" />
|
|
|
+ <el-table-column prop="leadTime" label="采购周期" width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ scope.row.leadTime }} 天
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="unitPrice" label="单价(元)" width="100" />
|
|
|
+ <el-table-column prop="totalValue" label="库存价值(元)" width="120" sortable />
|
|
|
<el-table-column label="操作" width="240" fixed="right">
|
|
|
- <template #default>
|
|
|
- <el-button type="primary" size="small">详情</el-button>
|
|
|
- <el-button type="success" size="small">编辑</el-button>
|
|
|
- <el-button type="warning" size="small">组装</el-button>
|
|
|
- <el-button type="danger" size="small">删除</el-button>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
|
|
|
+ <el-button type="success" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
|
|
+ <el-button type="warning" size="small" @click="handleAssemble(scope.row)">组装</el-button>
|
|
|
+ <el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
<el-pagination
|
|
|
- :current-page="1"
|
|
|
- :page-size="10"
|
|
|
- :total="0"
|
|
|
+ :current-page="currentPage"
|
|
|
+ :page-size="pageSize"
|
|
|
+ :total="totalItems"
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
style="margin-top: 20px; justify-content: flex-end;"
|
|
|
/>
|
|
|
</el-card>
|
|
|
|
|
|
- <!-- 半成品分析图表 -->
|
|
|
<el-row :gutter="20" class="charts-row">
|
|
|
<el-col :span="12">
|
|
|
<el-card>
|
|
|
- <template #header>
|
|
|
+ <template slot="header">
|
|
|
<span>半成品库存分布</span>
|
|
|
</template>
|
|
|
- <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
|
|
|
- 暂无图表数据
|
|
|
- </div>
|
|
|
+ <div ref="stockDistributionChart" class="chart" />
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-card>
|
|
|
- <template #header>
|
|
|
+ <template slot="header">
|
|
|
<span>半成品使用趋势</span>
|
|
|
</template>
|
|
|
- <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
|
|
|
- 暂无图表数据
|
|
|
- </div>
|
|
|
+ <div ref="usageTrendChart" class="chart" />
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
+ <el-card class="bom-card">
|
|
|
+ <template slot="header">
|
|
|
+ <div class="card-header">
|
|
|
+ <span>半成品BOM关系图</span>
|
|
|
+ <el-select
|
|
|
+ v-model="selectedBomProduct"
|
|
|
+ placeholder="选择成品查看BOM"
|
|
|
+ style="width: 250px;"
|
|
|
+ >
|
|
|
+ <el-option label="智能手表 Pro X1" value="watch" />
|
|
|
+ <el-option label="无线耳机 Elite" value="earphone" />
|
|
|
+ <el-option label="智能音箱" value="speaker" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div ref="bomRelationChart" class="chart" style="height: 400px;"></div>
|
|
|
+ </el-card>
|
|
|
|
|
|
-
|
|
|
- <!-- 半成品周转分析 -->
|
|
|
<el-card class="turnover-card">
|
|
|
- <template #header>
|
|
|
+ <template slot="header">
|
|
|
<span>半成品周转效率分析</span>
|
|
|
</template>
|
|
|
- <el-table stripe>
|
|
|
- <el-table-column label="半成品名称" width="200" />
|
|
|
- <el-table-column label="平均周转天数" width="150" />
|
|
|
- <el-table-column label="使用率" width="120" />
|
|
|
- <el-table-column label="月消耗量" width="120" />
|
|
|
- <el-table-column label="再订货点" width="120" />
|
|
|
- <el-table-column label="库存状态" width="120" />
|
|
|
- <el-table-column label="优化建议" min-width="250" />
|
|
|
+ <el-table :data="turnoverAnalysis" stripe>
|
|
|
+ <el-table-column prop="name" label="半成品名称" width="200" />
|
|
|
+ <el-table-column prop="avgTurnover" label="平均周转天数" width="150" sortable>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span :style="{ color: getTurnoverColor(scope.row.avgTurnover) }">
|
|
|
+ {{ scope.row.avgTurnover }} 天
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="usageRate" label="使用率" width="120" sortable>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-progress :percentage="scope.row.usageRate" :color="getProgressColor(scope.row.usageRate)" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="monthlyConsumption" label="月消耗量" width="120" />
|
|
|
+ <el-table-column prop="reorderPoint" label="再订货点" width="120" />
|
|
|
+ <el-table-column label="库存状态" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag :type="getStockStatusType(scope.row.stockStatus)">
|
|
|
+ {{ scope.row.stockStatus }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="suggestion" label="优化建议" min-width="250" />
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup>
|
|
|
- // REMOVED ALL ICON IMPORTS (no @element-plus/icons-vue dependency)
|
|
|
+<script>
|
|
|
+import request from '@/utils/request'
|
|
|
+import { Message, MessageBox } from 'element-ui'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+require('echarts/theme/macarons')
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'StorageSemiProduct',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ stats: {
|
|
|
+ totalSemiProducts: 0,
|
|
|
+ totalQuantity: 0,
|
|
|
+ estimatedFinished: 0
|
|
|
+ },
|
|
|
+ searchKeyword: '',
|
|
|
+ filterStatus: '',
|
|
|
+ filterCategory: '',
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ semiProductList: [
|
|
|
+ {
|
|
|
+ code: 'SEM-101',
|
|
|
+ name: '电路板 PCB-A',
|
|
|
+ category: 'pcb',
|
|
|
+ quantity: 3200,
|
|
|
+ safeStock: 2000,
|
|
|
+ status: 'in-stock',
|
|
|
+ usedForProducts: ['智能手表 Pro X1', '智能手环'],
|
|
|
+ supplier: '深圳电子有限公司',
|
|
|
+ leadTime: 15,
|
|
|
+ unitPrice: 45.8,
|
|
|
+ totalValue: 146560
|
|
|
+ },
|
|
|
+ {
|
|
|
+ code: 'SEM-102',
|
|
|
+ name: '显示屏模块',
|
|
|
+ category: 'electronics',
|
|
|
+ quantity: 2800,
|
|
|
+ safeStock: 1500,
|
|
|
+ status: 'in-stock',
|
|
|
+ usedForProducts: ['智能手表 Pro X1'],
|
|
|
+ supplier: '京东方科技',
|
|
|
+ leadTime: 20,
|
|
|
+ unitPrice: 128.5,
|
|
|
+ totalValue: 359800
|
|
|
+ },
|
|
|
+ {
|
|
|
+ code: 'SEM-103',
|
|
|
+ name: '锂电池组',
|
|
|
+ category: 'electronics',
|
|
|
+ quantity: 1200,
|
|
|
+ safeStock: 1800,
|
|
|
+ status: 'pending',
|
|
|
+ usedForProducts: ['智能手表 Pro X1', '无线耳机 Elite'],
|
|
|
+ supplier: '宁德时代',
|
|
|
+ leadTime: 25,
|
|
|
+ unitPrice: 68.0,
|
|
|
+ totalValue: 81600
|
|
|
+ },
|
|
|
+ {
|
|
|
+ code: 'SEM-104',
|
|
|
+ name: '金属外壳',
|
|
|
+ category: 'housing',
|
|
|
+ quantity: 4500,
|
|
|
+ safeStock: 3000,
|
|
|
+ status: 'in-stock',
|
|
|
+ usedForProducts: ['智能手表 Pro X1', '智能音箱'],
|
|
|
+ supplier: '精密制造厂',
|
|
|
+ leadTime: 12,
|
|
|
+ unitPrice: 35.2,
|
|
|
+ totalValue: 158400
|
|
|
+ },
|
|
|
+ {
|
|
|
+ code: 'SEM-105',
|
|
|
+ name: '蓝牙模块',
|
|
|
+ category: 'electronics',
|
|
|
+ quantity: 580,
|
|
|
+ safeStock: 1000,
|
|
|
+ status: 'in-production',
|
|
|
+ usedForProducts: ['无线耳机 Elite', '智能音箱', '智能手表 Pro X1'],
|
|
|
+ supplier: '高通技术',
|
|
|
+ leadTime: 30,
|
|
|
+ unitPrice: 85.5,
|
|
|
+ totalValue: 49590
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ assemblyCapacities: [],
|
|
|
+ selectedBomProduct: 'watch',
|
|
|
+ turnoverAnalysis: [
|
|
|
+ {
|
|
|
+ name: '电路板 PCB-A',
|
|
|
+ avgTurnover: 18,
|
|
|
+ usageRate: 85,
|
|
|
+ monthlyConsumption: 850,
|
|
|
+ reorderPoint: 1200,
|
|
|
+ stockStatus: '正常',
|
|
|
+ suggestion: '库存充足,周转良好'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '显示屏模块',
|
|
|
+ avgTurnover: 22,
|
|
|
+ usageRate: 78,
|
|
|
+ monthlyConsumption: 650,
|
|
|
+ reorderPoint: 1000,
|
|
|
+ stockStatus: '正常',
|
|
|
+ suggestion: '建议保持当前库存水平'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '锂电池组',
|
|
|
+ avgTurnover: 35,
|
|
|
+ usageRate: 92,
|
|
|
+ monthlyConsumption: 720,
|
|
|
+ reorderPoint: 1500,
|
|
|
+ stockStatus: '偏低',
|
|
|
+ suggestion: '建议立即补货,避免缺货风险'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '金属外壳',
|
|
|
+ avgTurnover: 15,
|
|
|
+ usageRate: 65,
|
|
|
+ monthlyConsumption: 580,
|
|
|
+ reorderPoint: 2000,
|
|
|
+ stockStatus: '充足',
|
|
|
+ suggestion: '库存充裕,可适当减少采购量'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '蓝牙模块',
|
|
|
+ avgTurnover: 45,
|
|
|
+ usageRate: 95,
|
|
|
+ monthlyConsumption: 480,
|
|
|
+ reorderPoint: 800,
|
|
|
+ stockStatus: '紧张',
|
|
|
+ suggestion: '使用率高,建议增加安全库存'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ stockDistributionOption: {
|
|
|
+ tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
|
|
+ legend: { orient: 'vertical', left: 'left' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '半成品类别',
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['40%', '70%'],
|
|
|
+ avoidLabelOverlap: false,
|
|
|
+ itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
|
|
+ label: { show: true, formatter: '{b}: {d}%' },
|
|
|
+ data: [
|
|
|
+ { value: 6800, name: '电子元件', itemStyle: { color: '#5470c6' } },
|
|
|
+ { value: 4500, name: '机械部件', itemStyle: { color: '#91cc75' } },
|
|
|
+ { value: 3200, name: '外壳模块', itemStyle: { color: '#fac858' } },
|
|
|
+ { value: 2620, name: '电路板', itemStyle: { color: '#ee6666' } }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ usageTrendOption: {
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { data: ['入库', '出库', '组装消耗'] },
|
|
|
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
|
+ xAxis: { type: 'category', boundaryGap: false, data: ['1月', '2月', '3月', '4月', '5月', '6月'] },
|
|
|
+ yAxis: { type: 'value', name: '数量' },
|
|
|
+ series: [
|
|
|
+ { name: '入库', type: 'line', data: [1200, 1350, 1180, 1450, 1600, 1520], smooth: true, itemStyle: { color: '#67c23a' } },
|
|
|
+ { name: '出库', type: 'line', data: [980, 1120, 1050, 1280, 1380, 1450], smooth: true, itemStyle: { color: '#e6a23c' } },
|
|
|
+ { name: '组装消耗', type: 'line', data: [850, 980, 920, 1150, 1250, 1320], smooth: true, itemStyle: { color: '#409eff' } }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ bomDataMap: {
|
|
|
+ watch: {
|
|
|
+ name: '智能手表 Pro X1',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ name: '电子模块',
|
|
|
+ children: [
|
|
|
+ { name: '电路板 PCB-A\n库存: 3200' },
|
|
|
+ { name: '显示屏模块\n库存: 2800' },
|
|
|
+ { name: '锂电池组\n库存: 1200' },
|
|
|
+ { name: '蓝牙模块\n库存: 580' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '结构件',
|
|
|
+ children: [
|
|
|
+ { name: '金属外壳\n库存: 4500' },
|
|
|
+ { name: '表带\n库存: 5200' },
|
|
|
+ { name: '按键组件\n库存: 6800' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '包装材料',
|
|
|
+ children: [
|
|
|
+ { name: '包装盒\n库存: 8500' },
|
|
|
+ { name: '说明书\n库存: 9200' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ earphone: {
|
|
|
+ name: '无线耳机 Elite',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ name: '电子模块',
|
|
|
+ children: [
|
|
|
+ { name: '蓝牙模块\n库存: 580' },
|
|
|
+ { name: '锂电池组\n库存: 1200' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '结构件',
|
|
|
+ children: [
|
|
|
+ { name: '耳机壳体\n库存: 2600' },
|
|
|
+ { name: '充电仓\n库存: 1800' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '包装材料',
|
|
|
+ children: [
|
|
|
+ { name: '包装盒\n库存: 4200' },
|
|
|
+ { name: '说明书\n库存: 5100' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ speaker: {
|
|
|
+ name: '智能音箱',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ name: '电子模块',
|
|
|
+ children: [
|
|
|
+ { name: '主控板\n库存: 900' },
|
|
|
+ { name: '蓝牙模块\n库存: 580' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '结构件',
|
|
|
+ children: [
|
|
|
+ { name: '金属外壳\n库存: 4500' },
|
|
|
+ { name: '喇叭组件\n库存: 1200' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '包装材料',
|
|
|
+ children: [
|
|
|
+ { name: '包装盒\n库存: 6000' },
|
|
|
+ { name: '说明书\n库存: 9200' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ stockChart: null,
|
|
|
+ usageChart: null,
|
|
|
+ bomChart: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ totalItems() {
|
|
|
+ return this.semiProductList.length
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.fetchStats()
|
|
|
+ this.fetchAssemblyCapacities()
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initCharts()
|
|
|
+ this.renderCharts()
|
|
|
+ })
|
|
|
+ window.addEventListener('resize', this.resizeCharts)
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('resize', this.resizeCharts)
|
|
|
+ if (this.stockChart) this.stockChart.dispose()
|
|
|
+ if (this.usageChart) this.usageChart.dispose()
|
|
|
+ if (this.bomChart) this.bomChart.dispose()
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ selectedBomProduct() {
|
|
|
+ this.updateBomChart()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ normalizeResponse(res) {
|
|
|
+ if (!res) return null
|
|
|
+ if (res.code === 200) return res.data
|
|
|
+ if (res.data) return res.data
|
|
|
+ return null
|
|
|
+ },
|
|
|
+ async fetchAssemblyCapacities() {
|
|
|
+ try {
|
|
|
+ const res = await request({ url: '/api/semi-product/assembly-capacity', method: 'get' })
|
|
|
+ const data = this.normalizeResponse(res)
|
|
|
+ if (Array.isArray(data)) this.assemblyCapacities = data
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取可组装能力失败:', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchStats() {
|
|
|
+ try {
|
|
|
+ const res = await request({ url: '/api/semi-product/statistics', method: 'get' })
|
|
|
+ const data = this.normalizeResponse(res)
|
|
|
+ if (data) this.stats = { ...this.stats, ...data }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取半成品统计失败:', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ initCharts() {
|
|
|
+ if (this.$refs.stockDistributionChart && !this.stockChart) {
|
|
|
+ this.stockChart = echarts.init(this.$refs.stockDistributionChart, 'macarons')
|
|
|
+ }
|
|
|
+ if (this.$refs.usageTrendChart && !this.usageChart) {
|
|
|
+ this.usageChart = echarts.init(this.$refs.usageTrendChart, 'macarons')
|
|
|
+ }
|
|
|
+ if (this.$refs.bomRelationChart && !this.bomChart) {
|
|
|
+ this.bomChart = echarts.init(this.$refs.bomRelationChart, 'macarons')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ renderCharts() {
|
|
|
+ if (this.stockChart) this.stockChart.setOption(this.stockDistributionOption, true)
|
|
|
+ if (this.usageChart) this.usageChart.setOption(this.usageTrendOption, true)
|
|
|
+ this.updateBomChart()
|
|
|
+ },
|
|
|
+ buildBomOption() {
|
|
|
+ const data = this.bomDataMap[this.selectedBomProduct] || this.bomDataMap.watch
|
|
|
+ return {
|
|
|
+ tooltip: { trigger: 'item', triggerOn: 'mousemove' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'tree',
|
|
|
+ data: [data],
|
|
|
+ top: '5%',
|
|
|
+ left: '10%',
|
|
|
+ bottom: '5%',
|
|
|
+ right: '20%',
|
|
|
+ symbolSize: 10,
|
|
|
+ label: { position: 'left', verticalAlign: 'middle', align: 'right', fontSize: 12 },
|
|
|
+ leaves: { label: { position: 'right', verticalAlign: 'middle', align: 'left' } },
|
|
|
+ emphasis: { focus: 'descendant' },
|
|
|
+ expandAndCollapse: true,
|
|
|
+ animationDuration: 550,
|
|
|
+ animationDurationUpdate: 750
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ updateBomChart() {
|
|
|
+ if (!this.bomChart) return
|
|
|
+ this.bomChart.setOption(this.buildBomOption(), true)
|
|
|
+ },
|
|
|
+ resizeCharts() {
|
|
|
+ if (this.stockChart) this.stockChart.resize()
|
|
|
+ if (this.usageChart) this.usageChart.resize()
|
|
|
+ if (this.bomChart) this.bomChart.resize()
|
|
|
+ },
|
|
|
+ getCategoryText(category) {
|
|
|
+ const map = {
|
|
|
+ electronics: '电子元件',
|
|
|
+ mechanics: '机械部件',
|
|
|
+ housing: '外壳模块',
|
|
|
+ pcb: '电路板'
|
|
|
+ }
|
|
|
+ return map[category] || '其他'
|
|
|
+ },
|
|
|
+ getStatusType(status) {
|
|
|
+ const map = {
|
|
|
+ 'in-stock': 'success',
|
|
|
+ 'in-production': 'warning',
|
|
|
+ pending: 'info',
|
|
|
+ used: 'danger'
|
|
|
+ }
|
|
|
+ return map[status] || 'info'
|
|
|
+ },
|
|
|
+ getStatusText(status) {
|
|
|
+ const map = {
|
|
|
+ 'in-stock': '在库',
|
|
|
+ 'in-production': '在制',
|
|
|
+ pending: '待检',
|
|
|
+ used: '已用'
|
|
|
+ }
|
|
|
+ return map[status] || '未知'
|
|
|
+ },
|
|
|
+ getTurnoverColor(days) {
|
|
|
+ if (days <= 20) return '#67c23a'
|
|
|
+ if (days <= 35) return '#e6a23c'
|
|
|
+ return '#f56c6c'
|
|
|
+ },
|
|
|
+ getProgressColor(rate) {
|
|
|
+ if (rate >= 80) return '#67c23a'
|
|
|
+ if (rate >= 60) return '#e6a23c'
|
|
|
+ return '#f56c6c'
|
|
|
+ },
|
|
|
+ getStockStatusType(status) {
|
|
|
+ const map = {
|
|
|
+ 正常: 'success',
|
|
|
+ 充足: 'success',
|
|
|
+ 偏低: 'warning',
|
|
|
+ 紧张: 'danger'
|
|
|
+ }
|
|
|
+ return map[status] || 'info'
|
|
|
+ },
|
|
|
+ handleSearch() {
|
|
|
+ Message.success('搜索成功')
|
|
|
+ },
|
|
|
+ handleReset() {
|
|
|
+ this.searchKeyword = ''
|
|
|
+ this.filterStatus = ''
|
|
|
+ this.filterCategory = ''
|
|
|
+ Message.info('已重置筛选条件')
|
|
|
+ },
|
|
|
+ handleAdd() {
|
|
|
+ Message.info('打开新增半成品对话框')
|
|
|
+ },
|
|
|
+ handleDetail(row) {
|
|
|
+ Message.info(`查看详情: ${row.name}`)
|
|
|
+ },
|
|
|
+ handleEdit(row) {
|
|
|
+ Message.info(`编辑: ${row.name}`)
|
|
|
+ },
|
|
|
+ handleAssemble(row) {
|
|
|
+ Message.info(`进入组装流程: ${row.name}`)
|
|
|
+ },
|
|
|
+ handleDelete(row) {
|
|
|
+ MessageBox.confirm(`确认删除半成品"${row.name}"?`, '警告', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ Message.success('删除成功')
|
|
|
+ }).catch(() => {})
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
- .semi-product {
|
|
|
- width: 100%;
|
|
|
- }
|
|
|
+.semi-product {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
|
|
|
- .overview-row {
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
+.overview-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
|
|
|
- .stat-card {
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s;
|
|
|
- }
|
|
|
+.stat-card {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
|
|
|
- .stat-card:hover {
|
|
|
- transform: translateY(-5px);
|
|
|
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
- }
|
|
|
+.stat-card:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
|
|
|
- .stat-content {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 15px;
|
|
|
- }
|
|
|
+.stat-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
|
|
|
- .stat-icon {
|
|
|
- width: 56px;
|
|
|
- height: 56px;
|
|
|
- border-radius: 12px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
+.stat-icon {
|
|
|
+ width: 56px;
|
|
|
+ height: 56px;
|
|
|
+ border-radius: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
|
|
|
- .stat-info {
|
|
|
- flex: 1;
|
|
|
- }
|
|
|
+.stat-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
|
|
|
- .stat-label {
|
|
|
- font-size: 14px;
|
|
|
- color: #909399;
|
|
|
- margin-bottom: 8px;
|
|
|
- }
|
|
|
+.stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
|
|
|
- .stat-value {
|
|
|
- font-size: 28px;
|
|
|
- font-weight: bold;
|
|
|
- color: #303133;
|
|
|
- line-height: 1;
|
|
|
- }
|
|
|
+.stat-value {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
|
|
|
- .stat-unit {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- margin-top: 4px;
|
|
|
- }
|
|
|
+.stat-unit {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
|
|
|
- .search-card {
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
|
|
|
- .table-card {
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
+.table-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
|
|
|
- .charts-row {
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
+.charts-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
|
|
|
- .card-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- }
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
|
|
|
- .bom-card,
|
|
|
- .turnover-card {
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
+.bom-card,
|
|
|
+.turnover-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart {
|
|
|
+ height: 300px;
|
|
|
+}
|
|
|
</style>
|