index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <template>
  2. <div class="app-container supply-dashboard-page">
  3. <div class="page-header">
  4. <h2><i class="el-icon-s-order"></i> 供应商成本分析</h2>
  5. <p class="page-desc">分析各供应商针对特定产品的报价及成本竞争力</p>
  6. </div>
  7. <!-- 搜索框 -->
  8. <el-card class="box-card">
  9. <div slot="header" class="clearfix">
  10. <span><i class="el-icon-search"></i> 查询产品成本分析</span>
  11. </div>
  12. <el-form :inline="true" class="form-inline">
  13. <el-form-item label="产品编码">
  14. <el-input
  15. v-model="inputProductCode"
  16. placeholder="请输入产品编码(如:20220606J0100MR4)"
  17. style="width: 300px"
  18. @keyup.enter.native="fetchCostData(inputProductCode)"
  19. />
  20. </el-form-item>
  21. <el-form-item>
  22. <el-button type="primary" @click="fetchCostData(inputProductCode)"
  23. >分析成本</el-button
  24. >
  25. </el-form-item>
  26. </el-form>
  27. </el-card>
  28. <!-- 表格数据 -->
  29. <el-card v-loading="loading" class="box-card">
  30. <div slot="header" class="clearfix">
  31. <span><i class="el-icon-document"></i> 成本详情表格</span>
  32. <el-button
  33. v-if="costDetails.length > 0"
  34. style="float: right; padding: 3px 0"
  35. type="text"
  36. @click="exportData"
  37. >
  38. 导出数据
  39. </el-button>
  40. </div>
  41. <el-table
  42. v-if="costDetails.length > 0"
  43. :data="costDetails"
  44. highlight-current-row
  45. style="width: 100%"
  46. >
  47. <el-table-column prop="供应商名称" label="供应商名称" min-width="220" />
  48. <el-table-column prop="参考价" label="参考价" width="110" align="right">
  49. <template slot-scope="scope">
  50. ¥{{ formatNumber(scope.row.参考价, 2) }}
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="入库数量" label="入库数量" width="120" align="right">
  54. <template slot-scope="scope">
  55. {{ formatNumber(scope.row.入库数量, 0) }}
  56. </template>
  57. </el-table-column>
  58. <el-table-column prop="入库金额" label="入库金额" width="130" align="right">
  59. <template slot-scope="scope">
  60. ¥{{ formatNumber(scope.row.入库金额, 2) }}
  61. </template>
  62. </el-table-column>
  63. <el-table-column prop="采购占比" label="采购占比" width="110" align="right">
  64. <template slot-scope="scope">
  65. {{ formatPercent(scope.row.采购占比) }}
  66. </template>
  67. </el-table-column>
  68. <el-table-column prop="价格差异率" label="价格差异率" width="120" align="right">
  69. <template slot-scope="scope">
  70. {{ formatPercent(scope.row.价格差异率) }}
  71. </template>
  72. </el-table-column>
  73. <el-table-column prop="成本排名" label="成本排名" width="100" align="center">
  74. <template slot-scope="scope"> 第{{ scope.row.成本排名 }}名 </template>
  75. </el-table-column>
  76. <el-table-column prop="成本分数" label="成本分数" width="110" align="right">
  77. <template slot-scope="scope">
  78. {{ formatNumber(scope.row.成本分数, 2) }}
  79. </template>
  80. </el-table-column>
  81. <el-table-column prop="成本等级" label="成本等级" width="100" align="center">
  82. <template slot-scope="scope">
  83. <el-tag :type="costLevelType(scope.row.成本等级)" size="mini">
  84. {{ scope.row.成本等级 }}
  85. </el-tag>
  86. </template>
  87. </el-table-column>
  88. <el-table-column prop="入库行数" label="入库行数" width="100" align="right" />
  89. <el-table-column prop="最近验收日期" label="最近验收日期" width="130" />
  90. </el-table>
  91. <div v-else class="no-data">
  92. <p v-if="error" class="error">{{ error }}</p>
  93. <p v-else>暂无成本数据,请输入产品编码进行分析</p>
  94. </div>
  95. </el-card>
  96. </div>
  97. </template>
  98. <script>
  99. import { saveAs } from "file-saver";
  100. import { getProductDetails } from "@/api/supply";
  101. export default {
  102. name: "SupplyCostAnalysis",
  103. data() {
  104. return {
  105. inputProductCode: "",
  106. costDetails: [],
  107. loading: false,
  108. error: "",
  109. };
  110. },
  111. created() {
  112. // 如果有传入的 productCode,则自动触发分析
  113. if (this.$route.query.productCode) {
  114. this.inputProductCode = this.$route.query.productCode;
  115. this.fetchCostData(this.inputProductCode);
  116. }
  117. },
  118. methods: {
  119. // 获取成本数据
  120. async fetchCostData(code) {
  121. if (!code) {
  122. this.$modal.msgError("请输入产品编码");
  123. return;
  124. }
  125. this.loading = true;
  126. this.error = "";
  127. try {
  128. // 使用项目标准的request方式调用API
  129. const response = await getProductDetails(code);
  130. // Flask API通常直接返回数据,没有code字段
  131. if (response && response.cost_details !== undefined) {
  132. this.costDetails = this.enrichCostDetails(response.cost_details || []);
  133. this.$modal.msgSuccess("成本分析完成");
  134. } else {
  135. // 尝试其他可能的数据结构
  136. if (response && Array.isArray(response)) {
  137. this.costDetails = this.enrichCostDetails(response || []);
  138. this.$modal.msgSuccess("成本分析完成");
  139. } else {
  140. this.error = "获取成本数据失败,API响应格式不匹配";
  141. this.$modal.msgError(this.error);
  142. }
  143. }
  144. } catch (err) {
  145. console.error("获取成本数据失败:", err);
  146. this.error = err.message || "获取成本数据失败";
  147. this.$modal.msgError(this.error);
  148. } finally {
  149. this.loading = false;
  150. }
  151. },
  152. enrichCostDetails(rows) {
  153. const list = Array.isArray(rows) ? rows : [];
  154. const totalAmount = list.reduce(
  155. (sum, row) => sum + this.toNumber(row.入库金额),
  156. 0
  157. );
  158. const minPrice = list.reduce((min, row) => {
  159. const price = this.toNumber(row.参考价);
  160. return price > 0 ? Math.min(min, price) : min;
  161. }, Number.POSITIVE_INFINITY);
  162. return list.map((row) => {
  163. const amount = this.toNumber(row.入库金额);
  164. const price = this.toNumber(row.参考价);
  165. const diffRate = Number.isFinite(minPrice) && minPrice > 0
  166. ? (price - minPrice) / minPrice
  167. : 0;
  168. return {
  169. ...row,
  170. 采购占比: totalAmount > 0 ? amount / totalAmount : 0,
  171. 价格差异率: diffRate,
  172. 成本等级: this.costLevel(row.成本排名),
  173. };
  174. });
  175. },
  176. toNumber(value) {
  177. const number = Number(value);
  178. return Number.isFinite(number) ? number : 0;
  179. },
  180. formatNumber(value, digits = 2) {
  181. return this.toNumber(value).toLocaleString("zh-CN", {
  182. minimumFractionDigits: digits,
  183. maximumFractionDigits: digits,
  184. });
  185. },
  186. formatPercent(value) {
  187. return `${(this.toNumber(value) * 100).toFixed(1)}%`;
  188. },
  189. costLevel(rank) {
  190. const value = Number(rank);
  191. if (value === 1) return "低成本";
  192. if (value <= 3) return "中成本";
  193. return "高成本";
  194. },
  195. costLevelType(level) {
  196. if (level === "低成本") return "success";
  197. if (level === "中成本") return "warning";
  198. return "danger";
  199. },
  200. // 导出数据
  201. exportData() {
  202. if (!this.costDetails || this.costDetails.length === 0) {
  203. this.$modal.msgError("暂无数据可导出");
  204. return;
  205. }
  206. // 创建CSV内容
  207. const header = [
  208. "供应商名称",
  209. "参考价",
  210. "入库数量",
  211. "入库金额",
  212. "采购占比",
  213. "价格差异率",
  214. "成本排名",
  215. "成本分数",
  216. "成本等级",
  217. "入库行数",
  218. "最近验收日期",
  219. ].join(",");
  220. const csvContent = [
  221. header,
  222. ...this.costDetails.map((row) =>
  223. [
  224. `"${row.供应商名称}"`,
  225. row.参考价,
  226. row.入库数量,
  227. row.入库金额,
  228. this.formatPercent(row.采购占比),
  229. this.formatPercent(row.价格差异率),
  230. `第${row.成本排名}名`,
  231. row.成本分数,
  232. row.成本等级,
  233. row.入库行数,
  234. row.最近验收日期,
  235. ].join(",")
  236. ),
  237. ].join("\n");
  238. // 下载文件
  239. const blob = new Blob(["\ufeff" + csvContent], {
  240. type: "text/csv;charset=utf-8;",
  241. });
  242. saveAs(blob, `供应商成本分析_${new Date().getTime()}.csv`);
  243. },
  244. },
  245. };
  246. </script>
  247. <style lang="scss" scoped>
  248. .app-container {
  249. padding: 20px;
  250. }
  251. .page-header {
  252. margin-bottom: 20px;
  253. h2 {
  254. font-size: 24px;
  255. font-weight: 600;
  256. color: #303133;
  257. margin-bottom: 8px;
  258. i {
  259. margin-right: 8px;
  260. color: #409eff;
  261. }
  262. }
  263. .page-desc {
  264. color: #909399;
  265. font-size: 14px;
  266. margin: 0;
  267. }
  268. }
  269. .box-card {
  270. margin-bottom: 20px;
  271. }
  272. .form-inline {
  273. display: flex;
  274. align-items: center;
  275. }
  276. .no-data {
  277. text-align: center;
  278. padding: 40px 0;
  279. color: #909399;
  280. .error {
  281. color: #f56c6c;
  282. }
  283. }
  284. ::v-deep .el-card__header {
  285. font-weight: bold;
  286. }
  287. </style>