index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <template>
  2. <div class="app-container">
  3. <div class="page-header">
  4. <h2><i class="el-icon-s-data"></i> 供应商查询、对比与付款计划</h2>
  5. <p class="page-desc">基于供应商账期、采购订单、采购入库和订单入库合并数据,补充供应商实时查询、对比分析和预计付款计划。</p>
  6. </div>
  7. <el-card class="box-card">
  8. <div slot="header" class="clearfix">
  9. <span><i class="el-icon-search"></i> 查询条件</span>
  10. </div>
  11. <el-form :inline="true" size="small" class="form-inline">
  12. <el-form-item label="供应商名称">
  13. <el-input v-model="query.supplierName" clearable placeholder="输入供应商名称" style="width: 260px" @keyup.enter.native="handleQuery" />
  14. </el-form-item>
  15. <el-form-item label="日期范围">
  16. <el-date-picker
  17. v-model="dateRange"
  18. type="daterange"
  19. value-format="yyyy-MM-dd"
  20. range-separator="至"
  21. start-placeholder="开始日期"
  22. end-placeholder="结束日期"
  23. />
  24. </el-form-item>
  25. <el-form-item>
  26. <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
  27. <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
  28. </el-form-item>
  29. </el-form>
  30. </el-card>
  31. <el-alert
  32. class="box-card"
  33. title="付款计划当前按 实际验收日期 + 供应商账期 推算;如果后续接入发票日期和真实付款日期,可以替换为真实付款计划。"
  34. type="warning"
  35. show-icon
  36. :closable="false"
  37. />
  38. <el-tabs v-model="activeTab" type="border-card" class="box-card" @tab-click="handleTabClick">
  39. <el-tab-pane label="供应商列表" name="list">
  40. <el-table
  41. v-loading="loading"
  42. :data="pagedSupplierList"
  43. row-key="supplierName"
  44. border
  45. highlight-current-row
  46. style="width: 100%"
  47. @selection-change="handleSelectionChange"
  48. >
  49. <el-table-column type="selection" width="48" reserve-selection />
  50. <el-table-column prop="supplierName" label="供应商名称" min-width="220" fixed="left" show-overflow-tooltip />
  51. <el-table-column prop="supplierCode" label="供应商代码" width="110" />
  52. <el-table-column label="账期" width="90" align="right">
  53. <template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template>
  54. </el-table-column>
  55. <el-table-column label="订单量" prop="orderCount" width="90" align="right" />
  56. <el-table-column label="订单数量" width="110" align="right">
  57. <template slot-scope="scope">{{ formatNumber(scope.row.orderQty) }}</template>
  58. </el-table-column>
  59. <el-table-column label="入库数量" width="110" align="right">
  60. <template slot-scope="scope">{{ formatNumber(scope.row.receiptQty) }}</template>
  61. </el-table-column>
  62. <el-table-column label="入库金额" width="130" align="right">
  63. <template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template>
  64. </el-table-column>
  65. <el-table-column label="交付及时率" width="120" align="right">
  66. <template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template>
  67. </el-table-column>
  68. <el-table-column label="完成率" width="110" align="right">
  69. <template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template>
  70. </el-table-column>
  71. </el-table>
  72. <div class="pagination-wrap">
  73. <el-pagination
  74. :current-page.sync="pagination.list.page"
  75. :page-size="pagination.list.size"
  76. :total="supplierList.length"
  77. layout="total, prev, pager, next, jumper"
  78. />
  79. </div>
  80. </el-tab-pane>
  81. <el-tab-pane label="供应商对比" name="compare">
  82. <div class="toolbar">
  83. <span>已选择 {{ selectedSuppliers.length }} 个供应商,支持勾选 2-5 个供应商进行对比。</span>
  84. <el-button type="primary" size="mini" :disabled="selectedSuppliers.length < 2" @click="loadCompare">生成对比</el-button>
  85. </div>
  86. <div class="chart-card">
  87. <div class="chart-title">供应商核心指标柱状对比</div>
  88. <div v-if="compareList.length === 0" class="chart-empty">请选择 2-5 个供应商并生成对比</div>
  89. <div ref="compareChart" class="compare-chart" />
  90. </div>
  91. <el-table v-loading="compareLoading" :data="pagedCompareList" border highlight-current-row style="width: 100%">
  92. <el-table-column prop="supplierName" label="供应商名称" min-width="220" fixed="left" show-overflow-tooltip />
  93. <el-table-column label="账期" width="90" align="right">
  94. <template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template>
  95. </el-table-column>
  96. <el-table-column label="订单量" prop="orderCount" width="90" align="right" />
  97. <el-table-column label="订单金额" width="130" align="right">
  98. <template slot-scope="scope">¥{{ formatMoney(scope.row.orderAmount) }}</template>
  99. </el-table-column>
  100. <el-table-column label="入库金额" width="130" align="right">
  101. <template slot-scope="scope">¥{{ formatMoney(scope.row.receiptAmount) }}</template>
  102. </el-table-column>
  103. <el-table-column label="交付及时率" width="120" align="right">
  104. <template slot-scope="scope">{{ formatPercent(scope.row.deliveryRate) }}</template>
  105. </el-table-column>
  106. <el-table-column label="完成率" width="110" align="right">
  107. <template slot-scope="scope">{{ formatPercent(scope.row.completionRate) }}</template>
  108. </el-table-column>
  109. <el-table-column label="未完工数" width="120" align="right">
  110. <template slot-scope="scope">{{ formatNumber(scope.row.uncompletedQty) }}</template>
  111. </el-table-column>
  112. </el-table>
  113. <div class="pagination-wrap">
  114. <el-pagination
  115. :current-page.sync="pagination.compare.page"
  116. :page-size="pagination.compare.size"
  117. :total="compareList.length"
  118. layout="total, prev, pager, next, jumper"
  119. />
  120. </div>
  121. </el-tab-pane>
  122. <el-tab-pane label="付款计划" name="payment">
  123. <el-table v-loading="paymentLoading" :data="pagedPaymentPlan" border highlight-current-row style="width: 100%">
  124. <el-table-column prop="dueDate" label="预计付款日" width="120" fixed="left" />
  125. <el-table-column prop="supplierName" label="供应商名称" min-width="220" show-overflow-tooltip />
  126. <el-table-column label="账期" width="90" align="right">
  127. <template slot-scope="scope">{{ scope.row.termDays || 0 }}天</template>
  128. </el-table-column>
  129. <el-table-column label="预计付款金额" width="150" align="right">
  130. <template slot-scope="scope">¥{{ formatMoney(scope.row.estimatedPayAmount) }}</template>
  131. </el-table-column>
  132. <el-table-column label="入库数量" width="120" align="right">
  133. <template slot-scope="scope">{{ formatNumber(scope.row.receiptQty) }}</template>
  134. </el-table-column>
  135. <el-table-column prop="receiptLines" label="入库行数" width="100" align="right" />
  136. <el-table-column prop="status" label="状态" width="110" align="center">
  137. <template slot-scope="scope">
  138. <el-tag :type="paymentTag(scope.row.status)" size="mini">{{ scope.row.status }}</el-tag>
  139. </template>
  140. </el-table-column>
  141. </el-table>
  142. <div class="pagination-wrap">
  143. <el-pagination
  144. :current-page.sync="pagination.payment.page"
  145. :page-size="pagination.payment.size"
  146. :total="paymentPlan.length"
  147. layout="total, prev, pager, next, jumper"
  148. />
  149. </div>
  150. </el-tab-pane>
  151. </el-tabs>
  152. </div>
  153. </template>
  154. <script>
  155. import * as echarts from 'echarts'
  156. import { getSupplyMonitorCompare, getSupplyMonitorSuppliers, getSupplyPaymentPlan } from '@/api/supply'
  157. export default {
  158. name: 'SupplyMonitorEnhancement',
  159. data() {
  160. return {
  161. activeTab: 'list',
  162. loading: false,
  163. compareLoading: false,
  164. paymentLoading: false,
  165. compareChart: null,
  166. query: {
  167. supplierName: ''
  168. },
  169. dateRange: [],
  170. supplierList: [],
  171. selectedSuppliers: [],
  172. compareList: [],
  173. paymentPlan: [],
  174. pagination: {
  175. list: { page: 1, size: 20 },
  176. compare: { page: 1, size: 20 },
  177. payment: { page: 1, size: 20 }
  178. }
  179. }
  180. },
  181. created() {
  182. this.handleQuery()
  183. },
  184. mounted() {
  185. window.addEventListener('resize', this.handleResize)
  186. },
  187. beforeDestroy() {
  188. window.removeEventListener('resize', this.handleResize)
  189. if (this.compareChart) {
  190. this.compareChart.dispose()
  191. this.compareChart = null
  192. }
  193. },
  194. computed: {
  195. pagedSupplierList() {
  196. return this.slicePage(this.supplierList, this.pagination.list)
  197. },
  198. pagedCompareList() {
  199. return this.slicePage(this.compareList, this.pagination.compare)
  200. },
  201. pagedPaymentPlan() {
  202. return this.slicePage(this.paymentPlan, this.pagination.payment)
  203. }
  204. },
  205. methods: {
  206. slicePage(list, pager) {
  207. const start = (pager.page - 1) * pager.size
  208. return list.slice(start, start + pager.size)
  209. },
  210. buildParams() {
  211. const params = {
  212. supplierName: this.query.supplierName
  213. }
  214. if (this.dateRange && this.dateRange.length === 2) {
  215. params.startDate = this.dateRange[0]
  216. params.endDate = this.dateRange[1]
  217. }
  218. return params
  219. },
  220. handleQuery() {
  221. this.loadSuppliers()
  222. if (this.activeTab === 'payment') {
  223. this.loadPaymentPlan()
  224. } else {
  225. this.paymentPlan = []
  226. }
  227. this.compareList = []
  228. this.$nextTick(this.renderCompareChart)
  229. },
  230. resetQuery() {
  231. this.query.supplierName = ''
  232. this.dateRange = []
  233. this.handleQuery()
  234. },
  235. async loadSuppliers() {
  236. this.loading = true
  237. try {
  238. const res = await getSupplyMonitorSuppliers(this.buildParams())
  239. this.supplierList = Array.isArray(res.data) ? res.data : []
  240. this.pagination.list.page = 1
  241. } finally {
  242. this.loading = false
  243. }
  244. },
  245. async loadPaymentPlan() {
  246. this.paymentLoading = true
  247. try {
  248. const res = await getSupplyPaymentPlan(this.buildParams())
  249. this.paymentPlan = Array.isArray(res.data) ? res.data : []
  250. this.pagination.payment.page = 1
  251. } finally {
  252. this.paymentLoading = false
  253. }
  254. },
  255. handleTabClick(tab) {
  256. if (tab.name === 'payment' && this.paymentPlan.length === 0) {
  257. this.loadPaymentPlan()
  258. }
  259. if (tab.name === 'compare') {
  260. this.$nextTick(this.renderCompareChart)
  261. }
  262. },
  263. async loadCompare() {
  264. const supplierNames = this.selectedSuppliers.slice(0, 5).map(item => item.supplierName).join(',')
  265. this.compareLoading = true
  266. try {
  267. const res = await getSupplyMonitorCompare({
  268. ...this.buildParams(),
  269. supplierNames
  270. })
  271. this.compareList = Array.isArray(res.data) ? res.data : []
  272. this.pagination.compare.page = 1
  273. this.activeTab = 'compare'
  274. this.$nextTick(this.renderCompareChart)
  275. } finally {
  276. this.compareLoading = false
  277. }
  278. },
  279. handleSelectionChange(selection) {
  280. this.selectedSuppliers = selection.slice(0, 5)
  281. },
  282. renderCompareChart() {
  283. const chartEl = this.$refs.compareChart
  284. if (!chartEl) return
  285. this.compareChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
  286. const names = this.compareList.map(item => item.supplierName)
  287. this.compareChart.setOption({
  288. color: ['#409eff', '#67c23a', '#e6a23c', '#f56c6c'],
  289. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  290. legend: { top: 0, data: ['订单金额', '入库金额', '交付及时率', '完成率'] },
  291. grid: { left: 56, right: 24, top: 48, bottom: 64 },
  292. xAxis: {
  293. type: 'category',
  294. data: names,
  295. axisLabel: { interval: 0, rotate: names.length > 3 ? 20 : 0 }
  296. },
  297. yAxis: [
  298. { type: 'value', name: '金额', axisLabel: { formatter: value => this.compactMoney(value) } },
  299. { type: 'value', name: '比例', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }
  300. ],
  301. series: [
  302. { name: '订单金额', type: 'bar', data: this.compareList.map(item => Number(item.orderAmount || 0)) },
  303. { name: '入库金额', type: 'bar', data: this.compareList.map(item => Number(item.receiptAmount || 0)) },
  304. { name: '交付及时率', type: 'bar', yAxisIndex: 1, data: this.compareList.map(item => Number(item.deliveryRate || 0) * 100) },
  305. { name: '完成率', type: 'bar', yAxisIndex: 1, data: this.compareList.map(item => Number(item.completionRate || 0) * 100) }
  306. ]
  307. }, true)
  308. this.compareChart.resize()
  309. },
  310. handleResize() {
  311. if (this.compareChart) this.compareChart.resize()
  312. },
  313. paymentTag(status) {
  314. if (status === '已到期') return 'danger'
  315. if (status === '7天内到期') return 'warning'
  316. return 'success'
  317. },
  318. compactMoney(value) {
  319. const number = Number(value || 0)
  320. if (Math.abs(number) >= 10000) {
  321. return `${this.formatNumber(number / 10000)}万`
  322. }
  323. return this.formatNumber(number)
  324. },
  325. formatNumber(value) {
  326. return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 2 }).format(Number(value || 0))
  327. },
  328. formatMoney(value) {
  329. return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(value || 0))
  330. },
  331. formatPercent(value) {
  332. return `${this.formatNumber(Number(value || 0) * 100)}%`
  333. }
  334. }
  335. }
  336. </script>
  337. <style lang="scss" scoped>
  338. .app-container {
  339. padding: 20px;
  340. }
  341. .page-header {
  342. margin-bottom: 20px;
  343. h2 {
  344. font-size: 24px;
  345. font-weight: 600;
  346. color: #303133;
  347. margin-bottom: 8px;
  348. i {
  349. margin-right: 8px;
  350. color: #409eff;
  351. }
  352. }
  353. .page-desc {
  354. color: #909399;
  355. font-size: 14px;
  356. margin: 0;
  357. }
  358. }
  359. .box-card {
  360. margin-bottom: 20px;
  361. }
  362. .form-inline {
  363. display: flex;
  364. align-items: center;
  365. flex-wrap: wrap;
  366. }
  367. .toolbar {
  368. display: flex;
  369. align-items: center;
  370. justify-content: space-between;
  371. margin-bottom: 12px;
  372. color: #606266;
  373. }
  374. .chart-card {
  375. position: relative;
  376. margin-bottom: 16px;
  377. padding: 16px;
  378. border: 1px solid #ebeef5;
  379. border-radius: 4px;
  380. background: #fff;
  381. }
  382. .chart-title {
  383. margin-bottom: 8px;
  384. color: #303133;
  385. font-weight: 600;
  386. }
  387. .compare-chart {
  388. width: 100%;
  389. height: 320px;
  390. }
  391. .chart-empty {
  392. position: absolute;
  393. left: 0;
  394. right: 0;
  395. top: 154px;
  396. z-index: 2;
  397. text-align: center;
  398. color: #909399;
  399. }
  400. .pagination-wrap {
  401. display: flex;
  402. justify-content: flex-end;
  403. padding-top: 16px;
  404. }
  405. ::v-deep .el-card__header {
  406. font-weight: bold;
  407. }
  408. </style>