|
@@ -19,12 +19,12 @@
|
|
|
<view class="stats-section">
|
|
<view class="stats-section">
|
|
|
<view class="stats-card main">
|
|
<view class="stats-card main">
|
|
|
<view class="stats-item" @click="navigateTo('/pages/orders/list')">
|
|
<view class="stats-item" @click="navigateTo('/pages/orders/list')">
|
|
|
- <text class="stats-value">{{ stats.todayStats?.orderCount || 0 }}</text>
|
|
|
|
|
|
|
+ <text class="stats-value">{{ overview.todayOrders || 0 }}</text>
|
|
|
<text class="stats-label">今日订单</text>
|
|
<text class="stats-label">今日订单</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="stats-divider"></view>
|
|
<view class="stats-divider"></view>
|
|
|
<view class="stats-item">
|
|
<view class="stats-item">
|
|
|
- <text class="stats-value accent">¥{{ formatMoney(stats.todayStats?.orderAmount || 0) }}</text>
|
|
|
|
|
|
|
+ <text class="stats-value accent">¥{{ formatMoney(overview.todaySales || 0) }}</text>
|
|
|
<text class="stats-label">今日销售额</text>
|
|
<text class="stats-label">今日销售额</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -33,14 +33,14 @@
|
|
|
<view class="stats-card mini success" @click="navigateTo('/pages/device/list')">
|
|
<view class="stats-card mini success" @click="navigateTo('/pages/device/list')">
|
|
|
<view class="stats-dot green"></view>
|
|
<view class="stats-dot green"></view>
|
|
|
<view class="stats-info">
|
|
<view class="stats-info">
|
|
|
- <text class="stats-value">{{ stats.todayStats?.deviceOnline || 0 }}</text>
|
|
|
|
|
|
|
+ <text class="stats-value">{{ overview.onlineDevices || 0 }}</text>
|
|
|
<text class="stats-label">在线设备</text>
|
|
<text class="stats-label">在线设备</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="stats-card mini warning">
|
|
<view class="stats-card mini warning">
|
|
|
<view class="stats-dot amber"></view>
|
|
<view class="stats-dot amber"></view>
|
|
|
<view class="stats-info">
|
|
<view class="stats-info">
|
|
|
- <text class="stats-value">{{ stats.todayStats?.deviceOffline || 0 }}</text>
|
|
|
|
|
|
|
+ <text class="stats-value">{{ overview.offlineDevices || 0 }}</text>
|
|
|
<text class="stats-label">离线设备</text>
|
|
<text class="stats-label">离线设备</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -83,8 +83,26 @@
|
|
|
</view>
|
|
</view>
|
|
|
<text class="quick-name">库存查询</text>
|
|
<text class="quick-name">库存查询</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="quick-item" @click="navigateTo('/pages/checkin/index')">
|
|
|
|
|
|
|
+ <view class="quick-item" @click="navigateTo('/pages/customer/list')">
|
|
|
|
|
+ <view class="quick-icon green">
|
|
|
|
|
+ <view class="icon-user"></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="quick-name">客户管理</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="quick-item" @click="navigateTo('/pages/statistics/overview')">
|
|
|
|
|
+ <view class="quick-icon blue">
|
|
|
|
|
+ <view class="icon-stats"></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="quick-name">数据统计</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="quick-item" @click="navigateTo('/pages/inventory/warning')">
|
|
|
<view class="quick-icon rose">
|
|
<view class="quick-icon rose">
|
|
|
|
|
+ <view class="icon-warning"></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="quick-name">库存预警</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="quick-item" @click="navigateTo('/pages/checkin/index')">
|
|
|
|
|
+ <view class="quick-icon amber">
|
|
|
<view class="icon-checkin"></view>
|
|
<view class="icon-checkin"></view>
|
|
|
</view>
|
|
</view>
|
|
|
<text class="quick-name">签到打卡</text>
|
|
<text class="quick-name">签到打卡</text>
|
|
@@ -100,45 +118,19 @@
|
|
|
</view>
|
|
</view>
|
|
|
<view class="chart-card">
|
|
<view class="chart-card">
|
|
|
<view class="chart-container">
|
|
<view class="chart-container">
|
|
|
- <view
|
|
|
|
|
- class="chart-bar-wrapper"
|
|
|
|
|
- v-for="(item, index) in stats.orderTrend || []"
|
|
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="chart-bar-wrapper"
|
|
|
|
|
+ v-for="(item, index) in orderTrend"
|
|
|
:key="index"
|
|
:key="index"
|
|
|
>
|
|
>
|
|
|
<text class="chart-value">{{ item.count }}</text>
|
|
<text class="chart-value">{{ item.count }}</text>
|
|
|
<view class="chart-bar-bg">
|
|
<view class="chart-bar-bg">
|
|
|
- <view
|
|
|
|
|
- class="chart-bar"
|
|
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="chart-bar"
|
|
|
:style="{ height: getBarHeight(item.count) + '%' }"
|
|
:style="{ height: getBarHeight(item.count) + '%' }"
|
|
|
></view>
|
|
></view>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="chart-label">{{ item.date.slice(-2) }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 热销商品 -->
|
|
|
|
|
- <view class="section" v-if="stats.hotProducts && stats.hotProducts.length > 0">
|
|
|
|
|
- <view class="section-header">
|
|
|
|
|
- <text class="section-title">热销商品</text>
|
|
|
|
|
- <text class="section-tag hot">TOP 5</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="hot-list">
|
|
|
|
|
- <view
|
|
|
|
|
- class="hot-item"
|
|
|
|
|
- v-for="(item, index) in stats.hotProducts || []"
|
|
|
|
|
- :key="item.id"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="hot-rank" :class="{ top: Number(index) < 3 }">
|
|
|
|
|
- <text>{{ Number(index) + 1 }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="hot-info">
|
|
|
|
|
- <text class="hot-name">{{ item.name }}</text>
|
|
|
|
|
- <text class="hot-sales">销量 {{ item.sales }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="hot-progress">
|
|
|
|
|
- <view class="hot-progress-bar" :style="{ width: getProgress(item.sales) + '%' }"></view>
|
|
|
|
|
|
|
+ <text class="chart-label">{{ item.date }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -151,7 +143,7 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted } from 'vue';
|
|
import { ref, onMounted } from 'vue';
|
|
|
-import { getDashboardData } from '@/api/dashboard';
|
|
|
|
|
|
|
+import { getDashboardOverview, getDashboardStatistics } from '@/api/dashboard';
|
|
|
import { formatMoney as formatMoneyUtil } from '@/utils/common';
|
|
import { formatMoney as formatMoneyUtil } from '@/utils/common';
|
|
|
import CustomTabBar from '@/components/CustomTabBar.vue';
|
|
import CustomTabBar from '@/components/CustomTabBar.vue';
|
|
|
|
|
|
|
@@ -159,11 +151,15 @@ import CustomTabBar from '@/components/CustomTabBar.vue';
|
|
|
const systemInfo = uni.getSystemInfoSync();
|
|
const systemInfo = uni.getSystemInfoSync();
|
|
|
const statusBarHeight = ref(systemInfo.statusBarHeight || 20);
|
|
const statusBarHeight = ref(systemInfo.statusBarHeight || 20);
|
|
|
|
|
|
|
|
-const stats = ref<any>({});
|
|
|
|
|
|
|
+const overview = ref<any>({});
|
|
|
|
|
+const statistics = ref<any>({});
|
|
|
|
|
+const orderTrend = ref<any[]>([]);
|
|
|
const maxCount = ref(0);
|
|
const maxCount = ref(0);
|
|
|
-const maxSales = ref(0);
|
|
|
|
|
|
|
|
|
|
-const formatMoney = (amount: number) => formatMoneyUtil(amount);
|
|
|
|
|
|
|
+const formatMoney = (amount: number | string) => {
|
|
|
|
|
+ const num = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
|
|
|
+ return formatMoneyUtil(num || 0);
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
const getTimeGreeting = () => {
|
|
const getTimeGreeting = () => {
|
|
|
const hour = new Date().getHours();
|
|
const hour = new Date().getHours();
|
|
@@ -178,24 +174,29 @@ const getBarHeight = (count: number) => {
|
|
|
return Math.max(15, (count / maxCount.value) * 100);
|
|
return Math.max(15, (count / maxCount.value) * 100);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const getProgress = (sales: number) => {
|
|
|
|
|
- if (maxSales.value === 0) return 0;
|
|
|
|
|
- return Math.max(10, (sales / maxSales.value) * 100);
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
const navigateTo = (url: string) => {
|
|
const navigateTo = (url: string) => {
|
|
|
uni.navigateTo({ url });
|
|
uni.navigateTo({ url });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const loadData = async () => {
|
|
const loadData = async () => {
|
|
|
try {
|
|
try {
|
|
|
- const data = await getDashboardData();
|
|
|
|
|
- stats.value = data;
|
|
|
|
|
- if (data.orderTrend && data.orderTrend.length > 0) {
|
|
|
|
|
- maxCount.value = Math.max(...data.orderTrend.map((item: any) => item.count));
|
|
|
|
|
- }
|
|
|
|
|
- if (data.hotProducts && data.hotProducts.length > 0) {
|
|
|
|
|
- maxSales.value = Math.max(...data.hotProducts.map((item: any) => item.sales));
|
|
|
|
|
|
|
+ const [overviewRes, statisticsRes] = await Promise.all([
|
|
|
|
|
+ getDashboardOverview(),
|
|
|
|
|
+ getDashboardStatistics('week')
|
|
|
|
|
+ ]);
|
|
|
|
|
+ overview.value = overviewRes || {};
|
|
|
|
|
+ statistics.value = statisticsRes || {};
|
|
|
|
|
+
|
|
|
|
|
+ // 构建订单趋势数据
|
|
|
|
|
+ const dates = statisticsRes?.dates || [];
|
|
|
|
|
+ const ordersData = statisticsRes?.ordersData || [];
|
|
|
|
|
+ orderTrend.value = dates.map((date: string, index: number) => ({
|
|
|
|
|
+ date: date.slice(5), // MM-dd
|
|
|
|
|
+ count: ordersData[index] || 0
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ if (orderTrend.value.length > 0) {
|
|
|
|
|
+ maxCount.value = Math.max(...orderTrend.value.map((item: any) => item.count));
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取仪表盘数据失败', error);
|
|
console.error('获取仪表盘数据失败', error);
|
|
@@ -585,10 +586,10 @@ onMounted(() => {
|
|
|
.icon-checkin {
|
|
.icon-checkin {
|
|
|
width: 28rpx;
|
|
width: 28rpx;
|
|
|
height: 28rpx;
|
|
height: 28rpx;
|
|
|
- border: 3rpx solid #f43f5e;
|
|
|
|
|
|
|
+ border: 3rpx solid #f97316;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
position: relative;
|
|
position: relative;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
&::before {
|
|
&::before {
|
|
|
content: '';
|
|
content: '';
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -597,10 +598,10 @@ onMounted(() => {
|
|
|
transform: translate(-50%, -50%);
|
|
transform: translate(-50%, -50%);
|
|
|
width: 8rpx;
|
|
width: 8rpx;
|
|
|
height: 8rpx;
|
|
height: 8rpx;
|
|
|
- background: #f43f5e;
|
|
|
|
|
|
|
+ background: #f97316;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
&::after {
|
|
&::after {
|
|
|
content: '';
|
|
content: '';
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -608,12 +609,81 @@ onMounted(() => {
|
|
|
right: -6rpx;
|
|
right: -6rpx;
|
|
|
width: 12rpx;
|
|
width: 12rpx;
|
|
|
height: 12rpx;
|
|
height: 12rpx;
|
|
|
|
|
+ background: #f97316;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .icon-user {
|
|
|
|
|
+ width: 24rpx;
|
|
|
|
|
+ height: 24rpx;
|
|
|
|
|
+ border: 3rpx solid #10b981;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -10rpx;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ width: 12rpx;
|
|
|
|
|
+ height: 12rpx;
|
|
|
|
|
+ border: 3rpx solid #10b981;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .icon-stats {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ width: 28rpx;
|
|
|
|
|
+ height: 24rpx;
|
|
|
|
|
+
|
|
|
|
|
+ &::before, &::after, & {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ width: 8rpx;
|
|
|
|
|
+ background: #0ea5e9;
|
|
|
|
|
+ border-radius: 4rpx;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::before { height: 10rpx; margin-right: 2rpx; }
|
|
|
|
|
+ & { height: 22rpx; margin-right: 2rpx; }
|
|
|
|
|
+ &::after { height: 16rpx; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .icon-warning {
|
|
|
|
|
+ width: 28rpx;
|
|
|
|
|
+ height: 28rpx;
|
|
|
|
|
+ border: 3rpx solid #f43f5e;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ width: 3rpx;
|
|
|
|
|
+ height: 12rpx;
|
|
|
|
|
+ background: #f43f5e;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::after {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 4rpx;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ width: 3rpx;
|
|
|
|
|
+ height: 3rpx;
|
|
|
background: #f43f5e;
|
|
background: #f43f5e;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
.quick-name {
|
|
.quick-name {
|
|
|
font-size: 24rpx;
|
|
font-size: 24rpx;
|
|
|
color: #475569;
|
|
color: #475569;
|