index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <template>
  2. <view class="page">
  3. <!-- 顶部导航 - 与内容融合 -->
  4. <view class="header">
  5. <view class="header-bg"></view>
  6. <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
  7. <view class="header-content">
  8. <view class="greeting">
  9. <text class="greeting-time">{{ getTimeGreeting() }}</text>
  10. <text class="greeting-name">运营管理员</text>
  11. </view>
  12. <view class="avatar" @click="navigateTo('/pages/my/my')">
  13. <view class="avatar-icon"></view>
  14. </view>
  15. </view>
  16. </view>
  17. <!-- 统计卡片 -->
  18. <view class="stats-section">
  19. <view class="stats-card main">
  20. <view class="stats-item" @click="navigateTo('/pages/orders/list')">
  21. <text class="stats-value">{{ stats.todayStats?.orderCount || 0 }}</text>
  22. <text class="stats-label">今日订单</text>
  23. </view>
  24. <view class="stats-divider"></view>
  25. <view class="stats-item">
  26. <text class="stats-value accent">¥{{ formatMoney(stats.todayStats?.orderAmount || 0) }}</text>
  27. <text class="stats-label">今日销售额</text>
  28. </view>
  29. </view>
  30. <view class="stats-row">
  31. <view class="stats-card mini success" @click="navigateTo('/pages/device/list')">
  32. <view class="stats-dot green"></view>
  33. <view class="stats-info">
  34. <text class="stats-value">{{ stats.todayStats?.deviceOnline || 0 }}</text>
  35. <text class="stats-label">在线设备</text>
  36. </view>
  37. </view>
  38. <view class="stats-card mini warning">
  39. <view class="stats-dot amber"></view>
  40. <view class="stats-info">
  41. <text class="stats-value">{{ stats.todayStats?.deviceOffline || 0 }}</text>
  42. <text class="stats-label">离线设备</text>
  43. </view>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 快捷功能 -->
  48. <view class="section">
  49. <view class="section-header">
  50. <text class="section-title">快捷功能</text>
  51. </view>
  52. <view class="quick-grid">
  53. <view class="quick-item" @click="navigateTo('/pages/orders/list')">
  54. <view class="quick-icon green">
  55. <view class="icon-order"></view>
  56. </view>
  57. <text class="quick-name">订单管理</text>
  58. </view>
  59. <view class="quick-item" @click="navigateTo('/pages/device/list')">
  60. <view class="quick-icon cyan">
  61. <view class="icon-device"></view>
  62. </view>
  63. <text class="quick-name">设备管理</text>
  64. </view>
  65. <view class="quick-item" @click="navigateTo('/pages/shop/list')">
  66. <view class="quick-icon amber">
  67. <view class="icon-shop"></view>
  68. </view>
  69. <text class="quick-name">门店管理</text>
  70. </view>
  71. <view class="quick-item" @click="navigateTo('/pages/products/list')">
  72. <view class="quick-icon purple">
  73. <view class="icon-product"></view>
  74. </view>
  75. <text class="quick-name">商品管理</text>
  76. </view>
  77. <view class="quick-item" @click="navigateTo('/pages/inventory/query')">
  78. <view class="quick-icon blue">
  79. <view class="icon-chart"></view>
  80. </view>
  81. <text class="quick-name">库存查询</text>
  82. </view>
  83. <view class="quick-item" @click="navigateTo('/pages/checkin/index')">
  84. <view class="quick-icon rose">
  85. <view class="icon-checkin"></view>
  86. </view>
  87. <text class="quick-name">签到打卡</text>
  88. </view>
  89. </view>
  90. </view>
  91. <!-- 订单趋势 -->
  92. <view class="section">
  93. <view class="section-header">
  94. <text class="section-title">订单趋势</text>
  95. <text class="section-tag">近7天</text>
  96. </view>
  97. <view class="chart-card">
  98. <view class="chart-container">
  99. <view
  100. class="chart-bar-wrapper"
  101. v-for="(item, index) in stats.orderTrend || []"
  102. :key="index"
  103. >
  104. <text class="chart-value">{{ item.count }}</text>
  105. <view class="chart-bar-bg">
  106. <view
  107. class="chart-bar"
  108. :style="{ height: getBarHeight(item.count) + '%' }"
  109. ></view>
  110. </view>
  111. <text class="chart-label">{{ item.date.slice(-2) }}</text>
  112. </view>
  113. </view>
  114. </view>
  115. </view>
  116. <!-- 热销商品 -->
  117. <view class="section" v-if="stats.hotProducts && stats.hotProducts.length > 0">
  118. <view class="section-header">
  119. <text class="section-title">热销商品</text>
  120. <text class="section-tag hot">TOP 5</text>
  121. </view>
  122. <view class="hot-list">
  123. <view
  124. class="hot-item"
  125. v-for="(item, index) in stats.hotProducts || []"
  126. :key="item.id"
  127. >
  128. <view class="hot-rank" :class="{ top: Number(index) < 3 }">
  129. <text>{{ Number(index) + 1 }}</text>
  130. </view>
  131. <view class="hot-info">
  132. <text class="hot-name">{{ item.name }}</text>
  133. <text class="hot-sales">销量 {{ item.sales }}</text>
  134. </view>
  135. <view class="hot-progress">
  136. <view class="hot-progress-bar" :style="{ width: getProgress(item.sales) + '%' }"></view>
  137. </view>
  138. </view>
  139. </view>
  140. </view>
  141. <!-- 自定义TabBar -->
  142. <CustomTabBar />
  143. </view>
  144. </template>
  145. <script setup lang="ts">
  146. import { ref, onMounted } from 'vue';
  147. import { getDashboardData } from '@/api/dashboard';
  148. import { formatMoney as formatMoneyUtil } from '@/utils/common';
  149. import CustomTabBar from '@/components/CustomTabBar.vue';
  150. // 获取系统信息用于计算状态栏高度
  151. const systemInfo = uni.getSystemInfoSync();
  152. const statusBarHeight = ref(systemInfo.statusBarHeight || 20);
  153. const stats = ref<any>({});
  154. const maxCount = ref(0);
  155. const maxSales = ref(0);
  156. const formatMoney = (amount: number) => formatMoneyUtil(amount);
  157. const getTimeGreeting = () => {
  158. const hour = new Date().getHours();
  159. if (hour < 6) return '凌晨好';
  160. if (hour < 12) return '上午好';
  161. if (hour < 18) return '下午好';
  162. return '晚上好';
  163. };
  164. const getBarHeight = (count: number) => {
  165. if (maxCount.value === 0) return 0;
  166. return Math.max(15, (count / maxCount.value) * 100);
  167. };
  168. const getProgress = (sales: number) => {
  169. if (maxSales.value === 0) return 0;
  170. return Math.max(10, (sales / maxSales.value) * 100);
  171. };
  172. const navigateTo = (url: string) => {
  173. uni.navigateTo({ url });
  174. };
  175. const loadData = async () => {
  176. try {
  177. const data = await getDashboardData();
  178. stats.value = data;
  179. if (data.orderTrend && data.orderTrend.length > 0) {
  180. maxCount.value = Math.max(...data.orderTrend.map((item: any) => item.count));
  181. }
  182. if (data.hotProducts && data.hotProducts.length > 0) {
  183. maxSales.value = Math.max(...data.hotProducts.map((item: any) => item.sales));
  184. }
  185. } catch (error) {
  186. console.error('获取仪表盘数据失败', error);
  187. }
  188. };
  189. onMounted(() => {
  190. loadData();
  191. });
  192. </script>
  193. <style lang="scss" scoped>
  194. .page {
  195. min-height: 100vh;
  196. background: #f8fafc;
  197. padding-bottom: 200rpx;
  198. }
  199. /* 顶部导航 */
  200. .header {
  201. position: relative;
  202. background: #ffffff;
  203. }
  204. .header-bg {
  205. position: absolute;
  206. top: 0;
  207. left: 0;
  208. right: 0;
  209. height: 280rpx;
  210. background: #ecfdf5;
  211. border-radius: 0 0 40rpx 40rpx;
  212. }
  213. .status-bar {
  214. width: 100%;
  215. position: relative;
  216. z-index: 1;
  217. }
  218. .header-content {
  219. position: relative;
  220. display: flex;
  221. justify-content: space-between;
  222. align-items: center;
  223. padding: 16rpx 24rpx 24rpx;
  224. }
  225. .greeting {
  226. .greeting-time {
  227. display: block;
  228. font-size: 26rpx;
  229. color: #64748b;
  230. margin-bottom: 4rpx;
  231. }
  232. .greeting-name {
  233. font-size: 40rpx;
  234. font-weight: 700;
  235. color: #1e293b;
  236. }
  237. }
  238. .avatar {
  239. width: 80rpx;
  240. height: 80rpx;
  241. border-radius: 50%;
  242. background: #ecfdf5;
  243. display: flex;
  244. align-items: center;
  245. justify-content: center;
  246. border: 2rpx solid #10b981;
  247. position: relative;
  248. .avatar-icon {
  249. width: 36rpx;
  250. height: 36rpx;
  251. background: #10b981;
  252. border-radius: 50%;
  253. position: relative;
  254. &::before {
  255. content: '';
  256. position: absolute;
  257. top: -16rpx;
  258. left: 50%;
  259. transform: translateX(-50%);
  260. width: 22rpx;
  261. height: 18rpx;
  262. background: #10b981;
  263. border-radius: 18rpx 18rpx 0 0;
  264. }
  265. }
  266. }
  267. /* 统计卡片 */
  268. .stats-section {
  269. padding: 16rpx 24rpx;
  270. }
  271. .stats-card {
  272. background: #ffffff;
  273. border: 1rpx solid #e2e8f0;
  274. border-radius: 20rpx;
  275. &.main {
  276. display: flex;
  277. padding: 32rpx 0;
  278. margin-bottom: 16rpx;
  279. .stats-item {
  280. flex: 1;
  281. display: flex;
  282. flex-direction: column;
  283. align-items: center;
  284. justify-content: center;
  285. }
  286. .stats-value {
  287. display: flex;
  288. align-items: center;
  289. justify-content: center;
  290. font-size: 48rpx;
  291. font-weight: 700;
  292. color: #1e293b;
  293. margin-bottom: 8rpx;
  294. &.accent {
  295. color: #f97316;
  296. }
  297. }
  298. .stats-label {
  299. display: flex;
  300. align-items: center;
  301. justify-content: center;
  302. font-size: 24rpx;
  303. color: #64748b;
  304. }
  305. .stats-divider {
  306. width: 1rpx;
  307. background: #e2e8f0;
  308. }
  309. }
  310. &.mini {
  311. flex: 1;
  312. display: flex;
  313. align-items: center;
  314. justify-content: center;
  315. padding: 24rpx;
  316. &:not(:last-child) {
  317. margin-right: 12rpx;
  318. }
  319. .stats-dot {
  320. width: 12rpx;
  321. height: 12rpx;
  322. border-radius: 50%;
  323. margin-right: 16rpx;
  324. flex-shrink: 0;
  325. &.green { background: #10b981; }
  326. &.amber { background: #f97316; }
  327. }
  328. .stats-info {
  329. display: flex;
  330. flex-direction: column;
  331. align-items: center;
  332. justify-content: center;
  333. .stats-value {
  334. display: flex;
  335. align-items: center;
  336. justify-content: center;
  337. font-size: 32rpx;
  338. font-weight: 700;
  339. color: #1e293b;
  340. }
  341. .stats-label {
  342. display: flex;
  343. align-items: center;
  344. justify-content: center;
  345. font-size: 22rpx;
  346. color: #64748b;
  347. }
  348. }
  349. }
  350. }
  351. .stats-row {
  352. display: flex;
  353. }
  354. /* 区块 */
  355. .section {
  356. margin: 16rpx 24rpx;
  357. }
  358. .section-header {
  359. display: flex;
  360. justify-content: space-between;
  361. align-items: center;
  362. margin-bottom: 16rpx;
  363. }
  364. .section-title {
  365. font-size: 32rpx;
  366. font-weight: 600;
  367. color: #1e293b;
  368. }
  369. .section-tag {
  370. font-size: 22rpx;
  371. color: #10b981;
  372. background: #ecfdf5;
  373. padding: 8rpx 16rpx;
  374. border-radius: 8rpx;
  375. &.hot {
  376. color: #f97316;
  377. background: #fff7ed;
  378. }
  379. }
  380. /* 快捷功能 */
  381. .quick-grid {
  382. display: flex;
  383. justify-content: space-between;
  384. background: #ffffff;
  385. border: 1rpx solid #e2e8f0;
  386. border-radius: 20rpx;
  387. padding: 24rpx 16rpx;
  388. }
  389. .quick-item {
  390. display: flex;
  391. flex-direction: column;
  392. align-items: center;
  393. .quick-icon {
  394. width: 88rpx;
  395. height: 88rpx;
  396. border-radius: 20rpx;
  397. display: flex;
  398. align-items: center;
  399. justify-content: center;
  400. margin-bottom: 12rpx;
  401. transition: transform 0.15s;
  402. &:active {
  403. transform: scale(0.92);
  404. }
  405. &.green { background: #ecfdf5; }
  406. &.cyan { background: #ecfeff; }
  407. &.amber { background: #fff7ed; }
  408. &.purple { background: #faf5ff; }
  409. &.blue { background: #f0f9ff; }
  410. &.rose { background: #fff1f2; }
  411. /* 图标 - 纯CSS几何图形 */
  412. .icon-order {
  413. width: 32rpx;
  414. height: 40rpx;
  415. border: 3rpx solid #10b981;
  416. border-radius: 4rpx;
  417. position: relative;
  418. &::before, &::after {
  419. content: '';
  420. position: absolute;
  421. left: 6rpx;
  422. width: 14rpx;
  423. height: 3rpx;
  424. background: #10b981;
  425. border-radius: 2rpx;
  426. }
  427. &::before { top: 12rpx; }
  428. &::after { top: 20rpx; width: 10rpx; }
  429. }
  430. .icon-device {
  431. width: 24rpx;
  432. height: 40rpx;
  433. border: 3rpx solid #0ea5e9;
  434. border-radius: 6rpx;
  435. position: relative;
  436. &::after {
  437. content: '';
  438. position: absolute;
  439. bottom: 6rpx;
  440. left: 50%;
  441. transform: translateX(-50%);
  442. width: 8rpx;
  443. height: 4rpx;
  444. background: #0ea5e9;
  445. border-radius: 2rpx;
  446. }
  447. }
  448. .icon-shop {
  449. position: relative;
  450. width: 36rpx;
  451. height: 36rpx;
  452. &::before {
  453. content: '';
  454. position: absolute;
  455. top: 0;
  456. left: 50%;
  457. transform: translateX(-50%);
  458. width: 0;
  459. height: 0;
  460. border-left: 20rpx solid transparent;
  461. border-right: 20rpx solid transparent;
  462. border-bottom: 14rpx solid #f97316;
  463. }
  464. &::after {
  465. content: '';
  466. position: absolute;
  467. bottom: 0;
  468. left: 50%;
  469. transform: translateX(-50%);
  470. width: 32rpx;
  471. height: 18rpx;
  472. background: #f97316;
  473. border-radius: 0 0 4rpx 4rpx;
  474. }
  475. }
  476. .icon-product {
  477. width: 32rpx;
  478. height: 32rpx;
  479. position: relative;
  480. &::before {
  481. content: '';
  482. position: absolute;
  483. top: 0;
  484. left: 50%;
  485. transform: translateX(-50%);
  486. width: 0;
  487. height: 0;
  488. border-left: 10rpx solid transparent;
  489. border-right: 10rpx solid transparent;
  490. border-bottom: 12rpx solid #a855f7;
  491. }
  492. &::after {
  493. content: '';
  494. position: absolute;
  495. bottom: 0;
  496. left: 50%;
  497. transform: translateX(-50%);
  498. width: 28rpx;
  499. height: 16rpx;
  500. background: #a855f7;
  501. border-radius: 4rpx;
  502. }
  503. }
  504. .icon-chart {
  505. display: flex;
  506. align-items: flex-end;
  507. justify-content: space-between;
  508. width: 32rpx;
  509. height: 28rpx;
  510. &::before, &::after, & {
  511. content: '';
  512. width: 8rpx;
  513. background: #0ea5e9;
  514. border-radius: 4rpx;
  515. }
  516. &::before { height: 12rpx; }
  517. & { height: 24rpx; }
  518. &::after { height: 16rpx; }
  519. }
  520. .icon-checkin {
  521. width: 28rpx;
  522. height: 28rpx;
  523. border: 3rpx solid #f43f5e;
  524. border-radius: 50%;
  525. position: relative;
  526. &::before {
  527. content: '';
  528. position: absolute;
  529. top: 50%;
  530. left: 50%;
  531. transform: translate(-50%, -50%);
  532. width: 8rpx;
  533. height: 8rpx;
  534. background: #f43f5e;
  535. border-radius: 50%;
  536. }
  537. &::after {
  538. content: '';
  539. position: absolute;
  540. top: -6rpx;
  541. right: -6rpx;
  542. width: 12rpx;
  543. height: 12rpx;
  544. background: #f43f5e;
  545. border-radius: 50%;
  546. }
  547. }
  548. }
  549. .quick-name {
  550. font-size: 24rpx;
  551. color: #475569;
  552. }
  553. }
  554. /* 图表卡片 */
  555. .chart-card {
  556. background: #ffffff;
  557. border: 1rpx solid #e2e8f0;
  558. border-radius: 20rpx;
  559. padding: 24rpx;
  560. }
  561. .chart-container {
  562. display: flex;
  563. justify-content: space-between;
  564. align-items: flex-end;
  565. height: 240rpx;
  566. padding-top: 32rpx;
  567. }
  568. .chart-bar-wrapper {
  569. flex: 1;
  570. display: flex;
  571. flex-direction: column;
  572. align-items: center;
  573. height: 100%;
  574. .chart-value {
  575. font-size: 22rpx;
  576. font-weight: 600;
  577. color: #475569;
  578. margin-bottom: 8rpx;
  579. }
  580. .chart-bar-bg {
  581. flex: 1;
  582. width: 28rpx;
  583. display: flex;
  584. align-items: flex-end;
  585. background: #f1f5f9;
  586. border-radius: 6rpx;
  587. .chart-bar {
  588. width: 100%;
  589. background: #10b981;
  590. border-radius: 6rpx;
  591. min-height: 20rpx;
  592. }
  593. }
  594. .chart-label {
  595. font-size: 20rpx;
  596. color: #94a3b8;
  597. margin-top: 10rpx;
  598. }
  599. }
  600. /* 热销列表 */
  601. .hot-list {
  602. background: #ffffff;
  603. border: 1rpx solid #e2e8f0;
  604. border-radius: 20rpx;
  605. padding: 8rpx 20rpx;
  606. }
  607. .hot-item {
  608. display: flex;
  609. align-items: center;
  610. padding: 20rpx 8rpx;
  611. border-bottom: 1rpx solid #f1f5f9;
  612. &:last-child {
  613. border-bottom: none;
  614. }
  615. .hot-rank {
  616. width: 44rpx;
  617. height: 44rpx;
  618. border-radius: 12rpx;
  619. background: #f1f5f9;
  620. display: flex;
  621. align-items: center;
  622. justify-content: center;
  623. margin-right: 16rpx;
  624. font-size: 24rpx;
  625. font-weight: 600;
  626. color: #94a3b8;
  627. &.top {
  628. background: #10b981;
  629. color: #fff;
  630. }
  631. }
  632. .hot-info {
  633. flex: 1;
  634. .hot-name {
  635. display: block;
  636. font-size: 28rpx;
  637. color: #1e293b;
  638. margin-bottom: 4rpx;
  639. font-weight: 500;
  640. }
  641. .hot-sales {
  642. font-size: 24rpx;
  643. color: #94a3b8;
  644. }
  645. }
  646. .hot-progress {
  647. width: 80rpx;
  648. height: 8rpx;
  649. background: #f1f5f9;
  650. border-radius: 4rpx;
  651. overflow: hidden;
  652. .hot-progress-bar {
  653. height: 100%;
  654. background: #f97316;
  655. border-radius: 4rpx;
  656. }
  657. }
  658. }
  659. </style>