| | |
| | | <div class="kpi-label">总生产成本</div> |
| | | <div class="kpi-value">¥{{ formatMoney(overview.totalCost) }}</div> |
| | | </div> |
| | | <div class="kpi-item kpi-raw"> |
| | | <div class="kpi-label">原料成本</div> |
| | | <div class="kpi-value">¥{{ formatMoney(overview.rawCost) }}</div> |
| | | </div> |
| | | <div class="kpi-item kpi-aux"> |
| | | <div class="kpi-label">辅料成本</div> |
| | | <div class="kpi-value">¥{{ formatMoney(overview.auxCost) }}</div> |
| | | <div class="kpi-item kpi-avg"> |
| | | <div class="kpi-label">每订单平均成本</div> |
| | | <div class="kpi-value">¥{{ formatMoney(overview.avgCostPerOrder) }}</div> |
| | | </div> |
| | | <div class="kpi-item kpi-order"> |
| | | <div class="kpi-label">订单数量</div> |
| | |
| | | </template> |
| | | <el-table :data="categorySummary" stripe class="lux-table" height="260"> |
| | | <el-table-column prop="category" label="产品类别" min-width="140" /> |
| | | <el-table-column prop="rawCost" label="原料成本(元)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ formatMoney(scope.row.rawCost) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="auxCost" label="辅料成本(元)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ formatMoney(scope.row.auxCost) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCost" label="总成本(元)" align="right"> |
| | | <el-table-column prop="totalCost" label="成本(元)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">¥{{ formatMoney(scope.row.totalCost) }}</span> |
| | | </template> |
| | |
| | | <el-table-column prop="timeLabel" :label="timeColumnLabel" min-width="110" /> |
| | | <el-table-column prop="category" label="产品类别" min-width="120" /> |
| | | <el-table-column prop="orderNo" label="生产订单" min-width="150" /> |
| | | <el-table-column prop="rawCost" label="原料成本(元)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ formatMoney(scope.row.rawCost) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="auxCost" label="辅料成本(元)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ formatMoney(scope.row.auxCost) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCost" label="总成本(元)" align="right"> |
| | | <el-table-column prop="totalCost" label="成本(元)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">¥{{ formatMoney(scope.row.totalCost) }}</span> |
| | | </template> |
| | |
| | | |
| | | <el-drawer |
| | | v-model="detailVisible" |
| | | title="生产成本拆分明细" |
| | | size="560px" |
| | | :with-header="false" |
| | | class="detail-drawer" |
| | | size="760px" |
| | | :close-on-click-modal="true" |
| | | :close-on-press-escape="true" |
| | | destroy-on-close |
| | | > |
| | | <div v-if="detailRow" class="drawer-head"> |
| | | <div><b>{{ timeColumnLabel }}:</b>{{ detailRow.timeLabel }}</div> |
| | | <div><b>产品类别:</b>{{ detailRow.category }}</div> |
| | | <div><b>生产订单:</b>{{ detailRow.orderNo }}</div> |
| | | <div class="meta-item"> |
| | | <span class="meta-label">{{ timeColumnLabel }}</span> |
| | | <span class="meta-value">{{ detailRow.timeLabel }}</span> |
| | | </div> |
| | | <div class="meta-item"> |
| | | <span class="meta-label">产品类别</span> |
| | | <span class="meta-value">{{ detailRow.category }}</span> |
| | | </div> |
| | | <div class="meta-item"> |
| | | <span class="meta-label">生产订单</span> |
| | | <span class="meta-value">{{ detailRow.orderNo }}</span> |
| | | </div> |
| | | </div> |
| | | <el-table :data="detailMaterials" class="lux-table" stripe> |
| | | <el-table-column prop="materialName" label="物料名称" min-width="120" /> |
| | | <el-table-column prop="materialType" label="类型" width="80" /> |
| | | <el-table-column prop="quantity" label="投入量" align="right"> |
| | | <el-table-column prop="quantity" label="投入量" align="right" min-width="140"> |
| | | <template #default="scope"> |
| | | {{ formatNumber(scope.row.quantity, 2) }} {{ scope.row.unit }} |
| | | <span class="quantity-cell"> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span> |
| | | <span class="quantity-unit">{{ scope.row.unit }}</span> |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitPrice" label="单价(元)" align="right"> |
| | |
| | | {{ formatNumber(scope.row.unitPrice, 2) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="cost" label="成本(元)" align="right"> |
| | | <el-table-column prop="cost" label="成本(元)" align="right" min-width="132"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">¥{{ formatMoney(scope.row.cost) }}</span> |
| | | <span class="cost-value no-wrap-money">¥{{ formatMoney(scope.row.cost) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="drawer-foot"> |
| | | <span>原料:¥{{ formatMoney(detailRawCost) }}</span> |
| | | <span>辅料:¥{{ formatMoney(detailAuxCost) }}</span> |
| | | <span class="strong">合计:¥{{ formatMoney(detailTotalCost) }}</span> |
| | | <div class="foot-item total"> |
| | | <span class="foot-label">成本合计</span> |
| | | <span class="foot-value no-wrap-money">¥{{ formatMoney(detailTotalCost) }}</span> |
| | | </div> |
| | | </div> |
| | | </el-drawer> |
| | | </div> |
| | |
| | | const key = keyFn(item); |
| | | if (!map.has(key)) { |
| | | map.set(key, { |
| | | rawCost: 0, |
| | | auxCost: 0, |
| | | totalCost: 0, |
| | | materials: [], |
| | | }); |
| | | } |
| | | const bucket = map.get(key); |
| | | if (item.materialType === "原料") bucket.rawCost += item.cost; |
| | | if (item.materialType === "辅料") bucket.auxCost += item.cost; |
| | | bucket.totalCost += item.cost; |
| | | bucket.materials.push(item); |
| | | } |
| | |
| | | timeLabel, |
| | | category, |
| | | orderNo, |
| | | rawCost: val.rawCost, |
| | | auxCost: val.auxCost, |
| | | totalCost: val.totalCost, |
| | | materials: val.materials, |
| | | }); |
| | |
| | | for (const [category, val] of map) { |
| | | rows.push({ |
| | | category, |
| | | rawCost: val.rawCost, |
| | | auxCost: val.auxCost, |
| | | totalCost: val.totalCost, |
| | | }); |
| | | } |
| | |
| | | }); |
| | | |
| | | const overview = computed(() => { |
| | | const rawCost = filteredRecords.value |
| | | .filter((item) => item.materialType === "原料") |
| | | .reduce((sum, item) => sum + item.cost, 0); |
| | | const auxCost = filteredRecords.value |
| | | .filter((item) => item.materialType === "辅料") |
| | | .reduce((sum, item) => sum + item.cost, 0); |
| | | const orderCount = new Set(filteredRecords.value.map((item) => item.orderNo)).size; |
| | | const totalCost = filteredRecords.value.reduce((sum, item) => sum + item.cost, 0); |
| | | return { |
| | | rawCost, |
| | | auxCost, |
| | | totalCost: rawCost + auxCost, |
| | | totalCost, |
| | | orderCount, |
| | | avgCostPerOrder: orderCount === 0 ? 0 : totalCost / orderCount, |
| | | }; |
| | | }); |
| | | |
| | |
| | | |
| | | const detailMaterials = computed(() => detailRow.value?.materials || []); |
| | | |
| | | const detailRawCost = computed(() => |
| | | detailMaterials.value |
| | | .filter((item) => item.materialType === "原料") |
| | | .reduce((sum, item) => sum + item.cost, 0) |
| | | ); |
| | | const detailAuxCost = computed(() => |
| | | detailMaterials.value |
| | | .filter((item) => item.materialType === "辅料") |
| | | .reduce((sum, item) => sum + item.cost, 0) |
| | | ); |
| | | const detailTotalCost = computed(() => |
| | | detailMaterials.value.reduce((sum, item) => sum + item.cost, 0) |
| | | ); |
| | |
| | | }; |
| | | |
| | | const handleExport = () => { |
| | | const headers = [timeColumnLabel.value, "产品类别", "生产订单", "原料成本", "辅料成本", "总成本"]; |
| | | const headers = [timeColumnLabel.value, "产品类别", "生产订单", "成本(元)"]; |
| | | const lines = tableData.value.map((row) => |
| | | [ |
| | | row.timeLabel, |
| | | row.category, |
| | | row.orderNo, |
| | | row.rawCost.toFixed(2), |
| | | row.auxCost.toFixed(2), |
| | | row.totalCost.toFixed(2), |
| | | ].join(",") |
| | | ); |
| | |
| | | |
| | | .kpi-strip { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | |
| | | background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | |
| | | .kpi-aux { |
| | | background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(255, 255, 255, 0.86)); |
| | | .kpi-avg { |
| | | background: linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | |
| | | .kpi-order { |
| | |
| | | color: var(--lux-danger); |
| | | } |
| | | |
| | | .no-wrap-money { |
| | | display: inline-block; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .drawer-head { |
| | | display: grid; |
| | | gap: 8px; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .meta-item { |
| | | padding: 10px 12px; |
| | | border-radius: 10px; |
| | | border: 1px solid var(--lux-border); |
| | | background: rgba(15, 23, 42, 0.03); |
| | | display: grid; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .meta-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .meta-value { |
| | | color: var(--lux-text); |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .material-type-tag { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 46px; |
| | | height: 24px; |
| | | padding: 0 8px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .material-type-tag.is-raw { |
| | | color: #15803d; |
| | | background: rgba(22, 163, 74, 0.12); |
| | | } |
| | | |
| | | .material-type-tag.is-aux { |
| | | color: #b45309; |
| | | background: rgba(245, 158, 11, 0.16); |
| | | } |
| | | |
| | | .quantity-value { |
| | | font-weight: 700; |
| | | color: var(--lux-text); |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .quantity-cell { |
| | | display: inline-flex; |
| | | align-items: baseline; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .quantity-unit { |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .drawer-foot { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 18px; |
| | | gap: 12px; |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px dashed var(--lux-border); |
| | | } |
| | | |
| | | .foot-item { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 7px 16px; |
| | | border-radius: 999px; |
| | | border: 1px solid var(--lux-border); |
| | | background: #fff; |
| | | } |
| | | |
| | | .foot-label { |
| | | color: var(--lux-subtle); |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .foot-value { |
| | | color: var(--lux-text); |
| | | font-weight: 700; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .foot-item.total { |
| | | border-color: rgba(47, 111, 237, 0.26); |
| | | background: rgba(47, 111, 237, 0.08); |
| | | } |
| | | |
| | | .foot-item.total .foot-value { |
| | | color: #1e3a8a; |
| | | font-size: 18px; |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .pagination-container { |
| | |
| | | .filter-layout { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .drawer-head { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .drawer-foot { |
| | | justify-content: flex-start; |
| | | flex-wrap: wrap; |
| | | } |
| | | } |
| | | </style> |