index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <template>
  2. <div class="app-container home p-6">
  3. <div class="dashboard-header">
  4. <div class="dashboard-title">
  5. <span class="title-text">概览数据看板</span>
  6. <span class="title-desc">Overview Dashboard</span>
  7. </div>
  8. <el-select v-model="activeProject" placeholder="请选择项目" class="project-select" filterable>
  9. <el-option v-for="opt in projectOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
  10. </el-select>
  11. </div>
  12. <!-- Stats Cards -->
  13. <el-row :gutter="20" class="kpi-row">
  14. <el-col :xs="12" :sm="12" :md="6">
  15. <el-card shadow="hover" class="kpi-card custom-card primary-card">
  16. <div class="kpi-inner">
  17. <div class="kpi-content">
  18. <div class="kpi-label">订单总量</div>
  19. <div class="kpi-value">
  20. <span class="num">{{ currentStats.orderCount }}</span>
  21. <span class="unit">单</span>
  22. </div>
  23. </div>
  24. <div class="kpi-icon-wrapper">
  25. <el-icon class="kpi-icon"><Tickets /></el-icon>
  26. </div>
  27. </div>
  28. </el-card>
  29. </el-col>
  30. <el-col :xs="12" :sm="12" :md="6">
  31. <el-card shadow="hover" class="kpi-card custom-card success-card">
  32. <div class="kpi-inner">
  33. <div class="kpi-content">
  34. <div class="kpi-label">商品数量</div>
  35. <div class="kpi-value">
  36. <span class="num">{{ currentStats.productCount }}</span>
  37. <span class="unit">件</span>
  38. </div>
  39. </div>
  40. <div class="kpi-icon-wrapper">
  41. <el-icon class="kpi-icon"><Goods /></el-icon>
  42. </div>
  43. </div>
  44. </el-card>
  45. </el-col>
  46. <el-col :xs="12" :sm="12" :md="6">
  47. <el-card shadow="hover" class="kpi-card custom-card warning-card">
  48. <div class="kpi-inner">
  49. <div class="kpi-content">
  50. <div class="kpi-label">热销商品</div>
  51. <div class="kpi-value">
  52. <span class="num">{{ currentStats.hotProductCount }}</span>
  53. <span class="unit">款</span>
  54. </div>
  55. </div>
  56. <div class="kpi-icon-wrapper">
  57. <el-icon class="kpi-icon"><PriceTag /></el-icon>
  58. </div>
  59. </div>
  60. </el-card>
  61. </el-col>
  62. <el-col :xs="12" :sm="12" :md="6">
  63. <el-card shadow="hover" class="kpi-card custom-card danger-card">
  64. <div class="kpi-inner">
  65. <div class="kpi-content">
  66. <div class="kpi-label">售后订单</div>
  67. <div class="kpi-value">
  68. <span class="num">{{ currentStats.afterSaleCount }}</span>
  69. <span class="unit">单</span>
  70. </div>
  71. </div>
  72. <div class="kpi-icon-wrapper">
  73. <el-icon class="kpi-icon"><Box /></el-icon>
  74. </div>
  75. </div>
  76. </el-card>
  77. </el-col>
  78. </el-row>
  79. <!-- Charts Row -->
  80. <el-row :gutter="20" class="board-row">
  81. <el-col :xs="24" :md="14">
  82. <el-card shadow="hover" class="board-card custom-card border-card">
  83. <template #header>
  84. <div class="board-card-title">
  85. <span class="dot primary-dot"></span>
  86. <span>订单趋势线</span>
  87. </div>
  88. </template>
  89. <div ref="orderTrendRef" class="chart chart-large" />
  90. </el-card>
  91. </el-col>
  92. <el-col :xs="24" :md="10">
  93. <el-card shadow="hover" class="board-card custom-card border-card">
  94. <template #header>
  95. <div class="board-card-title">
  96. <span class="dot warning-dot"></span>
  97. <span>售后状态占比</span>
  98. </div>
  99. </template>
  100. <div ref="afterSalePieRef" class="chart chart-large" />
  101. </el-card>
  102. </el-col>
  103. </el-row>
  104. <!-- Tables Row -->
  105. <el-row :gutter="20" class="board-row">
  106. <el-col :xs="24" :md="14">
  107. <el-card shadow="hover" class="board-card custom-card border-card">
  108. <template #header>
  109. <div class="board-card-title">
  110. <span class="dot success-dot"></span>
  111. <span>热销商品排行榜 TOP 5</span>
  112. </div>
  113. </template>
  114. <el-table
  115. :data="currentStats.hotProducts"
  116. style="width: 100%"
  117. :header-cell-style="{ background: '#f8f9fa', color: '#333', fontWeight: 'bold' }"
  118. stripe
  119. >
  120. <el-table-column label="排名" type="index" width="80" align="center">
  121. <template #default="scope">
  122. <span :class="['rank-badge', `rank-${scope.$index + 1}`]">
  123. {{ scope.$index + 1 }}
  124. </span>
  125. </template>
  126. </el-table-column>
  127. <el-table-column label="商品名称" prop="name" show-overflow-tooltip>
  128. <template #default="scope">
  129. <span class="product-name">{{ scope.row.name }}</span>
  130. </template>
  131. </el-table-column>
  132. <el-table-column label="近期销量" prop="sales" width="120" align="center">
  133. <template #default="scope">
  134. <el-tag type="danger" effect="light" size="small">{{ scope.row.sales }} 件</el-tag>
  135. </template>
  136. </el-table-column>
  137. </el-table>
  138. </el-card>
  139. </el-col>
  140. <el-col :xs="24" :md="10">
  141. <el-card shadow="hover" class="board-card custom-card border-card">
  142. <template #header>
  143. <div class="board-card-title">
  144. <span class="dot danger-dot"></span>
  145. <span>近期售后动态</span>
  146. </div>
  147. <div class="mini-kpi-wrap">
  148. <div class="mini-kpi">
  149. <div class="m-label">待处理</div>
  150. <div class="m-value text-danger">{{ currentStats.afterSalePending }}</div>
  151. </div>
  152. <div class="mini-v-divider"></div>
  153. <div class="mini-kpi">
  154. <div class="m-label">已完成</div>
  155. <div class="m-value text-success">{{ currentStats.afterSaleDone }}</div>
  156. </div>
  157. </div>
  158. </template>
  159. <el-table
  160. :data="currentStats.recentAfterSales"
  161. style="width: 100%"
  162. :header-cell-style="{ background: '#f8f9fa' }"
  163. >
  164. <el-table-column label="售后单号" prop="no" show-overflow-tooltip min-width="140" />
  165. <el-table-column label="状态" prop="status" width="90" align="center">
  166. <template #default="scope">
  167. <el-tag :type="scope.row.status === '已完成' ? 'success' : 'warning'" size="small">
  168. {{ scope.row.status }}
  169. </el-tag>
  170. </template>
  171. </el-table-column>
  172. <el-table-column label="申请时间" prop="time" width="110" />
  173. </el-table>
  174. </el-card>
  175. </el-col>
  176. </el-row>
  177. </div>
  178. </template>
  179. <script setup name="Index" lang="ts">
  180. import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue';
  181. import { Box, Goods, PriceTag, Tickets } from '@element-plus/icons-vue';
  182. import * as echarts from 'echarts';
  183. type ProjectKey = 'zhongzhi' | 'zhongche';
  184. const activeProject = ref<ProjectKey>('zhongzhi');
  185. const projectOptions: Array<{ label: string; value: ProjectKey }> = [
  186. { label: '中直', value: 'zhongzhi' },
  187. { label: '中车', value: 'zhongche' }
  188. ];
  189. const dashboardData = reactive<Record<ProjectKey, any>>({
  190. zhongzhi: {
  191. orderCount: 1280,
  192. productCount: 3560,
  193. hotProductCount: 56,
  194. afterSaleCount: 38,
  195. afterSalePending: 9,
  196. afterSaleDone: 29,
  197. hotProducts: [
  198. { name: '中直-热销商品型号 2024款', sales: 320 },
  199. { name: '高配自选升级包 豪华版', sales: 265 },
  200. { name: '专业定制服务 年卡版', sales: 210 },
  201. { name: '中直-专属增值服务包', sales: 154 },
  202. { name: '标准配件套装 (多色)', sales: 112 }
  203. ],
  204. recentAfterSales: [
  205. { no: 'AS20250303001', status: '待处理', time: '03-03 10:20' },
  206. { no: 'AS20250303002', status: '已完成', time: '03-03 09:12' },
  207. { no: 'AS20250303003', status: '待处理', time: '03-02 14:35' },
  208. { no: 'AS20250302004', status: '已完成', time: '03-02 11:21' }
  209. ],
  210. orderTrend: {
  211. xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
  212. orderSeries: [120, 160, 140, 220, 260, 200, 180],
  213. amountSeries: [18, 26, 22, 35, 42, 31, 28]
  214. },
  215. afterSalePie: [
  216. { name: '待处理', value: 9 },
  217. { name: '处理中', value: 6 },
  218. { name: '已完成', value: 23 }
  219. ]
  220. },
  221. zhongche: {
  222. orderCount: 980,
  223. productCount: 2890,
  224. hotProductCount: 44,
  225. afterSaleCount: 26,
  226. afterSalePending: 5,
  227. afterSaleDone: 21,
  228. hotProducts: [
  229. { name: '中车-特供机型 v2', sales: 280 },
  230. { name: '工业配件标准件包', sales: 230 },
  231. { name: '中车-定制保养套装', sales: 190 },
  232. { name: '机电耗材包 (月卡)', sales: 140 },
  233. { name: '基础版维修备件', sales: 90 }
  234. ],
  235. recentAfterSales: [
  236. { no: 'AS20250303011', status: '已完成', time: '03-03 14:10' },
  237. { no: 'AS20250303012', status: '待处理', time: '03-02 09:30' },
  238. { no: 'AS20250303013', status: '已完成', time: '03-01 16:45' },
  239. { no: 'AS20250302014', status: '待处理', time: '03-01 11:05' }
  240. ],
  241. orderTrend: {
  242. xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
  243. orderSeries: [80, 110, 95, 140, 170, 150, 130],
  244. amountSeries: [12, 18, 15, 22, 28, 24, 20]
  245. },
  246. afterSalePie: [
  247. { name: '待处理', value: 5 },
  248. { name: '处理中', value: 4 },
  249. { name: '已完成', value: 17 }
  250. ]
  251. }
  252. });
  253. const currentStats = computed(() => dashboardData[activeProject.value]);
  254. const orderTrendRef = ref<HTMLDivElement>();
  255. const afterSalePieRef = ref<HTMLDivElement>();
  256. let orderTrendChart: echarts.ECharts | undefined;
  257. let afterSalePieChart: echarts.ECharts | undefined;
  258. const renderCharts = () => {
  259. const stats = currentStats.value;
  260. if (orderTrendRef.value) {
  261. if (!orderTrendChart) {
  262. orderTrendChart = echarts.init(orderTrendRef.value);
  263. }
  264. orderTrendChart.setOption({
  265. backgroundColor: 'transparent',
  266. tooltip: {
  267. trigger: 'axis',
  268. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  269. borderColor: '#eee',
  270. padding: 10,
  271. textStyle: { color: '#333' }
  272. },
  273. legend: {
  274. data: ['订单量', '销售额(万)'],
  275. top: 0
  276. },
  277. grid: { left: 40, right: 20, top: 40, bottom: 30, containLabel: true },
  278. xAxis: {
  279. type: 'category',
  280. data: stats.orderTrend.xAxis,
  281. axisLine: { lineStyle: { color: '#ddd' } },
  282. axisLabel: { color: '#666' }
  283. },
  284. yAxis: [
  285. {
  286. type: 'value',
  287. name: '订单量',
  288. nameTextStyle: { color: '#999' },
  289. splitLine: { lineStyle: { type: 'dashed', color: '#eee' } },
  290. axisLabel: { color: '#666' }
  291. },
  292. {
  293. type: 'value',
  294. name: '销售额',
  295. nameTextStyle: { color: '#999' },
  296. splitLine: { show: false },
  297. axisLabel: { color: '#666' }
  298. }
  299. ],
  300. series: [
  301. {
  302. name: '订单量',
  303. type: 'line',
  304. smooth: true,
  305. symbolSize: 8,
  306. itemStyle: { color: '#409EFF' },
  307. areaStyle: {
  308. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  309. { offset: 0, color: 'rgba(64,158,255,0.3)' },
  310. { offset: 1, color: 'rgba(64,158,255,0)' }
  311. ])
  312. },
  313. data: stats.orderTrend.orderSeries
  314. },
  315. {
  316. name: '销售额(万)',
  317. type: 'bar',
  318. yAxisIndex: 1,
  319. barWidth: 12,
  320. itemStyle: {
  321. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  322. { offset: 0, color: '#36cfc9' },
  323. { offset: 1, color: '#009688' }
  324. ]),
  325. borderRadius: [4, 4, 0, 0]
  326. },
  327. data: stats.orderTrend.amountSeries
  328. }
  329. ]
  330. });
  331. }
  332. if (afterSalePieRef.value) {
  333. if (!afterSalePieChart) {
  334. afterSalePieChart = echarts.init(afterSalePieRef.value);
  335. }
  336. afterSalePieChart.setOption({
  337. backgroundColor: 'transparent',
  338. tooltip: { trigger: 'item' },
  339. legend: { bottom: 0, left: 'center', itemWidth: 10, itemHeight: 10, icon: 'circle' },
  340. color: ['#F56C6C', '#E6A23C', '#67C23A'],
  341. series: [
  342. {
  343. name: '售后状态',
  344. type: 'pie',
  345. radius: ['45%', '70%'],
  346. center: ['50%', '42%'],
  347. avoidLabelOverlap: false,
  348. itemStyle: {
  349. borderRadius: 6,
  350. borderColor: '#fff',
  351. borderWidth: 2
  352. },
  353. label: {
  354. show: true,
  355. formatter: '{b}\n{d}%',
  356. color: '#666'
  357. },
  358. labelLine: { show: true, length: 15, length2: 10 },
  359. data: stats.afterSalePie
  360. }
  361. ]
  362. });
  363. }
  364. };
  365. const resizeCharts = () => {
  366. orderTrendChart?.resize();
  367. afterSalePieChart?.resize();
  368. };
  369. watch(activeProject, () => {
  370. renderCharts();
  371. });
  372. onMounted(() => {
  373. renderCharts();
  374. window.addEventListener('resize', resizeCharts);
  375. });
  376. </script>
  377. <style lang="scss" scoped>
  378. .app-container {
  379. background-color: #f2f5f9;
  380. min-height: calc(100vh - 84px);
  381. padding: 20px;
  382. }
  383. .dashboard-header {
  384. display: flex;
  385. justify-content: space-between;
  386. align-items: center;
  387. margin-bottom: 24px;
  388. .dashboard-title {
  389. display: flex;
  390. align-items: baseline;
  391. gap: 12px;
  392. .title-text {
  393. font-size: 22px;
  394. font-weight: 700;
  395. color: #2c3e50;
  396. letter-spacing: 1px;
  397. }
  398. .title-desc {
  399. font-size: 14px;
  400. color: #909399;
  401. font-weight: 400;
  402. letter-spacing: 0.5px;
  403. }
  404. }
  405. .project-select {
  406. width: 180px;
  407. :deep(.el-input__wrapper) {
  408. border-radius: 20px;
  409. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  410. background-color: #fff;
  411. padding: 2px 15px;
  412. transition: all 0.3s;
  413. &:hover, &.is-focus {
  414. box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
  415. }
  416. }
  417. }
  418. }
  419. .kpi-row, .board-row {
  420. margin-bottom: 20px;
  421. }
  422. .custom-card {
  423. border: none;
  424. border-radius: 12px;
  425. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  426. background: #fff;
  427. &:hover {
  428. transform: translateY(-4px);
  429. box-shadow: 0 10px 20px rgba(0,0,0,0.08);
  430. }
  431. }
  432. /* KPI Cards */
  433. .kpi-card {
  434. overflow: hidden;
  435. position: relative;
  436. .kpi-inner {
  437. display: flex;
  438. justify-content: space-between;
  439. align-items: center;
  440. padding: 24px 20px;
  441. position: relative;
  442. z-index: 1;
  443. }
  444. .kpi-label {
  445. font-size: 15px;
  446. color: #fff;
  447. opacity: 0.9;
  448. margin-bottom: 8px;
  449. font-weight: 500;
  450. }
  451. .kpi-value {
  452. color: #fff;
  453. .num {
  454. font-size: 32px;
  455. font-weight: 700;
  456. line-height: 1;
  457. font-family: 'Rubik', Arial, sans-serif;
  458. }
  459. .unit {
  460. font-size: 14px;
  461. margin-left: 4px;
  462. opacity: 0.8;
  463. }
  464. }
  465. .kpi-icon-wrapper {
  466. width: 60px;
  467. height: 60px;
  468. border-radius: 50%;
  469. display: flex;
  470. align-items: center;
  471. justify-content: center;
  472. background: rgba(255, 255, 255, 0.2);
  473. backdrop-filter: blur(4px);
  474. .kpi-icon {
  475. font-size: 30px;
  476. color: #fff;
  477. }
  478. }
  479. &::after {
  480. content: '';
  481. position: absolute;
  482. right: -20px;
  483. top: -20px;
  484. width: 120px;
  485. height: 120px;
  486. border-radius: 50%;
  487. background: rgba(255, 255, 255, 0.1);
  488. z-index: 0;
  489. }
  490. }
  491. .primary-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
  492. .success-card { background: linear-gradient(135deg, #20E2D7 0%, #F9FEA5 100%); .kpi-label, .kpi-value {color: #333} .kpi-icon {color: #333} .kpi-icon-wrapper {background: rgba(0,0,0,0.05)}}
  493. .warning-card { background: linear-gradient(135deg, #f6d365 0%, #fda085 100%); }
  494. .danger-card { background: linear-gradient(135deg, #ff0844 0%, #ffb199 100%); }
  495. /* Board Cards */
  496. .border-card {
  497. box-shadow: 0 4px 12px rgba(0,0,0,0.03);
  498. :deep(.el-card__header) {
  499. border-bottom: 1px solid #f0f2f5;
  500. padding: 16px 20px;
  501. display: flex;
  502. justify-content: space-between;
  503. align-items: center;
  504. }
  505. :deep(.el-card__body) {
  506. padding: 20px;
  507. }
  508. }
  509. .board-card-title {
  510. display: flex;
  511. align-items: center;
  512. font-size: 16px;
  513. font-weight: 600;
  514. color: #303133;
  515. .dot {
  516. width: 8px;
  517. height: 8px;
  518. border-radius: 50%;
  519. margin-right: 10px;
  520. &.primary-dot { background-color: #409EFF; }
  521. &.warning-dot { background-color: #E6A23C; }
  522. &.success-dot { background-color: #67C23A; }
  523. &.danger-dot { background-color: #F56C6C; }
  524. }
  525. }
  526. .chart-large {
  527. height: 340px;
  528. width: 100%;
  529. }
  530. /* Tables styling */
  531. .rank-badge {
  532. display: inline-block;
  533. width: 24px;
  534. height: 24px;
  535. line-height: 24px;
  536. text-align: center;
  537. border-radius: 50%;
  538. font-weight: bold;
  539. font-size: 12px;
  540. background-color: #f4f4f5;
  541. color: #909399;
  542. &.rank-1 { background-color: #f56c6c; color: white; }
  543. &.rank-2 { background-color: #ff9800; color: white; }
  544. &.rank-3 { background-color: #e6a23c; color: white; }
  545. }
  546. .product-name {
  547. font-weight: 500;
  548. color: #303133;
  549. }
  550. .mini-kpi-wrap {
  551. display: flex;
  552. align-items: center;
  553. gap: 16px;
  554. }
  555. .mini-v-divider {
  556. width: 1px;
  557. height: 24px;
  558. background-color: #ebeef5;
  559. }
  560. .mini-kpi {
  561. display: flex;
  562. align-items: center;
  563. gap: 8px;
  564. .m-label {
  565. font-size: 13px;
  566. color: #909399;
  567. }
  568. .m-value {
  569. font-size: 18px;
  570. font-weight: 700;
  571. font-family: 'Rubik', Arial, sans-serif;
  572. }
  573. .text-danger { color: #f56c6c; }
  574. .text-success { color: #67c23a; }
  575. }
  576. </style>