You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

571 lines
18 KiB

1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
1 month ago
  1. <template>
  2. <div class="customer-css part-price-analysis">
  3. <el-form :inline="true" label-position="top" class="pi-search-form">
  4. <el-form-item label="开始时间" required>
  5. <el-date-picker
  6. v-model="searchData.startDate"
  7. type="date"
  8. format="yyyy-MM-dd"
  9. value-format="yyyy-MM-dd"
  10. placeholder="开始时间"
  11. style="width: 150px"
  12. clearable
  13. />
  14. </el-form-item>
  15. <el-form-item label="结束时间">
  16. <el-date-picker
  17. v-model="searchData.endDate"
  18. type="date"
  19. format="yyyy-MM-dd"
  20. value-format="yyyy-MM-dd"
  21. placeholder="结束时间"
  22. style="width: 150px"
  23. clearable
  24. />
  25. </el-form-item>
  26. <el-form-item required>
  27. <span style="cursor: pointer" slot="label" @click="getBaseList(2017)"><a href="javascript:void(0)">产品编码</a></span>
  28. <el-input
  29. v-model="searchData.partNo"
  30. placeholder="产品编码"
  31. style="width: 160px"
  32. clearable
  33. @keyup.enter.native="handleSearch"
  34. />
  35. </el-form-item>
  36. <el-form-item>
  37. <span style="cursor: pointer" slot="label" @click="getBaseList(520)"><a href="javascript:void(0)">供应商编码</a></span>
  38. <el-input
  39. v-model="searchData.supplierId"
  40. placeholder="不填则按产品查询"
  41. style="width: 160px"
  42. clearable
  43. @keyup.enter.native="handleSearch"
  44. />
  45. </el-form-item>
  46. <el-form-item label=" ">
  47. <el-button type="primary" class="customer-bun-min" :loading="loading" @click="handleSearch">查询</el-button>
  48. <el-button class="customer-bun-min" @click="resetSearch">重置</el-button>
  49. </el-form-item>
  50. </el-form>
  51. <el-form v-if="showProductInfo" :inline="true" label-position="top" class="pi-product-form">
  52. <el-form-item label="产品编码">
  53. <el-input :value="displayText(productInfo.partNo || searchData.partNo)" disabled style="width: 160px" />
  54. </el-form-item>
  55. <el-form-item label="产品名称">
  56. <el-input :value="displayText(productInfo.partDesc)" disabled style="width: 220px" />
  57. </el-form-item>
  58. <el-form-item label="Item">
  59. <el-input :value="displayText(productInfo.item)" disabled style="width: 120px" />
  60. </el-form-item>
  61. <el-form-item label="SKU">
  62. <el-input :value="displayText(productInfo.sku)" disabled style="width: 120px" />
  63. </el-form-item>
  64. <el-form-item label="物料分类">
  65. <el-input :value="displayText(productInfo.categoryName)" disabled style="width: 160px" />
  66. </el-form-item>
  67. <el-form-item label="物料二级分类">
  68. <el-input :value="displayText(productInfo.categoryLevel2Name)" disabled style="width: 160px" />
  69. </el-form-item>
  70. </el-form>
  71. <el-table
  72. :data="dataList"
  73. :height="tableHeight"
  74. border
  75. stripe
  76. v-loading="loading"
  77. style="width: 100%"
  78. empty-text="请填写查询条件后点击查询"
  79. >
  80. <el-table-column fixed="left" label="操作" width="90" header-align="center" align="center">
  81. <template slot-scope="scope">
  82. <el-link style="cursor: pointer" @click="openPriceSheet(scope.row)">价格表</el-link>
  83. </template>
  84. </el-table-column>
  85. <el-table-column prop="price" label="价格" min-width="80" header-align="center" align="right">
  86. <template slot-scope="scope">
  87. {{ formatMoney(scope.row.price) }}
  88. </template>
  89. </el-table-column>
  90. <el-table-column prop="priceChangeRate" label="涨跌" min-width="100" header-align="center" align="center">
  91. <template slot-scope="scope">
  92. <span :class="getPriceChangeClass(scope.row.priceChangeRate)">
  93. {{ scope.row.priceChangeRate || '-' }}
  94. </span>
  95. </template>
  96. </el-table-column>
  97. <el-table-column prop="priceDate" label="价格日期" min-width="160" header-align="center" align="center">
  98. <template slot-scope="scope">
  99. {{ formatDateOnly(scope.row.priceDate) }}
  100. </template>
  101. </el-table-column>
  102. <el-table-column prop="supplierId" label="供应商编码" min-width="120" header-align="center" align="left" show-overflow-tooltip />
  103. <el-table-column prop="supplierName" label="供应商名称" min-width="200" header-align="center" align="left" show-overflow-tooltip />
  104. </el-table>
  105. <el-card shadow="never" class="chart-card">
  106. <div slot="header" class="chart-card__header">
  107. <span>价格趋势供应商历史价格</span>
  108. </div>
  109. <div id="partPriceChart" class="chart-box" :style="{ height: chartHeight + 'px' }"></div>
  110. </el-card>
  111. <el-dialog
  112. title="价格表"
  113. :visible.sync="priceSheetDialogVisible"
  114. width="600px"
  115. :close-on-click-modal="false"
  116. v-drag
  117. append-to-body
  118. >
  119. <el-form :inline="true" label-position="top" class="pi-price-sheet-form">
  120. <el-form-item label="产品">
  121. <el-input :value="displayText(priceSheetHeader.productText)" disabled style="width: 190px" />
  122. </el-form-item>
  123. <el-form-item label="价格日期">
  124. <el-input :value="displayText(formatDateOnly(priceSheetHeader.priceDate))" disabled style="width: 80px" />
  125. </el-form-item>
  126. <!-- <el-form-item label="供应商编码">-->
  127. <!-- <el-input :value="displayText(priceSheetHeader.supplierId)" disabled style="width: 100px" />-->
  128. <!-- </el-form-item>-->
  129. <el-form-item label="供应商名称">
  130. <el-input :value="displayText(priceSheetHeader.supplierName)" disabled style="width: 180px" />
  131. </el-form-item>
  132. <el-form-item label="币种">
  133. <el-input :value="displayText(priceSheetHeader.currency)" disabled style="width: 70px" />
  134. </el-form-item>
  135. </el-form>
  136. <el-table
  137. :data="priceSheetDetailList"
  138. :height="300"
  139. border
  140. stripe
  141. v-loading="priceSheetLoading"
  142. style="width: 100%"
  143. >
  144. <el-table-column prop="qtyStart" label="开始数量" min-width="80" header-align="center" align="right">
  145. <template slot-scope="scope">
  146. {{ formatNumber(scope.row.qtyStart) }}
  147. </template>
  148. </el-table-column>
  149. <el-table-column prop="qtyEnd" label="结束数量" min-width="80" header-align="center" align="right">
  150. <template slot-scope="scope">
  151. {{ formatNumber(scope.row.qtyEnd) }}
  152. </template>
  153. </el-table-column>
  154. <el-table-column prop="unitPrice" label="单价" min-width="80" header-align="center" align="right">
  155. <template slot-scope="scope">
  156. {{ formatNumber(scope.row.unitPrice) }}
  157. </template>
  158. </el-table-column>
  159. <el-table-column prop="remark" label="备注" min-width="150" header-align="center" align="left" show-overflow-tooltip />
  160. </el-table>
  161. <div slot="footer" class="dialog-footer">
  162. <el-button type="primary" @click="priceSheetDialogVisible = false">关闭</el-button>
  163. </div>
  164. </el-dialog>
  165. <Chooselist
  166. ref="baseList"
  167. @getBaseData="getBaseData">
  168. </Chooselist>
  169. </div>
  170. </template>
  171. <script>
  172. import echarts from 'echarts'
  173. import { searchPartPriceAnalysis } from '@/api/supplier/partPriceAnalysis'
  174. import { listTierPriceHistoryDetail } from '@/api/supplier/supplierPartMoqPrice'
  175. import Chooselist from '@/views/modules/common/Chooselist_eam'
  176. function getDefaultStartDate () {
  177. const date = new Date()
  178. date.setMonth(date.getMonth() - 3)
  179. const pad = num => String(num).padStart(2, '0')
  180. return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
  181. }
  182. function createEmptyProductInfo () {
  183. return {
  184. partNo: '',
  185. partDesc: '',
  186. item: '',
  187. sku: '',
  188. categoryName: '',
  189. categoryLevel2Name: ''
  190. }
  191. }
  192. export default {
  193. name: 'PartPriceAnalysis',
  194. components: {
  195. Chooselist
  196. },
  197. data () {
  198. return {
  199. loading: false,
  200. tagNo: '',
  201. dataList: [],
  202. searchData: {
  203. site: this.$store.state.user.site,
  204. startDate: getDefaultStartDate(),
  205. endDate: '',
  206. partNo: '',
  207. supplierId: ''
  208. },
  209. showProductInfo: false,
  210. productInfo: createEmptyProductInfo(),
  211. priceSheetDialogVisible: false,
  212. priceSheetLoading: false,
  213. priceSheetHeader: {
  214. productText: '',
  215. priceDate: '',
  216. supplierId: '',
  217. supplierName: '',
  218. currency: '',
  219. historyMainId: null
  220. },
  221. priceSheetDetailList: [],
  222. tableHeight: 280,
  223. chartHeight: 320,
  224. chartInstance: null
  225. }
  226. },
  227. mounted () {
  228. this.$nextTick(() => {
  229. this.updateLayout()
  230. this.initChart()
  231. })
  232. window.addEventListener('resize', this.handleResize)
  233. },
  234. activated () {
  235. if (this.chartInstance) {
  236. this.chartInstance.resize()
  237. }
  238. },
  239. beforeDestroy () {
  240. window.removeEventListener('resize', this.handleResize)
  241. if (this.chartInstance) {
  242. this.chartInstance.dispose()
  243. this.chartInstance = null
  244. }
  245. },
  246. methods: {
  247. updateLayout () {
  248. const availableHeight = window.innerHeight - 220
  249. this.tableHeight = Math.max(220, Math.floor(availableHeight * 0.45))
  250. this.chartHeight = Math.max(260, Math.floor(availableHeight * 0.5))
  251. },
  252. handleResize () {
  253. this.updateLayout()
  254. if (this.chartInstance) {
  255. this.chartInstance.resize()
  256. }
  257. },
  258. initChart () {
  259. const dom = document.getElementById('partPriceChart')
  260. if (!dom) {
  261. return
  262. }
  263. if (this.chartInstance) {
  264. this.chartInstance.dispose()
  265. }
  266. this.chartInstance = echarts.init(dom)
  267. this.renderChart([])
  268. },
  269. resetSearch () {
  270. this.searchData = {
  271. site: this.$store.state.user.site,
  272. startDate: getDefaultStartDate(),
  273. endDate: '',
  274. partNo: '',
  275. supplierId: ''
  276. }
  277. this.showProductInfo = false
  278. this.productInfo = createEmptyProductInfo()
  279. this.dataList = []
  280. this.renderChart([])
  281. },
  282. getBaseList (val) {
  283. this.tagNo = val
  284. this.$nextTick(() => {
  285. let strVal = ''
  286. let conSql = ''
  287. if (val === 520) {
  288. strVal = this.searchData.supplierId || ''
  289. conSql = " and supplier_no <> '' "
  290. } else if (val === 2017) {
  291. strVal = this.searchData.partNo || ''
  292. conSql = " and part_no <> '' "
  293. }
  294. this.$refs.baseList.init(val, strVal, conSql)
  295. })
  296. },
  297. getBaseData (val) {
  298. if (this.tagNo === 520) {
  299. this.searchData.supplierId = val.supplier_no || val.code || ''
  300. } else if (this.tagNo === 2017) {
  301. this.searchData.partNo = val.part_no || val.code || ''
  302. }
  303. },
  304. validateSearch () {
  305. if (!this.searchData.startDate) {
  306. this.$message.warning('请选择开始时间')
  307. return false
  308. }
  309. if (!this.searchData.partNo || !this.searchData.partNo.trim()) {
  310. this.$message.warning('请输入产品编码')
  311. return false
  312. }
  313. if (this.searchData.endDate && this.searchData.startDate > this.searchData.endDate) {
  314. this.$message.warning('开始时间不能大于结束时间')
  315. return false
  316. }
  317. return true
  318. },
  319. handleSearch () {
  320. if (!this.validateSearch()) {
  321. return
  322. }
  323. this.loading = true
  324. const params = {
  325. site: this.$store.state.user.site,
  326. startDate: this.searchData.startDate,
  327. endDate: this.searchData.endDate || '',
  328. partNo: this.searchData.partNo.trim(),
  329. supplierId: this.searchData.supplierId ? this.searchData.supplierId.trim() : ''
  330. }
  331. searchPartPriceAnalysis(params).then(({ data }) => {
  332. if (data && data.code === 0) {
  333. this.showProductInfo = true
  334. this.productInfo = Object.assign(createEmptyProductInfo(), data.productInfo || {}, {
  335. partNo: (data.productInfo && data.productInfo.partNo) || params.partNo
  336. })
  337. this.dataList = data.list || []
  338. this.renderChart(this.dataList)
  339. if (this.dataList.length === 0) {
  340. this.$message.info('未查询到历史价格记录')
  341. }
  342. } else {
  343. this.$message.error((data && data.msg) || '查询失败')
  344. }
  345. this.loading = false
  346. }).catch(() => {
  347. this.loading = false
  348. this.$message.error('请求失败')
  349. })
  350. },
  351. openPriceSheet (row) {
  352. if (!row || !row.historyMainId) {
  353. this.$message.warning('当前记录未找到关联价格表')
  354. return
  355. }
  356. const productText = [row.partNo, row.partDesc].filter(Boolean).join(' / ')
  357. this.priceSheetHeader = {
  358. productText,
  359. priceDate: row.priceDate || '',
  360. supplierId: row.supplierId || '',
  361. supplierName: row.supplierName || '',
  362. currency: row.currency || '',
  363. historyMainId: row.historyMainId
  364. }
  365. this.priceSheetDetailList = []
  366. this.priceSheetDialogVisible = true
  367. this.priceSheetLoading = true
  368. const params = {
  369. site: this.$store.state.user.site,
  370. historyMainId: row.historyMainId
  371. }
  372. listTierPriceHistoryDetail(params).then(({ data }) => {
  373. if (data && data.code === 0) {
  374. this.priceSheetDetailList = data.rows || []
  375. } else {
  376. this.priceSheetDetailList = []
  377. this.$message.error((data && data.msg) || '获取价格表明细失败')
  378. }
  379. this.priceSheetLoading = false
  380. }).catch(() => {
  381. this.priceSheetLoading = false
  382. this.priceSheetDetailList = []
  383. this.$message.error('获取价格表明细失败')
  384. })
  385. },
  386. renderChart (list) {
  387. if (!this.chartInstance) {
  388. this.initChart()
  389. }
  390. const chartData = [...list].sort((a, b) => {
  391. return new Date(a.priceDate).getTime() - new Date(b.priceDate).getTime()
  392. })
  393. const xAxisData = chartData.map(item => this.formatChartDate(item.priceDate))
  394. const seriesData = chartData.map(item => item.price)
  395. const titleSuffix = this.searchData.supplierId
  396. ? `${this.searchData.partNo} / ${this.searchData.supplierId}`
  397. : this.searchData.partNo
  398. const option = {
  399. title: {
  400. text: chartData.length ? `产品价格趋势 - ${titleSuffix}` : '暂无数据',
  401. left: 'center',
  402. textStyle: {
  403. fontSize: 14,
  404. fontWeight: 'normal'
  405. }
  406. },
  407. tooltip: {
  408. trigger: 'axis',
  409. formatter: (params) => {
  410. if (!params || !params.length) {
  411. return ''
  412. }
  413. const index = params[0].dataIndex
  414. const row = chartData[index] || {}
  415. return [
  416. `价格日期:${this.formatDateOnly(row.priceDate)}`,
  417. `价格:${this.formatMoney(row.price)}`,
  418. `供应商:${row.supplierId || '-'} ${row.supplierName || ''}`
  419. ].join('<br/>')
  420. }
  421. },
  422. grid: {
  423. left: '3%',
  424. right: '4%',
  425. bottom: '12%',
  426. containLabel: true
  427. },
  428. xAxis: {
  429. type: 'category',
  430. boundaryGap: false,
  431. data: xAxisData,
  432. axisLabel: {
  433. rotate: xAxisData.length > 8 ? 35 : 0
  434. }
  435. },
  436. yAxis: {
  437. type: 'value',
  438. name: '价格',
  439. scale: true
  440. },
  441. series: [{
  442. name: '价格',
  443. type: 'line',
  444. smooth: true,
  445. symbol: 'circle',
  446. symbolSize: 6,
  447. data: seriesData,
  448. connectNulls: true
  449. }]
  450. }
  451. this.chartInstance.setOption(option, true)
  452. this.$nextTick(() => {
  453. if (this.chartInstance) {
  454. this.chartInstance.resize()
  455. }
  456. })
  457. },
  458. formatMoney (value) {
  459. if (value === null || value === undefined || value === '') {
  460. return '-'
  461. }
  462. return new Intl.NumberFormat('zh-CN', {
  463. minimumFractionDigits: 2,
  464. maximumFractionDigits: 2
  465. }).format(value)
  466. },
  467. displayText (value) {
  468. return value === null || value === undefined || value === '' ? '-' : value
  469. },
  470. formatNumber (value) {
  471. if (value === null || value === undefined || value === '') {
  472. return '-'
  473. }
  474. const num = Number(value)
  475. if (Number.isNaN(num)) {
  476. return value
  477. }
  478. return String(Object.is(num, -0) ? 0 : num)
  479. },
  480. getPriceChangeClass (value) {
  481. if (!value || value === '-') {
  482. return ''
  483. }
  484. if (value.startsWith('+')) {
  485. return 'price-up'
  486. }
  487. if (value.startsWith('-')) {
  488. return 'price-down'
  489. }
  490. return ''
  491. },
  492. formatChartDate (value) {
  493. if (!value) {
  494. return ''
  495. }
  496. const date = new Date(value)
  497. if (Number.isNaN(date.getTime())) {
  498. return value
  499. }
  500. const pad = num => String(num).padStart(2, '0')
  501. const year = String(date.getFullYear()).slice(-2)
  502. return `${year}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
  503. },
  504. formatDateTime (value) {
  505. if (!value) {
  506. return ''
  507. }
  508. const date = new Date(value)
  509. if (Number.isNaN(date.getTime())) {
  510. return value
  511. }
  512. const pad = num => String(num).padStart(2, '0')
  513. return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
  514. },
  515. formatDateOnly (value) {
  516. if (!value) {
  517. return ''
  518. }
  519. const date = new Date(value)
  520. if (Number.isNaN(date.getTime())) {
  521. return value
  522. }
  523. const pad = num => String(num).padStart(2, '0')
  524. return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
  525. }
  526. }
  527. }
  528. </script>
  529. <style scoped>
  530. .part-price-analysis .chart-card {
  531. margin-top: 12px;
  532. }
  533. .part-price-analysis .pi-product-form {
  534. margin-top: 2px;
  535. margin-bottom: 10px;
  536. }
  537. .part-price-analysis .pi-price-sheet-form {
  538. margin-top: 2px;
  539. }
  540. .part-price-analysis .chart-card__header {
  541. font-size: 14px;
  542. font-weight: 500;
  543. }
  544. .part-price-analysis .chart-box {
  545. width: 100%;
  546. }
  547. .part-price-analysis .price-up {
  548. color: #f56c6c;
  549. }
  550. .part-price-analysis .price-down {
  551. color: #67c23a;
  552. }
  553. </style>