index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. <template>
  2. <div class="dashboard">
  3. <div class="welcome-section">
  4. <div class="welcome-bg-decor">
  5. <div class="decor-ring ring-1"></div>
  6. <div class="decor-ring ring-2"></div>
  7. <div class="decor-ring ring-3"></div>
  8. <div class="decor-grid"></div>
  9. <div class="decor-particle p1"></div>
  10. <div class="decor-particle p2"></div>
  11. <div class="decor-particle p3"></div>
  12. </div>
  13. <div class="welcome-content">
  14. <div class="welcome-text">
  15. <div class="welcome-tag">华晟ERP订单管理系统</div>
  16. <h2>{{ greeting }},{{ userStore.nickname || userStore.name }}</h2>
  17. <p class="welcome-sub">
  18. <span class="date-icon">📅</span>
  19. {{ currentDate }}
  20. </p>
  21. </div>
  22. <div class="welcome-actions">
  23. <el-button plain round icon="Refresh" @click="loadStatistics" :loading="loading">刷新数据</el-button>
  24. </div>
  25. </div>
  26. </div>
  27. <div class="stats-grid">
  28. <div
  29. v-for="(stat, idx) in statistics"
  30. :key="stat.label"
  31. class="stat-card"
  32. :style="{ '--card-color': stat.color, '--card-bg': stat.bgColor, '--delay': idx * 0.08 + 's' }"
  33. @click="router.push('/order')"
  34. >
  35. <div class="stat-card-glow" :style="{ background: stat.color }"></div>
  36. <div class="stat-card-inner">
  37. <div class="stat-top">
  38. <div class="stat-icon-wrap">
  39. <svg-icon :icon-class="stat.icon" />
  40. </div>
  41. <div class="stat-badge" :style="{ background: stat.bgColor, color: stat.color }">
  42. {{ stat.label }}
  43. </div>
  44. </div>
  45. <div class="stat-body">
  46. <div class="stat-value">{{ stat.count }}</div>
  47. <div class="stat-unit">笔</div>
  48. </div>
  49. <div class="stat-footer">
  50. <span class="stat-hint">点击查看详情</span>
  51. <span class="stat-arrow">→</span>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. <div class="chart-section">
  57. <div class="chart-header">
  58. <div class="chart-header-left">
  59. <div class="chart-title">订单分布</div>
  60. <div class="chart-subtitle">各状态订单数量占比</div>
  61. </div>
  62. <div class="chart-legend">
  63. <div v-for="stat in statistics" :key="stat.label" class="legend-item">
  64. <span class="legend-dot" :style="{ background: stat.color }"></span>
  65. <span class="legend-label">{{ stat.label }}</span>
  66. </div>
  67. </div>
  68. </div>
  69. <div class="chart-body">
  70. <div v-for="(stat, idx) in statistics" :key="stat.label" class="chart-row">
  71. <div class="chart-row-label">
  72. <span class="chart-row-dot" :style="{ background: stat.color }"></span>
  73. {{ stat.label }}
  74. </div>
  75. <div class="chart-row-bar-wrap">
  76. <div class="chart-row-bar-bg">
  77. <div
  78. class="chart-row-bar-fill"
  79. :style="{
  80. width: barWidth(stat.count),
  81. background: `linear-gradient(90deg, ${stat.color}, ${stat.color}dd)`,
  82. '--delay': idx * 0.1 + 's'
  83. }"
  84. ></div>
  85. </div>
  86. </div>
  87. <div class="chart-row-value">{{ stat.count }}<span class="chart-row-unit">笔</span></div>
  88. <div class="chart-row-pct">{{ barPercent(stat.count) }}</div>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. </template>
  94. <script setup name="Index" lang="ts">
  95. import { countOrder } from '@/api/erp/order';
  96. import { useUserStore } from '@/store/modules/user';
  97. import { useRouter } from 'vue-router';
  98. /** @Author: Antigravity */
  99. const userStore = useUserStore();
  100. const router = useRouter();
  101. const loading = ref(false);
  102. const statistics = ref([
  103. { label: '全部订单', count: 0, color: '#4F6EF7', bgColor: '#EEF1FE', icon: 'order' },
  104. { label: '待审核', count: 0, color: '#F7A83E', bgColor: '#FEF6E6', icon: 'wait' },
  105. { label: '生产中', count: 0, color: '#5FDBA7', bgColor: '#E8FBF2', icon: 'success' },
  106. { label: '已完成', count: 0, color: '#409EFF', bgColor: '#ECF5FF', icon: 'finish' }
  107. ]);
  108. const currentDate = computed(() => {
  109. const d = new Date();
  110. const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
  111. return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 星期${weekdays[d.getDay()]}`;
  112. });
  113. const greeting = computed(() => {
  114. const hour = new Date().getHours();
  115. if (hour < 6) return '夜深了';
  116. if (hour < 9) return '早上好';
  117. if (hour < 12) return '上午好';
  118. if (hour < 14) return '中午好';
  119. if (hour < 18) return '下午好';
  120. return '晚上好';
  121. });
  122. function barWidth(count: number) {
  123. const max = Math.max(...statistics.value.map(s => s.count), 1);
  124. return (count / max * 100).toFixed(1) + '%';
  125. }
  126. function barPercent(count: number) {
  127. const total = statistics.value.reduce((s, v) => s + v.count, 0);
  128. if (!total) return '0%';
  129. return (count / total * 100).toFixed(1) + '%';
  130. }
  131. function loadStatistics() {
  132. loading.value = true;
  133. countOrder().then(res => {
  134. const data = res.data || {};
  135. const total = Object.values(data).reduce((sum: number, v: any) => sum + (Number(v) || 0), 0);
  136. statistics.value[0].count = total;
  137. statistics.value[1].count = data[0] || 0;
  138. statistics.value[2].count = data[3] || 0;
  139. statistics.value[3].count = data[4] || 0;
  140. }).finally(() => {
  141. loading.value = false;
  142. });
  143. }
  144. onMounted(() => {
  145. loadStatistics();
  146. });
  147. </script>
  148. <style lang="scss" scoped>
  149. .dashboard {
  150. padding: 24px;
  151. min-height: calc(100vh - 100px);
  152. background: #f5f7fa;
  153. }
  154. .welcome-section {
  155. background: linear-gradient(135deg, #4F6EF7 0%, #7C5CFC 50%, #A855F7 100%);
  156. border-radius: 16px;
  157. padding: 36px 44px;
  158. margin-bottom: 28px;
  159. position: relative;
  160. overflow: hidden;
  161. .welcome-bg-decor {
  162. position: absolute;
  163. inset: 0;
  164. pointer-events: none;
  165. .decor-ring {
  166. position: absolute;
  167. border-radius: 50%;
  168. border: 1px solid rgba(255, 255, 255, 0.06);
  169. &.ring-1 {
  170. top: -30%;
  171. right: -3%;
  172. width: 450px;
  173. height: 450px;
  174. }
  175. &.ring-2 {
  176. bottom: -20%;
  177. right: 8%;
  178. width: 300px;
  179. height: 300px;
  180. border-color: rgba(255, 255, 255, 0.04);
  181. }
  182. &.ring-3 {
  183. top: 15%;
  184. left: 50%;
  185. width: 160px;
  186. height: 160px;
  187. border-color: rgba(255, 255, 255, 0.03);
  188. }
  189. }
  190. .decor-grid {
  191. position: absolute;
  192. top: 24px;
  193. left: 32px;
  194. width: 72px;
  195. height: 72px;
  196. background-image:
  197. linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px),
  198. linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px);
  199. background-size: 12px 12px;
  200. }
  201. .decor-particle {
  202. position: absolute;
  203. border-radius: 50%;
  204. background: rgba(255, 255, 255, 0.08);
  205. &.p1 {
  206. top: 25%;
  207. left: 38%;
  208. width: 6px;
  209. height: 6px;
  210. }
  211. &.p2 {
  212. top: 55%;
  213. left: 42%;
  214. width: 4px;
  215. height: 4px;
  216. background: rgba(255, 255, 255, 0.05);
  217. }
  218. &.p3 {
  219. top: 35%;
  220. left: 60%;
  221. width: 5px;
  222. height: 5px;
  223. }
  224. }
  225. }
  226. .welcome-content {
  227. display: flex;
  228. justify-content: space-between;
  229. align-items: center;
  230. position: relative;
  231. z-index: 1;
  232. }
  233. .welcome-text {
  234. .welcome-tag {
  235. display: inline-block;
  236. font-size: 12px;
  237. color: rgba(255, 255, 255, 0.7);
  238. background: rgba(255, 255, 255, 0.1);
  239. padding: 3px 12px;
  240. border-radius: 20px;
  241. margin-bottom: 12px;
  242. letter-spacing: 1px;
  243. backdrop-filter: blur(4px);
  244. border: 1px solid rgba(255, 255, 255, 0.08);
  245. }
  246. h2 {
  247. margin: 0 0 10px 0;
  248. font-size: 26px;
  249. font-weight: 700;
  250. color: #fff;
  251. letter-spacing: 0.5px;
  252. }
  253. .welcome-sub {
  254. margin: 0;
  255. font-size: 14px;
  256. color: rgba(255, 255, 255, 0.7);
  257. display: flex;
  258. align-items: center;
  259. gap: 6px;
  260. .date-icon {
  261. font-size: 14px;
  262. }
  263. }
  264. }
  265. .welcome-actions {
  266. .el-button {
  267. background: rgba(255, 255, 255, 0.12);
  268. border-color: rgba(255, 255, 255, 0.2);
  269. color: #fff;
  270. font-size: 13px;
  271. padding: 9px 22px;
  272. backdrop-filter: blur(6px);
  273. border-radius: 20px;
  274. &:hover {
  275. background: rgba(255, 255, 255, 0.22);
  276. border-color: rgba(255, 255, 255, 0.3);
  277. }
  278. }
  279. }
  280. }
  281. .stats-grid {
  282. display: grid;
  283. grid-template-columns: repeat(4, 1fr);
  284. gap: 20px;
  285. margin-bottom: 28px;
  286. .stat-card {
  287. background: #fff;
  288. border-radius: 16px;
  289. cursor: pointer;
  290. transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  291. border: 1px solid #edf0f5;
  292. position: relative;
  293. overflow: hidden;
  294. animation: cardFadeIn 0.6s ease both;
  295. animation-delay: var(--delay);
  296. &:hover {
  297. transform: translateY(-8px);
  298. box-shadow: 0 20px 48px -8px rgba(0, 0, 0, 0.1);
  299. border-color: var(--card-color);
  300. .stat-card-glow {
  301. opacity: 1;
  302. transform: scale(1);
  303. }
  304. .stat-arrow {
  305. transform: translateX(4px);
  306. opacity: 1;
  307. }
  308. }
  309. .stat-card-glow {
  310. position: absolute;
  311. top: -80px;
  312. right: -80px;
  313. width: 200px;
  314. height: 200px;
  315. border-radius: 50%;
  316. opacity: 0;
  317. transform: scale(0.3);
  318. transition: all 0.5s ease;
  319. filter: blur(50px);
  320. }
  321. .stat-card-inner {
  322. position: relative;
  323. z-index: 1;
  324. padding: 24px 24px 18px;
  325. }
  326. .stat-top {
  327. display: flex;
  328. justify-content: space-between;
  329. align-items: center;
  330. margin-bottom: 20px;
  331. }
  332. .stat-icon-wrap {
  333. width: 46px;
  334. height: 46px;
  335. border-radius: 14px;
  336. background: var(--card-bg);
  337. display: flex;
  338. align-items: center;
  339. justify-content: center;
  340. font-size: 24px;
  341. color: var(--card-color);
  342. transition: all 0.4s ease;
  343. }
  344. &:hover .stat-icon-wrap {
  345. transform: scale(1.08) rotate(-6deg);
  346. box-shadow: 0 8px 24px rgba(79, 110, 247, 0.15);
  347. }
  348. .stat-badge {
  349. font-size: 12px;
  350. font-weight: 500;
  351. padding: 4px 14px;
  352. border-radius: 20px;
  353. letter-spacing: 0.5px;
  354. }
  355. .stat-body {
  356. display: flex;
  357. align-items: baseline;
  358. gap: 4px;
  359. margin-bottom: 16px;
  360. .stat-value {
  361. font-size: 32px;
  362. font-weight: 800;
  363. color: #1a1a2e;
  364. line-height: 1;
  365. font-variant-numeric: tabular-nums;
  366. letter-spacing: -0.5px;
  367. }
  368. .stat-unit {
  369. font-size: 14px;
  370. color: #bfc0c4;
  371. font-weight: 400;
  372. }
  373. }
  374. .stat-footer {
  375. display: flex;
  376. justify-content: space-between;
  377. align-items: center;
  378. padding-top: 14px;
  379. border-top: 1px solid #f0f2f5;
  380. .stat-hint {
  381. font-size: 12px;
  382. color: #c0c4cc;
  383. }
  384. .stat-arrow {
  385. font-size: 14px;
  386. color: var(--card-color);
  387. transition: all 0.3s ease;
  388. opacity: 0.5;
  389. }
  390. }
  391. }
  392. }
  393. .chart-section {
  394. background: #fff;
  395. border-radius: 16px;
  396. padding: 28px 32px;
  397. border: 1px solid #edf0f5;
  398. .chart-header {
  399. display: flex;
  400. justify-content: space-between;
  401. align-items: center;
  402. margin-bottom: 28px;
  403. .chart-header-left {
  404. .chart-title {
  405. font-size: 16px;
  406. font-weight: 600;
  407. color: #1a1a2e;
  408. margin-bottom: 4px;
  409. }
  410. .chart-subtitle {
  411. font-size: 13px;
  412. color: #bfc0c4;
  413. }
  414. }
  415. .chart-legend {
  416. display: flex;
  417. gap: 20px;
  418. .legend-item {
  419. display: flex;
  420. align-items: center;
  421. gap: 6px;
  422. .legend-dot {
  423. width: 8px;
  424. height: 8px;
  425. border-radius: 50%;
  426. }
  427. .legend-label {
  428. font-size: 12px;
  429. color: #606266;
  430. }
  431. }
  432. }
  433. }
  434. .chart-body {
  435. display: flex;
  436. flex-direction: column;
  437. gap: 18px;
  438. .chart-row {
  439. display: grid;
  440. grid-template-columns: 100px 1fr 70px 60px;
  441. align-items: center;
  442. gap: 16px;
  443. .chart-row-label {
  444. display: flex;
  445. align-items: center;
  446. gap: 8px;
  447. font-size: 13px;
  448. color: #606266;
  449. font-weight: 500;
  450. .chart-row-dot {
  451. width: 6px;
  452. height: 6px;
  453. border-radius: 50%;
  454. flex-shrink: 0;
  455. }
  456. }
  457. .chart-row-bar-wrap {
  458. .chart-row-bar-bg {
  459. height: 8px;
  460. background: #f0f2f5;
  461. border-radius: 4px;
  462. overflow: hidden;
  463. .chart-row-bar-fill {
  464. height: 100%;
  465. border-radius: 4px;
  466. animation: barGrow 1s ease both;
  467. animation-delay: var(--delay);
  468. }
  469. }
  470. }
  471. .chart-row-value {
  472. font-size: 14px;
  473. font-weight: 600;
  474. color: #1a1a2e;
  475. text-align: right;
  476. .chart-row-unit {
  477. font-size: 11px;
  478. color: #bfc0c4;
  479. font-weight: 400;
  480. margin-left: 2px;
  481. }
  482. }
  483. .chart-row-pct {
  484. font-size: 12px;
  485. color: #909399;
  486. text-align: right;
  487. }
  488. }
  489. }
  490. }
  491. @keyframes cardFadeIn {
  492. from {
  493. opacity: 0;
  494. transform: translateY(24px);
  495. }
  496. to {
  497. opacity: 1;
  498. transform: translateY(0);
  499. }
  500. }
  501. @keyframes barGrow {
  502. from {
  503. width: 0 !important;
  504. }
  505. }
  506. @media (max-width: 1200px) {
  507. .stats-grid {
  508. grid-template-columns: repeat(2, 1fr);
  509. }
  510. }
  511. @media (max-width: 768px) {
  512. .stats-grid {
  513. grid-template-columns: repeat(2, 1fr);
  514. gap: 12px;
  515. }
  516. .welcome-section {
  517. padding: 24px;
  518. .welcome-content {
  519. flex-direction: column;
  520. align-items: flex-start;
  521. gap: 16px;
  522. }
  523. }
  524. .chart-section {
  525. padding: 20px;
  526. .chart-header {
  527. flex-direction: column;
  528. align-items: flex-start;
  529. gap: 12px;
  530. .chart-legend {
  531. flex-wrap: wrap;
  532. gap: 12px;
  533. }
  534. }
  535. .chart-body .chart-row {
  536. grid-template-columns: 80px 1fr 50px 50px;
  537. gap: 10px;
  538. }
  539. }
  540. }
  541. </style>