Digital Office Automation System
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

CommonAnalysis.vue 30KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153
  1. <template>
  2. <div class="app-container">
  3. <!-- 筛选 -->
  4. <div class="filter-card">
  5. <h1>{{ this.params.category }} 销量一览</h1>
  6. <div class="filter-item">
  7. <label>日期范围:</label>
  8. <el-date-picker
  9. v-model="dateRange"
  10. type="daterange"
  11. range-separator="至"
  12. start-placeholder="开始日期"
  13. end-placeholder="结束日期"
  14. format="yyyy-MM-dd"
  15. value-format="yyyy-MM-dd"
  16. style="width: 380px"
  17. size="small"
  18. :picker-options="datePickerOptions"
  19. @change="handleDateChange"
  20. />
  21. </div>
  22. </div>
  23. <div class="filter-section">
  24. <div v-for="(item, key) in filterOptions" :key="key" class="filter-cell">
  25. <div class="cell-header">
  26. <span class="filter-title">{{ key | formatTitle }}</span>
  27. <el-button size="mini" type="text" @click="resetFilter(key)">
  28. 重置
  29. </el-button>
  30. </div>
  31. <div class="cell-content">
  32. <!-- 多选模式:复选框组 -->
  33. <div class="checkbox-group">
  34. <el-checkbox
  35. v-for="option in item"
  36. :key="option"
  37. v-model="selectedValues[key]"
  38. :label="option"
  39. :disabled="isOptionDisabled(key, option)"
  40. size="small"
  41. :class="{
  42. 'available-option': !isOptionDisabled(key, option),
  43. 'disabled-option': isOptionDisabled(key, option)
  44. }"
  45. @change="handleMultipleSelect(key, selectedValues[key])"
  46. >
  47. {{ option }}
  48. </el-checkbox>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="data-section">
  54. <div class="table-chart-data">
  55. <div class="table-chart-data-header">
  56. <span>月度数据</span>
  57. </div>
  58. <div class="table-chart-data-content">
  59. <div class="table-card">
  60. <el-table
  61. :data="monthData"
  62. style="width: 100%"
  63. show-summary
  64. :height="300"
  65. size="mini"
  66. border
  67. >
  68. <el-table-column prop="month" label="日付" />
  69. <el-table-column prop="totalQuantity" label="販売数量" />
  70. <el-table-column
  71. prop="totalAmount"
  72. label="販売金額"
  73. :formatter="formatAmount"
  74. />
  75. </el-table>
  76. </div>
  77. <div id="monthSalesBarChart" class="chart-card" style="height: 300px;" />
  78. </div>
  79. </div>
  80. <div v-for="(item, key) in moreData" :key="key" class="table-chart-data">
  81. <div class="table-chart-data-header">
  82. <span>{{ key | formatTitle }}</span>
  83. </div>
  84. <div class="table-chart-data-content">
  85. <div class="table-card">
  86. <el-table
  87. :data="item"
  88. style="width: 100%"
  89. show-summary
  90. size="mini"
  91. border
  92. :height="300"
  93. >
  94. <el-table-column
  95. v-if="key === 'customer_name'"
  96. prop="customer_name"
  97. label="客户"
  98. />
  99. <el-table-column
  100. v-if="key === 'product_code'"
  101. prop="product_code"
  102. label="商品コード"
  103. />
  104. <el-table-column
  105. v-if="key === 'brand'"
  106. prop="brand"
  107. label="品牌"
  108. />
  109. <el-table-column prop="totalQuantity" label="販売数量" />
  110. <el-table-column
  111. prop="totalAmount"
  112. label="販売金額"
  113. :formatter="formatAmount"
  114. />
  115. </el-table>
  116. </div>
  117. <div
  118. :id="getChartId(key)"
  119. class="chart-card"
  120. style="height: 300px;"
  121. />
  122. </div>
  123. </div>
  124. <div v-for="(item, key, index) in specsData" :key="key" class="table-chart-data">
  125. <div class="table-chart-data-header">
  126. <span>{{ key | formatTitle }}</span>
  127. </div>
  128. <div class="table-chart-data-content">
  129. <div class="table-card">
  130. <el-table
  131. :data="formatSpecsData(item)"
  132. style="width: 100%"
  133. show-summary
  134. :height="300"
  135. size="mini"
  136. border
  137. >
  138. <el-table-column prop="name" :label="key" />
  139. <el-table-column prop="totalQuantity" label="販売数量" />
  140. <el-table-column
  141. prop="totalAmount"
  142. label="販売金額"
  143. :formatter="formatAmount"
  144. />
  145. </el-table>
  146. </div>
  147. <div
  148. :id="getSpecsChartId(key, index)"
  149. class="chart-card"
  150. style="height: 300px;"
  151. />
  152. </div>
  153. </div>
  154. </div>
  155. </div>
  156. </template>
  157. <script>
  158. import * as echarts from 'echarts'
  159. import {
  160. getAnalysisAllFilterOptions,
  161. getAnalysisProductAnalysis,
  162. getAnalysisMonthlyAnalysis,
  163. getAnalysisComprehensiveAnalysis
  164. } from '@/api/sales-analysis'
  165. export default {
  166. name: 'RamAnalysis',
  167. components: {},
  168. filters: {
  169. formatTitle(key) {
  170. if (key === 'product_code') {
  171. return '商品コード'
  172. } else if (key === 'shop_name') {
  173. return '販売店舗'
  174. } else if (key === 'brand') {
  175. return '品牌'
  176. } else if (key === 'customer_name') {
  177. return '客户'
  178. }
  179. return key
  180. }
  181. },
  182. data() {
  183. return {
  184. loading: false,
  185. dateRange: null,
  186. filters: {
  187. productCode: ''
  188. },
  189. // 筛选选项
  190. filterOptions: {},
  191. // 绑定筛选选项
  192. filterBindOptions: [],
  193. // 所有筛选项都使用多选模式
  194. // 每个筛选项的选中值
  195. selectedValues: {},
  196. params: {
  197. startDate: '2025-08-01',
  198. endDate: '2025-12-31',
  199. category: 'SSD'
  200. },
  201. datePickerOptions: {},
  202. // 月度数据
  203. monthData: [],
  204. // 更多数据
  205. moreData: [],
  206. // 规格数据
  207. specsData: {}
  208. }
  209. },
  210. computed: {
  211. /**
  212. * 计算每个筛选项的可用选项
  213. * 根据当前选择和其他筛选项的绑定关系,确定哪些选项是可选的
  214. */
  215. availableOptions() {
  216. const result = {}
  217. // 获取当前所有筛选项的选择状态
  218. const currentSelections = {}
  219. Object.keys(this.filterOptions).forEach((key) => {
  220. const value = this.selectedValues[key]
  221. if (
  222. value !== null &&
  223. value !== undefined &&
  224. Array.isArray(value) &&
  225. value.length > 0
  226. ) {
  227. currentSelections[key] = value
  228. }
  229. })
  230. // 为每个筛选项计算可用选项
  231. Object.keys(this.filterOptions).forEach((filterKey) => {
  232. const options = this.filterOptions[filterKey]
  233. const available = []
  234. options.forEach((option) => {
  235. // 检查这个选项在当前选择下是否可用
  236. if (this.isOptionAvailable(filterKey, option, currentSelections)) {
  237. available.push(option)
  238. }
  239. })
  240. result[filterKey] = available
  241. })
  242. return result
  243. }
  244. },
  245. watch: {
  246. route: {
  247. handler(newVal) {
  248. if (newVal.query.category) {
  249. this.params.category = newVal.query.category
  250. this.fetchData()
  251. }
  252. }
  253. },
  254. specsData: {
  255. handler(newVal) {
  256. console.log('specsData变化:', newVal)
  257. if (newVal && Object.keys(newVal).length > 0) {
  258. this.$nextTick(() => {
  259. setTimeout(() => {
  260. Object.keys(newVal).forEach((key, index) => {
  261. this.renderSpecsPieChart(key, `specsPieChart${index}`)
  262. })
  263. }, 200)
  264. })
  265. }
  266. },
  267. deep: true
  268. }
  269. },
  270. mounted() {
  271. this.buildDatePickerOptions()
  272. this.fetchData()
  273. this.getData()
  274. },
  275. methods: {
  276. // 转换为千位符
  277. formatAmount(row) {
  278. return row.totalAmount.toLocaleString('ja-JP')
  279. },
  280. /**
  281. * 格式化规格数据,将嵌套对象转换为表格数据格式
  282. * @param {Object} specsData - 规格数据对象
  283. * @returns {Array} 格式化后的数组数据
  284. */
  285. formatSpecsData(specsData) {
  286. if (!specsData || typeof specsData !== 'object') {
  287. return []
  288. }
  289. return Object.keys(specsData).map((name) => ({
  290. name: name,
  291. totalQuantity: specsData[name].totalQuantity || 0,
  292. totalAmount: specsData[name].totalAmount || 0
  293. }))
  294. },
  295. /**
  296. * 构建日期选择器选项:禁用未来时间 + 快捷范围
  297. */
  298. buildDatePickerOptions() {
  299. const end = new Date()
  300. const startOfMonth = new Date(end.getFullYear(), end.getMonth(), 1)
  301. this.datePickerOptions = {
  302. disabledDate(time) {
  303. const today = new Date()
  304. // 禁用未来日期(大于今天)
  305. return time.getTime() > today.getTime()
  306. },
  307. shortcuts: [
  308. {
  309. text: '1年',
  310. onClick: (picker) => {
  311. const endDate = new Date()
  312. const startDate = new Date()
  313. startDate.setFullYear(startDate.getFullYear() - 1)
  314. picker.$emit('pick', [formatDate(startDate), formatDate(endDate)])
  315. }
  316. },
  317. {
  318. text: '6个月',
  319. onClick: (picker) => {
  320. const endDate = new Date()
  321. const startDate = new Date()
  322. startDate.setMonth(startDate.getMonth() - 6)
  323. picker.$emit('pick', [formatDate(startDate), formatDate(endDate)])
  324. }
  325. },
  326. {
  327. text: '3个月',
  328. onClick: (picker) => {
  329. const endDate = new Date()
  330. const startDate = new Date()
  331. startDate.setMonth(startDate.getMonth() - 3)
  332. picker.$emit('pick', [formatDate(startDate), formatDate(endDate)])
  333. }
  334. },
  335. {
  336. text: '1个月',
  337. onClick: (picker) => {
  338. const endDate = new Date()
  339. const startDate = new Date()
  340. startDate.setMonth(startDate.getMonth() - 1)
  341. picker.$emit('pick', [formatDate(startDate), formatDate(endDate)])
  342. }
  343. }
  344. ]
  345. }
  346. // 如果当前没有选择日期,默认设置为本月初至今天
  347. if (!this.dateRange) {
  348. this.dateRange = [formatDate(startOfMonth), formatDate(end)]
  349. this.params.startDate = this.dateRange[0]
  350. this.params.endDate = this.dateRange[1]
  351. }
  352. function formatDate(date) {
  353. const y = date.getFullYear()
  354. const m = String(date.getMonth() + 1).padStart(2, '0')
  355. const d = String(date.getDate()).padStart(2, '0')
  356. return `${y}-${m}-${d}`
  357. }
  358. },
  359. handleDateChange(value) {
  360. this.params.startDate = value[0]
  361. this.params.endDate = value[1]
  362. this.fetchData()
  363. },
  364. // 获取数据
  365. async fetchData() {
  366. try {
  367. this.loading = true
  368. const response = await getAnalysisAllFilterOptions(this.params)
  369. this.filterOptions = response
  370. const bindResponse = await getAnalysisProductAnalysis(this.params)
  371. this.filterBindOptions = bindResponse
  372. // 初始化筛选项状态
  373. this.initializeFilterStates()
  374. } catch (error) {
  375. console.error('获取数据失败:', error)
  376. this.$message.error('获取数据失败')
  377. } finally {
  378. this.loading = false
  379. }
  380. },
  381. /**
  382. * 获取月度数据
  383. * 根据当前筛选条件获取月度分析数据
  384. */
  385. async getData() {
  386. try {
  387. // 构建筛选参数,转换为接口需要的重复参数名格式
  388. const filterParams = {}
  389. const categorySpecs = {}
  390. Object.keys(this.selectedValues).forEach((key) => {
  391. const value = this.selectedValues[key]
  392. if (
  393. value !== null &&
  394. value !== undefined &&
  395. Array.isArray(value) &&
  396. value.length > 0
  397. ) {
  398. // 检查是否是规格相关的筛选条件
  399. if (this.specsData && this.specsData[key]) {
  400. // 这是规格筛选条件,添加到categorySpecs中
  401. categorySpecs[key] = value
  402. } else {
  403. // 转换键名
  404. let newKey = '';
  405. if(key === 'product_code') {
  406. newKey = 'productCode';
  407. } else if(key === 'shop_name') {
  408. newKey = 'shopName';
  409. } else if(key === 'brand') {
  410. newKey = 'brand';
  411. } else if(key === 'customer_name') {
  412. newKey = 'customerName';
  413. } else {
  414. newKey = key;
  415. }
  416. // 将数组转换为重复参数名格式
  417. filterParams[newKey] = value
  418. }
  419. }
  420. })
  421. // 如果有规格筛选条件,添加到filterParams中
  422. if (Object.keys(categorySpecs).length > 0) {
  423. filterParams.categorySpecs = categorySpecs
  424. }
  425. console.log('筛选参数:', filterParams)
  426. console.log('规格筛选条件:', categorySpecs)
  427. const response = await getAnalysisMonthlyAnalysis({
  428. ...this.params,
  429. ...filterParams
  430. })
  431. this.monthData = response
  432. const resData = await getAnalysisComprehensiveAnalysis({
  433. ...this.params,
  434. ...filterParams
  435. })
  436. // 处理规格数据
  437. if (resData.category_specs) {
  438. this.specsData = resData.category_specs
  439. console.log('规格数据:', this.specsData)
  440. delete resData.category_specs
  441. } else {
  442. this.specsData = {}
  443. console.log('未找到规格数据')
  444. }
  445. this.moreData = resData
  446. // 渲染图表
  447. this.renderAllCharts()
  448. // if (resData.length) {
  449. // this.productData = resData[1].product_code
  450. // this.shopData = resData[0].shop_name
  451. // } else {
  452. // this.productData = []
  453. // this.shopData = []
  454. // }
  455. } catch (error) {
  456. console.error(error)
  457. }
  458. },
  459. /**
  460. * 初始化筛选项状态
  461. * 为每个筛选项设置默认的多选模式和空数组
  462. */
  463. initializeFilterStates() {
  464. const values = {}
  465. Object.keys(this.filterOptions).forEach((key) => {
  466. values[key] = [] // 默认无选中值,使用空数组
  467. })
  468. this.selectedValues = values
  469. },
  470. /**
  471. * 重置筛选项选择
  472. * @param {string} filterKey - 筛选项的键名
  473. */
  474. resetFilter(filterKey) {
  475. this.selectedValues[filterKey] = []
  476. },
  477. /**
  478. * 处理多选值变化
  479. * @param {string} filterKey - 筛选项的键名
  480. * @param {Array} values - 选中的值数组
  481. */
  482. handleMultipleSelect(filterKey, values) {
  483. this.selectedValues[filterKey] = values
  484. this.onFilterChange()
  485. },
  486. /**
  487. * 筛选条件变化时的回调
  488. */
  489. onFilterChange() {
  490. console.log('筛选条件变化:', this.selectedValues)
  491. // 调用月度数据获取
  492. this.getData()
  493. },
  494. /**
  495. * 检查某个选项是否可用
  496. * @param {string} filterKey - 筛选项的键名
  497. * @param {string} option - 要检查的选项值
  498. * @param {Object} currentSelections - 当前所有筛选项的选择状态
  499. * @returns {boolean} 是否可用
  500. */
  501. isOptionAvailable(filterKey, option, currentSelections) {
  502. // 如果没有绑定数据或不是数组,所有选项都可用
  503. if (
  504. !this.filterBindOptions ||
  505. !Array.isArray(this.filterBindOptions) ||
  506. this.filterBindOptions.length === 0
  507. ) {
  508. return true
  509. }
  510. // 构建临时选择状态来测试这个选项
  511. const testSelections = { ...currentSelections }
  512. testSelections[filterKey] = [option]
  513. // 检查是否有任何绑定记录匹配这个选择组合
  514. return this.filterBindOptions.some((record) => {
  515. return this.isSelectionMatch(record, testSelections)
  516. })
  517. },
  518. /**
  519. * 检查选择状态是否匹配绑定记录
  520. * @param {Object} record - 绑定记录
  521. * @param {Object} selections - 选择状态
  522. * @returns {boolean} 是否匹配
  523. */
  524. isSelectionMatch(record, selections) {
  525. // 检查所有筛选项是否都匹配
  526. return Object.keys(selections).every((filterKey) => {
  527. const selectedValues = selections[filterKey]
  528. const recordValues = record[filterKey]
  529. // 如果记录中没有这个筛选项,跳过检查
  530. if (!recordValues) {
  531. return true
  532. }
  533. // 检查是否有交集
  534. return selectedValues.some((selected) =>
  535. recordValues.includes(selected)
  536. )
  537. })
  538. },
  539. /**
  540. * 检查选项是否被禁用
  541. * @param {string} filterKey - 筛选项的键名
  542. * @param {string} option - 选项值
  543. * @returns {boolean} 是否被禁用
  544. */
  545. isOptionDisabled(filterKey, option) {
  546. return !this.availableOptions[filterKey]?.includes(option)
  547. },
  548. /**
  549. * 渲染月度数据柱形图
  550. */
  551. renderMonthSalesChart() {
  552. this.$nextTick(() => {
  553. const chartDom = document.getElementById('monthSalesBarChart')
  554. if (!chartDom) return
  555. const myChart = echarts.init(chartDom)
  556. const option = {
  557. backgroundColor: 'transparent',
  558. tooltip: {
  559. trigger: 'axis',
  560. axisPointer: {
  561. type: 'shadow'
  562. },
  563. formatter: function(params) {
  564. let result = params[0].name + '<br/>'
  565. params.forEach(param => {
  566. if (param.seriesName === '販売数量') {
  567. result += param.seriesName + ': ' + param.value + '<br/>'
  568. } else {
  569. result += param.seriesName + ': ¥' + param.value.toLocaleString('ja-JP')
  570. }
  571. })
  572. return result
  573. }
  574. },
  575. legend: {
  576. data: ['販売数量', '販売金額'],
  577. textStyle: {
  578. color: '#333'
  579. }
  580. },
  581. xAxis: {
  582. type: 'category',
  583. data: this.monthData.map(item => item.month),
  584. axisLabel: {
  585. color: '#666'
  586. }
  587. },
  588. yAxis: [
  589. {
  590. type: 'value',
  591. name: '販売数量',
  592. position: 'left',
  593. axisLabel: {
  594. color: '#666'
  595. }
  596. },
  597. {
  598. type: 'value',
  599. name: '販売金額',
  600. position: 'right',
  601. axisLabel: {
  602. color: '#666',
  603. formatter: function(value) {
  604. return '¥' + (value / 10000).toFixed(0) + '万'
  605. }
  606. }
  607. }
  608. ],
  609. series: [
  610. {
  611. name: '販売数量',
  612. type: 'bar',
  613. data: this.monthData.map(item => item.totalQuantity),
  614. itemStyle: {
  615. color: '#5470c6'
  616. }
  617. },
  618. {
  619. name: '販売金額',
  620. type: 'bar',
  621. yAxisIndex: 1,
  622. data: this.monthData.map(item => item.totalAmount),
  623. itemStyle: {
  624. color: '#91cc75'
  625. }
  626. }
  627. ]
  628. }
  629. myChart.setOption(option)
  630. window.addEventListener('resize', () => myChart.resize())
  631. })
  632. },
  633. /**
  634. * 渲染客户销量占比饼图
  635. */
  636. renderCustomerPieChart() {
  637. this.$nextTick(() => {
  638. const chartDom = document.getElementById('customerPieChart')
  639. if (!chartDom) return
  640. const myChart = echarts.init(chartDom)
  641. const data = this.moreData.customer_name || []
  642. const option = {
  643. backgroundColor: 'transparent',
  644. tooltip: {
  645. trigger: 'item',
  646. formatter: '{a} <br/>{b}: {c} ({d}%)'
  647. },
  648. legend: {
  649. orient: 'vertical',
  650. left: 'left',
  651. textStyle: {
  652. color: '#333'
  653. }
  654. },
  655. series: [
  656. {
  657. name: '客户销量占比',
  658. type: 'pie',
  659. radius: '50%',
  660. data: data.map(item => ({
  661. value: item.totalQuantity,
  662. name: item.customer_name
  663. })),
  664. emphasis: {
  665. itemStyle: {
  666. shadowBlur: 10,
  667. shadowOffsetX: 0,
  668. shadowColor: 'rgba(0, 0, 0, 0.5)'
  669. }
  670. }
  671. }
  672. ]
  673. }
  674. myChart.setOption(option)
  675. window.addEventListener('resize', () => myChart.resize())
  676. })
  677. },
  678. /**
  679. * 渲染商品销量排行柱形图
  680. */
  681. renderProductRankingChart() {
  682. this.$nextTick(() => {
  683. const chartDom = document.getElementById('productRankingChart')
  684. if (!chartDom) return
  685. const myChart = echarts.init(chartDom)
  686. const data = this.moreData.product_code || []
  687. const option = {
  688. backgroundColor: 'transparent',
  689. tooltip: {
  690. trigger: 'axis',
  691. axisPointer: {
  692. type: 'shadow'
  693. }
  694. },
  695. grid: {
  696. left: '3%',
  697. right: '4%',
  698. bottom: '3%',
  699. containLabel: true
  700. },
  701. xAxis: {
  702. type: 'value',
  703. axisLabel: {
  704. color: '#666'
  705. }
  706. },
  707. yAxis: {
  708. type: 'category',
  709. data: data.map(item => item.product_code),
  710. axisLabel: {
  711. color: '#666'
  712. }
  713. },
  714. series: [
  715. {
  716. name: '販売数量',
  717. type: 'bar',
  718. data: data.map(item => item.totalQuantity),
  719. itemStyle: {
  720. color: '#5470c6'
  721. }
  722. }
  723. ]
  724. }
  725. myChart.setOption(option)
  726. window.addEventListener('resize', () => myChart.resize())
  727. })
  728. },
  729. /**
  730. * 渲染品牌销量占比饼图
  731. */
  732. renderBrandPieChart() {
  733. this.$nextTick(() => {
  734. const chartDom = document.getElementById('brandPieChart')
  735. if (!chartDom) return
  736. const myChart = echarts.init(chartDom)
  737. const data = this.moreData.brand || []
  738. const option = {
  739. backgroundColor: 'transparent',
  740. tooltip: {
  741. trigger: 'item',
  742. formatter: '{a} <br/>{b}: {c} ({d}%)'
  743. },
  744. legend: {
  745. orient: 'vertical',
  746. left: 'left',
  747. textStyle: {
  748. color: '#333'
  749. }
  750. },
  751. series: [
  752. {
  753. name: '品牌销量占比',
  754. type: 'pie',
  755. radius: '50%',
  756. data: data.map(item => ({
  757. value: item.totalQuantity,
  758. name: item.brand
  759. })),
  760. emphasis: {
  761. itemStyle: {
  762. shadowBlur: 10,
  763. shadowOffsetX: 0,
  764. shadowColor: 'rgba(0, 0, 0, 0.5)'
  765. }
  766. }
  767. }
  768. ]
  769. }
  770. myChart.setOption(option)
  771. window.addEventListener('resize', () => myChart.resize())
  772. })
  773. },
  774. /**
  775. * 渲染规格销售占比饼图
  776. */
  777. renderSpecsPieChart(specKey, chartId) {
  778. this.$nextTick(() => {
  779. const chartDom = document.getElementById(chartId)
  780. if (!chartDom) {
  781. console.warn(`图表容器未找到: ${chartId}`)
  782. return
  783. }
  784. const myChart = echarts.init(chartDom)
  785. const data = this.formatSpecsData(this.specsData[specKey])
  786. console.log(`渲染规格图表: ${specKey}`, {
  787. chartId,
  788. data,
  789. specsData: this.specsData[specKey]
  790. })
  791. if (!data || data.length === 0) {
  792. console.warn(`规格数据为空: ${specKey}`)
  793. return
  794. }
  795. const option = {
  796. backgroundColor: 'transparent',
  797. tooltip: {
  798. trigger: 'item',
  799. formatter: '{a} <br/>{b}: {c} ({d}%)'
  800. },
  801. legend: {
  802. orient: 'vertical',
  803. left: 'left',
  804. textStyle: {
  805. color: '#333'
  806. }
  807. },
  808. series: [
  809. {
  810. name: specKey + '销售占比',
  811. type: 'pie',
  812. radius: '50%',
  813. data: data.map(item => ({
  814. value: item.totalQuantity,
  815. name: item.name
  816. })),
  817. emphasis: {
  818. itemStyle: {
  819. shadowBlur: 10,
  820. shadowOffsetX: 0,
  821. shadowColor: 'rgba(0, 0, 0, 0.5)'
  822. }
  823. }
  824. }
  825. ]
  826. }
  827. myChart.setOption(option)
  828. window.addEventListener('resize', () => myChart.resize())
  829. })
  830. },
  831. /**
  832. * 获取图表ID
  833. * @param {string} key - 数据键名
  834. * @returns {string} 图表ID
  835. */
  836. getChartId(key) {
  837. if (key === 'customer_name') {
  838. return 'customerPieChart'
  839. } else if (key === 'product_code') {
  840. return 'productRankingChart'
  841. } else if (key === 'brand') {
  842. return 'brandPieChart'
  843. }
  844. return 'defaultChart'
  845. },
  846. /**
  847. * 获取规格图表ID
  848. * @param {string} key - 规格键名
  849. * @param {number} index - 索引
  850. * @returns {string} 图表ID
  851. */
  852. getSpecsChartId(key, index) {
  853. return `specsPieChart${index}`
  854. },
  855. /**
  856. * 渲染所有图表
  857. */
  858. renderAllCharts() {
  859. // 先渲染基础图表
  860. this.renderMonthSalesChart()
  861. this.renderCustomerPieChart()
  862. this.renderProductRankingChart()
  863. this.renderBrandPieChart()
  864. // 规格图表由watch监听器处理
  865. }
  866. }
  867. }
  868. </script>
  869. <style lang="scss" scoped>
  870. .app-container {
  871. display: flex;
  872. flex-direction: column;
  873. gap: 10px;
  874. padding: 10px;
  875. width: 100%;
  876. min-height: 100vh;
  877. box-sizing: border-box;
  878. }
  879. .filter-card {
  880. background-color: #fff;
  881. padding: 10px 20px;
  882. border-radius: 8px;
  883. border: 1px solid #e8e8e8;
  884. display: flex;
  885. justify-content: space-between;
  886. align-items: center;
  887. gap: 20px;
  888. }
  889. .filter-section {
  890. display: flex;
  891. flex-direction: column;
  892. gap: 8px;
  893. }
  894. .filter-cell {
  895. background-color: #fff;
  896. border-radius: 6px;
  897. border: 1px solid #e8e8e8;
  898. overflow: hidden;
  899. display: flex;
  900. }
  901. .cell-header {
  902. display: flex;
  903. justify-content: space-between;
  904. align-items: center;
  905. padding: 8px 12px;
  906. background-color: #f8f9fa;
  907. width: 150px;
  908. }
  909. .cell-content {
  910. padding: 8px 12px;
  911. flex-grow: 1;
  912. width: 0;
  913. display: flex;
  914. align-items: center;
  915. }
  916. .filter-title {
  917. font-weight: 500;
  918. color: #333;
  919. font-size: 14px;
  920. }
  921. .header-buttons {
  922. display: flex;
  923. gap: 8px;
  924. }
  925. .header-buttons .el-button {
  926. padding: 4px 8px;
  927. font-size: 12px;
  928. color: #666;
  929. }
  930. .header-buttons .el-button:hover {
  931. color: #409eff;
  932. }
  933. .header-buttons .el-button.active-mode {
  934. color: #409eff;
  935. font-weight: 500;
  936. }
  937. .checkbox-group {
  938. display: flex;
  939. flex-direction: row;
  940. flex-wrap: wrap;
  941. gap: 8px 12px;
  942. max-height: 120px;
  943. overflow-y: auto;
  944. }
  945. .checkbox-group .el-checkbox {
  946. margin-right: 0;
  947. margin-bottom: 0;
  948. white-space: nowrap;
  949. flex-shrink: 0;
  950. }
  951. .checkbox-group .el-checkbox__label {
  952. font-size: 13px;
  953. color: #666;
  954. }
  955. /* 可用选项的高亮样式 */
  956. .available-option .el-checkbox__label {
  957. color: #333;
  958. font-weight: 500;
  959. }
  960. .available-option:hover .el-checkbox__label {
  961. color: #409eff;
  962. }
  963. /* 禁用选项的样式 */
  964. .disabled-option .el-checkbox__label {
  965. color: #c0c4cc;
  966. text-decoration: line-through;
  967. }
  968. .disabled-option .el-checkbox__input.is-disabled .el-checkbox__inner {
  969. background-color: #f5f7fa;
  970. border-color: #e4e7ed;
  971. }
  972. /* 选中状态的样式 */
  973. .available-option.is-checked .el-checkbox__label {
  974. color: #409eff;
  975. font-weight: 600;
  976. }
  977. .filter-row {
  978. display: flex;
  979. align-items: center;
  980. gap: 20px;
  981. flex-wrap: wrap;
  982. }
  983. .filter-item {
  984. display: flex;
  985. align-items: center;
  986. gap: 8px;
  987. }
  988. .filter-item label {
  989. font-weight: 500;
  990. color: #333;
  991. white-space: nowrap;
  992. }
  993. .charts-section {
  994. width: 100%;
  995. min-width: 0;
  996. display: flex;
  997. flex-direction: column;
  998. gap: 10px;
  999. }
  1000. .chart-card {
  1001. background-color: #fff;
  1002. border-radius: 8px;
  1003. overflow: hidden;
  1004. }
  1005. .card-header {
  1006. display: flex;
  1007. align-items: center;
  1008. justify-content: center;
  1009. font-size: 18px;
  1010. padding: 15px 20px;
  1011. }
  1012. .chart-container {
  1013. padding: 20px;
  1014. width: 100%;
  1015. height: auto;
  1016. min-height: 0;
  1017. overflow: hidden;
  1018. }
  1019. .data-section {
  1020. display: grid;
  1021. grid-template-columns: repeat(2, 1fr);
  1022. gap: 10px;
  1023. .table-chart-data {
  1024. .table-chart-data-header {
  1025. text-align: center;
  1026. font-size: 18px;
  1027. font-weight: 500;
  1028. color: #333;
  1029. padding: 10px 0;
  1030. }
  1031. .table-chart-data-content {
  1032. display: grid;
  1033. gap: 10px;
  1034. background-color: #fff;
  1035. border-radius: 8px;
  1036. border: 1px solid #e8e8e8;
  1037. .table-card {
  1038. width: 100%;
  1039. height: 100%;
  1040. }
  1041. .chart-card {
  1042. width: 100%;
  1043. height: 100%;
  1044. min-height: 300px;
  1045. }
  1046. }
  1047. }
  1048. }
  1049. .no-data-section {
  1050. display: flex;
  1051. justify-content: center;
  1052. align-items: center;
  1053. min-height: 400px;
  1054. flex-direction: column;
  1055. }
  1056. .no-data-content {
  1057. text-align: center;
  1058. color: #999;
  1059. }
  1060. .no-data-content i {
  1061. font-size: 48px;
  1062. margin-bottom: 20px;
  1063. color: #ccc;
  1064. display: block;
  1065. }
  1066. .no-data-content p {
  1067. font-size: 16px;
  1068. margin: 0 0 10px 0;
  1069. color: #666;
  1070. }
  1071. .no-data-content small {
  1072. font-size: 14px;
  1073. color: #aaa;
  1074. }
  1075. </style>