<template>
|
<div class="production-cost-page">
|
<el-card class="filter-card"
|
shadow="never">
|
<template #header>
|
<div class="card-head">
|
<div class="card-head-left">
|
<el-icon class="card-icon ui-icon">
|
<DataLine />
|
</el-icon>
|
<span class="card-title">生产成本核算</span>
|
<span class="subtle">成本 = Σ 投入量 × 对应单价</span>
|
</div>
|
<!-- <div class="card-head-right">
|
<el-radio-group
|
v-model="statisticsType"
|
size="small"
|
@change="handleTypeChange"
|
>
|
<el-radio-button label="day">按日</el-radio-button>
|
<el-radio-button label="month">按月</el-radio-button>
|
</el-radio-group>
|
</div> -->
|
</div>
|
</template>
|
<div class="filter-layout">
|
<el-form :model="searchForm"
|
:inline="true"
|
class="filter-form">
|
<el-form-item label="时间范围">
|
<el-date-picker v-if="statisticsType === 'day'"
|
v-model="searchForm.dateRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
value-format="YYYY-MM-DD"
|
class="w-260"
|
@change="handleQuery" />
|
<el-date-picker v-else
|
v-model="searchForm.monthRange"
|
type="monthrange"
|
range-separator="至"
|
start-placeholder="开始月份"
|
end-placeholder="结束月份"
|
value-format="YYYY-MM"
|
class="w-260"
|
@change="handleQuery" />
|
</el-form-item>
|
<el-form-item label="产品类别">
|
<el-select v-model="searchForm.dictCode"
|
clearable
|
filterable
|
placeholder="全部类别"
|
class="w-180"
|
@change="handleQuery">
|
<el-option v-for="item in categoryOptions"
|
:key="item.dictCode"
|
:label="item.dictLabel"
|
:value="item.dictCode" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="生产订单">
|
<el-select v-model="searchForm.productOrderId"
|
clearable
|
filterable
|
placeholder="全部订单"
|
class="w-180"
|
@change="handleQuery">
|
<el-option v-for="order in orderList"
|
:key="order.id"
|
:label="`${order.npsNo}`"
|
:value="order.id" />
|
</el-select>
|
</el-form-item>
|
</el-form>
|
<div class="filter-actions">
|
<el-button class="lux-btn"
|
type="primary"
|
@click="handleQuery">
|
刷新
|
</el-button>
|
<el-button class="lux-btn"
|
@click="handleReset">重置</el-button>
|
<el-button class="lux-btn"
|
type="success"
|
plain
|
@click="handleExport">
|
导出
|
</el-button>
|
</div>
|
</div>
|
</el-card>
|
<el-card class="panel-card"
|
shadow="never">
|
<div class="kpi-strip">
|
<div class="kpi-item kpi-total">
|
<div class="kpi-label">总生产成本</div>
|
<div class="kpi-value">¥{{ formatMoney(overview.totalCost) }}</div>
|
</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>
|
<div class="kpi-value">{{ overview.orderCount }}</div>
|
</div>
|
</div>
|
</el-card>
|
<el-row :gutter="14"
|
class="summary-row">
|
<el-col :span="12">
|
<el-card class="table-card"
|
shadow="never">
|
<template #header>
|
<div class="panel-head">
|
<span class="card-title">按产品类别汇总</span>
|
</div>
|
</template>
|
<el-table :data="categorySummary"
|
stripe
|
class="lux-table"
|
height="260">
|
<el-table-column prop="name"
|
label="产品类别"
|
min-width="140" />
|
<el-table-column prop="quantity"
|
label="用量"
|
align="right"
|
min-width="120">
|
<template #default="scope">
|
<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="totalCost"
|
label="成本(元)"
|
align="right">
|
<template #default="scope">
|
<span class="cost-value">¥{{ formatMoney(scope.row.totalCost) }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
</el-col>
|
<el-col :span="12">
|
<el-card class="table-card"
|
shadow="never">
|
<template #header>
|
<div class="panel-head">
|
<span class="card-title">按生产订单汇总</span>
|
</div>
|
</template>
|
<el-table :data="orderSummary"
|
stripe
|
class="lux-table"
|
height="260">
|
<el-table-column prop="name"
|
label="生产订单"
|
min-width="150" />
|
<el-table-column prop="strength"
|
label="产品类别"
|
min-width="120" />
|
<el-table-column prop="quantity"
|
label="用量"
|
align="right"
|
min-width="120">
|
<template #default="scope">
|
<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="totalCost"
|
label="总成本(元)"
|
align="right">
|
<template #default="scope">
|
<span class="cost-value">¥{{ formatMoney(scope.row.totalCost) }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
</el-col>
|
</el-row>
|
<el-row :gutter="14"
|
class="summary-row">
|
<el-col :span="12">
|
<el-card class="table-card"
|
shadow="never">
|
<template #header>
|
<div class="panel-head">
|
<span class="card-title">产品物料Top10</span>
|
</div>
|
</template>
|
<div ref="topOrdersChartRef"
|
class="chart-container"
|
style="height: 300px;"></div>
|
</el-card>
|
</el-col>
|
<el-col :span="12">
|
<el-card class="table-card"
|
shadow="never">
|
<template #header>
|
<div class="panel-head">
|
<span class="card-title">生产订单Top10</span>
|
</div>
|
</template>
|
</el-card>
|
</el-col>
|
</el-row>
|
<el-drawer v-model="detailVisible"
|
: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 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="quantity"
|
label="投入量"
|
align="right"
|
min-width="140">
|
<template #default="scope">
|
<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">
|
<template #default="scope">
|
{{ formatNumber(scope.row.unitPrice, 2) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="cost"
|
label="成本(元)"
|
align="right"
|
min-width="132">
|
<template #default="scope">
|
<span class="cost-value no-wrap-money">¥{{ formatMoney(scope.row.cost) }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div class="drawer-foot">
|
<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>
|
</template>
|
|
<script setup>
|
import { computed, reactive, ref, watch, onMounted, nextTick } from "vue";
|
import { DataLine } from "@element-plus/icons-vue";
|
import { ElMessage } from "element-plus";
|
import * as echarts from "echarts";
|
import { getDicts } from "@/api/system/dict/data.js";
|
import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
|
import {
|
getProductionCostSummary,
|
getProductionCostAggregateByProduct,
|
getProductionCostAggregateByOrder,
|
getProductionCostTopOrders,
|
} from "@/api/costAccounting/productionCost.js";
|
|
const statisticsType = ref("day");
|
|
const getDefaultDateRange = () => {
|
const end = new Date();
|
const start = new Date();
|
start.setDate(start.getDate() - 6);
|
return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)];
|
};
|
|
const getDefaultMonthRange = () => {
|
const end = new Date();
|
const start = new Date();
|
start.setMonth(start.getMonth() - 2);
|
return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
|
};
|
|
const searchForm = reactive({
|
dateRange: getDefaultDateRange(),
|
monthRange: getDefaultMonthRange(),
|
dictCode: "",
|
productOrderId: "",
|
});
|
|
const sourceRecords = ref([
|
{
|
date: "2026-03-17",
|
category: "瓷砖",
|
orderNo: "PO-260317-01",
|
materialName: "陶瓷粉",
|
materialType: "原料",
|
quantity: 1200,
|
unit: "kg",
|
unitPrice: 2.8,
|
},
|
{
|
date: "2026-03-17",
|
category: "瓷砖",
|
orderNo: "PO-260317-01",
|
materialName: "釉料",
|
materialType: "辅料",
|
quantity: 180,
|
unit: "kg",
|
unitPrice: 8.6,
|
},
|
{
|
date: "2026-03-17",
|
category: "水泥",
|
orderNo: "PO-260317-02",
|
materialName: "熟料",
|
materialType: "原料",
|
quantity: 2200,
|
unit: "kg",
|
unitPrice: 1.36,
|
},
|
{
|
date: "2026-03-17",
|
category: "水泥",
|
orderNo: "PO-260317-02",
|
materialName: "石膏",
|
materialType: "辅料",
|
quantity: 260,
|
unit: "kg",
|
unitPrice: 0.92,
|
},
|
{
|
date: "2026-03-18",
|
category: "砂浆",
|
orderNo: "PO-260318-01",
|
materialName: "机制砂",
|
materialType: "原料",
|
quantity: 1600,
|
unit: "kg",
|
unitPrice: 0.58,
|
},
|
{
|
date: "2026-03-18",
|
category: "砂浆",
|
orderNo: "PO-260318-01",
|
materialName: "保水剂",
|
materialType: "辅料",
|
quantity: 65,
|
unit: "kg",
|
unitPrice: 11.4,
|
},
|
{
|
date: "2026-03-19",
|
category: "瓷砖",
|
orderNo: "PO-260319-01",
|
materialName: "陶瓷粉",
|
materialType: "原料",
|
quantity: 980,
|
unit: "kg",
|
unitPrice: 2.9,
|
},
|
{
|
date: "2026-03-19",
|
category: "瓷砖",
|
orderNo: "PO-260319-01",
|
materialName: "色料",
|
materialType: "辅料",
|
quantity: 42,
|
unit: "kg",
|
unitPrice: 15.8,
|
},
|
{
|
date: "2026-03-19",
|
category: "砂浆",
|
orderNo: "PO-260319-03",
|
materialName: "机制砂",
|
materialType: "原料",
|
quantity: 1400,
|
unit: "kg",
|
unitPrice: 0.56,
|
},
|
{
|
date: "2026-03-19",
|
category: "砂浆",
|
orderNo: "PO-260319-03",
|
materialName: "减水剂",
|
materialType: "辅料",
|
quantity: 74,
|
unit: "kg",
|
unitPrice: 7.2,
|
},
|
{
|
date: "2026-03-20",
|
category: "水泥",
|
orderNo: "PO-260320-02",
|
materialName: "熟料",
|
materialType: "原料",
|
quantity: 2400,
|
unit: "kg",
|
unitPrice: 1.33,
|
},
|
{
|
date: "2026-03-20",
|
category: "水泥",
|
orderNo: "PO-260320-02",
|
materialName: "矿粉",
|
materialType: "辅料",
|
quantity: 380,
|
unit: "kg",
|
unitPrice: 1.08,
|
},
|
]);
|
const orderList = ref([]);
|
|
// 加载生产订单列表
|
const loadOrders = () => {
|
productOrderListPage({ pageNum: -1, pageSize: -1 })
|
.then(res => {
|
orderList.value = res.data.records || [];
|
})
|
.finally(() => {});
|
};
|
const normalizedRecords = computed(() =>
|
sourceRecords.value.map(item => {
|
const month = item.date.slice(0, 7);
|
const cost = Number(item.quantity) * Number(item.unitPrice);
|
return { ...item, month, cost };
|
})
|
);
|
|
const categoryOptions = ref([]);
|
// 获取产品类型字典
|
const getProductTypeOptions = () => {
|
getDicts("product_type")
|
.then(res => {
|
if (res.code === 200) {
|
categoryOptions.value = res.data;
|
}
|
})
|
.catch(err => {
|
console.error("获取产品类型字典失败:", err);
|
});
|
};
|
|
const orderOptions = computed(() =>
|
Array.from(new Set(normalizedRecords.value.map(item => item.orderNo)))
|
);
|
|
const inRange = (value, range) => {
|
if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1])
|
return true;
|
return value >= range[0] && value <= range[1];
|
};
|
|
const getMonthRangeDays = monthRange => {
|
if (
|
!Array.isArray(monthRange) ||
|
monthRange.length !== 2 ||
|
!monthRange[0] ||
|
!monthRange[1]
|
) {
|
return 0;
|
}
|
const [startMonth, endMonth] = monthRange;
|
const startDate = new Date(`${startMonth}-01T00:00:00`);
|
const endDate = new Date(`${endMonth}-01T00:00:00`);
|
if (
|
Number.isNaN(startDate.getTime()) ||
|
Number.isNaN(endDate.getTime()) ||
|
startDate > endDate
|
) {
|
return 0;
|
}
|
const endMonthLastDay = new Date(
|
endDate.getFullYear(),
|
endDate.getMonth() + 1,
|
0
|
);
|
const diffMs = endMonthLastDay.getTime() - startDate.getTime();
|
return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1;
|
};
|
|
const buildQueryParams = () => {
|
const isDay = statisticsType.value === "day";
|
const params = {
|
statisticsType: statisticsType.value,
|
dictCode: searchForm.dictCode || undefined,
|
productOrderId: searchForm.productOrderId || undefined,
|
};
|
|
if (isDay) {
|
const [startDate, endDate] = searchForm.dateRange || [];
|
params.startDate = startDate;
|
params.endDate = endDate;
|
} else {
|
const [startMonth, endMonth] = searchForm.monthRange || [];
|
params.startMonth = startMonth;
|
params.endMonth = endMonth;
|
params.days = getMonthRangeDays(searchForm.monthRange);
|
}
|
|
return params;
|
};
|
|
const filteredRecords = computed(() =>
|
normalizedRecords.value.filter(item => {
|
const hitTime =
|
statisticsType.value === "day"
|
? inRange(item.date, searchForm.dateRange)
|
: inRange(item.month, searchForm.monthRange);
|
const hitCategory =
|
!searchForm.dictCode || item.dictCode === searchForm.dictCode;
|
const hitOrder =
|
!searchForm.productOrderId ||
|
item.productOrderId === searchForm.productOrderId;
|
return hitTime && hitCategory && hitOrder;
|
})
|
);
|
|
const timeColumnLabel = computed(() =>
|
statisticsType.value === "day" ? "日期" : "月份"
|
);
|
|
const aggregateBy = (list, keyFn) => {
|
const map = new Map();
|
for (const item of list) {
|
const key = keyFn(item);
|
if (!map.has(key)) {
|
map.set(key, {
|
totalCost: 0,
|
totalQuantity: 0,
|
materials: [],
|
});
|
}
|
const bucket = map.get(key);
|
bucket.totalCost += item.cost;
|
bucket.totalQuantity += Number(item.quantity) || 0;
|
bucket.materials.push(item);
|
}
|
return map;
|
};
|
|
const groupedMap = computed(() =>
|
aggregateBy(filteredRecords.value, item => {
|
const timeKey = statisticsType.value === "day" ? item.date : item.month;
|
return `${timeKey}__${item.category}__${item.orderNo}`;
|
})
|
);
|
|
const tableData = computed(() => {
|
const rows = [];
|
for (const [key, val] of groupedMap.value) {
|
const [timeLabel, category, orderNo] = key.split("__");
|
rows.push({
|
key,
|
timeLabel,
|
category,
|
orderNo,
|
totalQuantity: val.totalQuantity,
|
unit: val.materials[0]?.unit || "",
|
totalCost: val.totalCost,
|
materials: val.materials,
|
});
|
}
|
return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1));
|
});
|
|
const page = reactive({
|
current: 1,
|
size: 10,
|
});
|
|
const pagedTableData = computed(() => {
|
const start = (page.current - 1) * page.size;
|
return tableData.value.slice(start, start + page.size);
|
});
|
|
const categorySummary = ref([]);
|
const orderSummary = ref([]);
|
const topOrders = ref([]);
|
|
const overview = ref({
|
totalCost: 0,
|
orderCount: 0,
|
avgCostPerOrder: 0,
|
});
|
|
// 图表相关
|
const topOrdersChartRef = ref(null);
|
let topOrdersChartInstance = null;
|
|
const detailVisible = ref(false);
|
const detailRow = ref(null);
|
|
const detailMaterials = computed(() => detailRow.value?.materials || []);
|
|
const detailTotalCost = computed(() =>
|
detailMaterials.value.reduce((sum, item) => sum + item.cost, 0)
|
);
|
|
const openDetail = row => {
|
detailRow.value = row;
|
detailVisible.value = true;
|
};
|
|
// 初始化生产订单Top10图表
|
const initTopOrdersChart = () => {
|
nextTick(() => {
|
if (topOrdersChartRef.value) {
|
topOrdersChartInstance = echarts.init(topOrdersChartRef.value);
|
updateTopOrdersChart();
|
}
|
});
|
};
|
|
// 更新生产订单Top10图表
|
const updateTopOrdersChart = () => {
|
if (!topOrdersChartInstance) return;
|
|
const data = topOrders.value;
|
const xAxisData = data.map(item => item.name);
|
const seriesData = data.map(item => item.totalCost);
|
|
const option = {
|
tooltip: {
|
trigger: "axis",
|
axisPointer: {
|
type: "shadow",
|
},
|
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
borderColor: "#409EFF",
|
borderWidth: 1,
|
textStyle: { color: "#303133" },
|
},
|
grid: {
|
left: "3%",
|
right: "4%",
|
bottom: "15%",
|
top: "3%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: xAxisData,
|
axisLabel: {
|
color: "#606266",
|
rotate: 45,
|
},
|
axisLine: { lineStyle: { color: "#ebeef5" } },
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "成本(元)",
|
nameTextStyle: { color: "#606266" },
|
axisLabel: { color: "#606266" },
|
axisLine: { show: false },
|
splitLine: { lineStyle: { color: "#f0f2f5" } },
|
},
|
series: [
|
{
|
name: "成本",
|
type: "bar",
|
data: seriesData,
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#409EFF" },
|
{ offset: 1, color: "#66B1FF" },
|
]),
|
},
|
barWidth: "60%",
|
},
|
],
|
};
|
|
topOrdersChartInstance.setOption(option);
|
};
|
|
// 窗口大小变化时重新渲染图表
|
const handleResize = () => {
|
topOrdersChartInstance && topOrdersChartInstance.resize();
|
};
|
|
const handleTypeChange = () => {
|
handleQuery();
|
};
|
|
const handleQuery = () => {
|
page.current = 1;
|
// 构建API请求参数
|
const apiParams = {
|
startDate: searchForm.dateRange?.[0],
|
endDate: searchForm.dateRange?.[1],
|
dictCode: searchForm.dictCode,
|
productOrderId: searchForm.productOrderId,
|
current: -1,
|
size: -1,
|
};
|
|
// 调用API获取概览数据
|
getProductionCostSummary(apiParams)
|
.then(res => {
|
if (res.code === 200) {
|
const data = res.data;
|
overview.value = {
|
totalCost: parseFloat(data.totalCost) || 0,
|
orderCount: data.orderCount || 0,
|
avgCostPerOrder: parseFloat(data.averageOrderCost) || 0,
|
};
|
} else {
|
ElMessage.error(res.message || "获取概览数据失败");
|
}
|
})
|
.catch(err => {
|
console.error("获取生产成本汇总数据失败:", err);
|
ElMessage.error("系统异常,获取概览数据失败");
|
});
|
getProductionCostAggregateByOrder;
|
// 调用API获取按产品物料汇总数据
|
getProductionCostAggregateByProduct(apiParams)
|
.then(res => {
|
if (res.code === 200) {
|
// 按物料名称分组计算
|
|
// 这里简化处理,orderSummary暂时使用相同的数据
|
// 实际项目中可能需要调用专门的API获取按订单汇总的数据
|
categorySummary.value = res.data.records || [];
|
} else {
|
ElMessage.error(res.message || "获取物料汇总数据失败");
|
}
|
})
|
.catch(err => {
|
console.error("获取按产品物料汇总数据失败:", err);
|
ElMessage.error("系统异常,获取物料汇总数据失败");
|
});
|
getProductionCostAggregateByOrder(apiParams)
|
.then(res => {
|
if (res.code === 200) {
|
// 按物料名称分组计算
|
|
// 这里简化处理,orderSummary暂时使用相同的数据
|
// 实际项目中可能需要调用专门的API获取按订单汇总的数据
|
orderSummary.value = res.data.records || [];
|
} else {
|
ElMessage.error(res.message || "获取订单汇总数据失败");
|
}
|
})
|
.catch(err => {
|
console.error("获取按订单汇总数据失败:", err);
|
ElMessage.error("系统异常,获取订单汇总数据失败");
|
});
|
|
// 调用API获取生产订单Top10数据
|
getProductionCostTopOrders(apiParams)
|
.then(res => {
|
if (res.code === 200) {
|
topOrders.value = res.data || [];
|
updateTopOrdersChart();
|
} else {
|
ElMessage.error(res.message || "获取生产订单Top10数据失败");
|
}
|
})
|
.catch(err => {
|
console.error("获取生产订单Top10数据失败:", err);
|
ElMessage.error("系统异常,获取生产订单Top10数据失败");
|
});
|
};
|
|
const handleReset = () => {
|
searchForm.dateRange = getDefaultDateRange();
|
searchForm.monthRange = getDefaultMonthRange();
|
searchForm.dictCode = "";
|
searchForm.productOrderId = "";
|
handleQuery();
|
};
|
|
const handleSizeChange = val => {
|
page.size = val;
|
page.current = 1;
|
};
|
|
const handleCurrentChange = val => {
|
page.current = val;
|
};
|
onMounted(() => {
|
getProductTypeOptions();
|
loadOrders();
|
handleQuery();
|
initTopOrdersChart();
|
window.addEventListener("resize", handleResize);
|
});
|
|
const handleExport = () => {
|
const headers = [
|
timeColumnLabel.value,
|
"产品类别",
|
"生产订单",
|
"用量",
|
"单位",
|
"成本(元)",
|
];
|
const lines = tableData.value.map(row =>
|
[
|
row.timeLabel,
|
row.category,
|
row.orderNo,
|
row.totalQuantity.toFixed(2),
|
row.unit || "",
|
row.totalCost.toFixed(2),
|
].join(",")
|
);
|
const csv = [headers.join(","), ...lines].join("\n");
|
const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" });
|
const url = URL.createObjectURL(blob);
|
const link = document.createElement("a");
|
link.href = url;
|
link.download = `生产成本汇总_${statisticsType.value}_${Date.now()}.csv`;
|
link.click();
|
URL.revokeObjectURL(url);
|
ElMessage.success("导出成功");
|
};
|
|
const formatMoney = v => {
|
const n = Number.parseFloat(v);
|
const value = Number.isFinite(n) ? n : 0;
|
return value.toLocaleString("zh-CN", {
|
minimumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
});
|
};
|
|
const formatNumber = (v, digits = 2) => {
|
const n = Number.parseFloat(v);
|
if (!Number.isFinite(n)) return "--";
|
return n.toLocaleString("zh-CN", {
|
minimumFractionDigits: digits,
|
maximumFractionDigits: digits,
|
});
|
};
|
|
watch(tableData, () => {
|
const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
|
if (page.current > maxPage) page.current = maxPage;
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.production-cost-page {
|
--lux-bg: #f6f7fb;
|
--lux-card: rgba(255, 255, 255, 0.86);
|
--lux-border: rgba(15, 23, 42, 0.08);
|
--lux-text: rgba(15, 23, 42, 0.92);
|
--lux-subtle: rgba(15, 23, 42, 0.58);
|
--lux-muted: rgba(15, 23, 42, 0.38);
|
--lux-primary: #2f6fed;
|
--lux-success: #16a34a;
|
--lux-warning: #f59e0b;
|
--lux-danger: #ef4444;
|
--lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
--lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
|
--lux-radius: 14px;
|
|
padding: 18px 22px 24px;
|
background: radial-gradient(
|
1200px 420px at 20% 0%,
|
rgba(47, 111, 237, 0.1),
|
transparent 55%
|
),
|
linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
|
}
|
|
.filter-card,
|
.panel-card,
|
.table-card {
|
border-radius: var(--lux-radius);
|
border-color: var(--lux-border);
|
background: var(--lux-card);
|
box-shadow: var(--lux-shadow-soft);
|
}
|
|
.filter-card {
|
margin-bottom: 16px;
|
}
|
|
.panel-card,
|
.summary-row {
|
margin-bottom: 14px;
|
}
|
|
.filter-layout {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 14px;
|
}
|
|
.filter-form {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 10px 14px;
|
}
|
|
.filter-form :deep(.el-form-item) {
|
margin: 0;
|
}
|
|
.filter-actions {
|
display: flex;
|
gap: 10px;
|
}
|
|
.card-head,
|
.panel-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
|
.card-head-left {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.card-head-right {
|
display: flex;
|
align-items: center;
|
}
|
|
.card-icon {
|
color: var(--lux-primary);
|
}
|
|
.card-title {
|
font-weight: 760;
|
color: var(--lux-text);
|
}
|
|
.subtle {
|
color: var(--lux-subtle);
|
font-size: 12px;
|
}
|
|
.kpi-strip {
|
display: grid;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
gap: 12px;
|
}
|
|
.kpi-item {
|
padding: 12px 14px;
|
border-radius: 12px;
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
}
|
|
.kpi-total {
|
background: linear-gradient(
|
135deg,
|
rgba(47, 111, 237, 0.1),
|
rgba(255, 255, 255, 0.86)
|
);
|
}
|
|
.kpi-raw {
|
background: linear-gradient(
|
135deg,
|
rgba(22, 163, 74, 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 {
|
background: linear-gradient(
|
135deg,
|
rgba(100, 116, 139, 0.1),
|
rgba(255, 255, 255, 0.86)
|
);
|
}
|
|
.kpi-label {
|
font-size: 12px;
|
color: var(--lux-subtle);
|
}
|
|
.kpi-value {
|
font-size: 22px;
|
margin-top: 6px;
|
font-weight: 780;
|
color: var(--lux-text);
|
}
|
|
.price-value {
|
font-weight: 700;
|
color: var(--lux-success);
|
}
|
|
.cost-value {
|
font-weight: 700;
|
color: var(--lux-danger);
|
}
|
|
.no-wrap-money {
|
display: inline-block;
|
white-space: nowrap;
|
}
|
|
.drawer-head {
|
display: grid;
|
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: 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 {
|
display: flex;
|
justify-content: flex-end;
|
padding-top: 12px;
|
}
|
|
.strong {
|
font-weight: 800;
|
}
|
|
.w-260 {
|
width: 260px;
|
}
|
|
.w-180 {
|
width: 180px;
|
}
|
|
::deep(.lux-table) {
|
border-radius: 12px;
|
overflow: hidden;
|
}
|
|
::deep(.lux-table th.el-table__cell) {
|
background: rgba(15, 23, 42, 0.03);
|
}
|
|
::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
|
background-color: rgba(47, 111, 237, 0.06) !important;
|
}
|
|
@media (max-width: 1100px) {
|
.kpi-strip {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
|
.filter-layout {
|
flex-direction: column;
|
}
|
|
.drawer-head {
|
grid-template-columns: 1fr;
|
}
|
|
.drawer-foot {
|
justify-content: flex-start;
|
flex-wrap: wrap;
|
}
|
}
|
</style>
|