feat(shop): 实时修复今日数据及优化商城设置与商品列表展示

- 修复 dashboard 今日数据统计,改为实时计算订单、销售额、用户数与优惠券使用量
- 统计数据写入 cmsStatistics 表,新增 couponUsedCount 状态与 getter
- dashboard 页面移除原有按日期查询逻辑,优惠券使用数改为响应式读取
- 商城设置 Logo 上传改为直接调用 OSS 接口上传,移除图片库选择组件
- 新增商品列表中市场价和会员价列,数据为空时显示 "-"
- 同步更新商品列表导出功能,包含新增的价格列与列宽配置
- 商品列表商品图片添加 OSS 压缩访问支持
- 调整商品分类搜索框与关键词输入框宽度以优化布局
This commit is contained in:
2026-07-14 20:27:26 +08:00
parent 8501306813
commit a2640ba7dc
6 changed files with 190 additions and 76 deletions

View File

@@ -35,6 +35,29 @@
- `src/views/shop/shopOrder/useOrderNotify.ts` 增加 `onNewOrder` 回调参数,检测到新订单触发提醒(叮声+语音)时同时调用回调。 - `src/views/shop/shopOrder/useOrderNotify.ts` 增加 `onNewOrder` 回调参数,检测到新订单触发提醒(叮声+语音)时同时调用回调。
- `src/views/shop/shopOrder/index.vue` 调用 `useOrderNotify({ onNewOrder: () => reload() })`,使新订单提醒时自动刷新表格数据,保持数据同步。 - `src/views/shop/shopOrder/index.vue` 调用 `useOrderNotify({ onNewOrder: () => reload() })`,使新订单提醒时自动刷新表格数据,保持数据同步。
## 2026-07-14 实时统计 Dashboard 今日数据
### 问题
用户再次反馈 `/shop/dashboard` 的「今日数据概况」与「待处理事项」全部显示为 0且已确认存在今日订单/用户数据。根本原因是 `cmsStatistics` 统计表未写入今日数据,而前端仍依赖该表读取 todaySales/todayOrders/todayUsers另外 dashboard 中直接按日期查询时使用了 `YYYY-MM-DD` 格式,缺少时间部分,导致时间范围不匹配。
### 修复
- `src/store/modules/statistics.ts`
- 引入 `dayjs``listShopOrder`
-`fetchStatistics` 中实时调用 `listShopOrder({ createTimeStart, createTimeEnd })` 获取今日订单列表,直接计算 `todayOrders``todaySales`(仅已付款订单)、`couponUsedCount`
- 调用 `pageUsers({ createTimeStart, createTimeEnd })` 获取 `todayUsers`
- 将今日数据同步写入 `cmsStatistics` 表(若存在记录),同时新增 `couponUsedCount` store 状态与 getter。
- `src/views/shop/dashboard/index.vue`
- 移除原来使用 `createTimeStart/End: today`(仅日期)查询今日订单并作为优惠券使用数的占位逻辑。
- `couponUsedCount` 改为从 `statisticsStore.couponUsedCount` 读取。
- 今日销售额使用 `formatMoney` 保留两位小数显示。
### 验证
- `pnpm run build` 成功。
- `vue-tsc --noEmit` 在改动文件中未引入新错误。
### 备注
- 待发货/退款仍使用 `statusFilter=1``statusFilter=6`;若后续出现货到付款订单未计入待发货,需再检查订单 `payStatus` 在后端是否被正确标记为已付款。
## 2026-07-14 订单列表商品图片接入 OSS 压缩 ## 2026-07-14 订单列表商品图片接入 OSS 压缩
### 改动内容 ### 改动内容
@@ -47,3 +70,34 @@
### 待办 ### 待办
- 用户提到后端也需要此方法,但当前工作区只有前端代码,后端实现需在后端项目补充。 - 用户提到后端也需要此方法,但当前工作区只有前端代码,后端实现需在后端项目补充。
## 2026-07-14 商城设置 Logo 改为直传
### 修改文件
- `src/views/shop/shopSetting/components/basic.vue`
### 修改内容
- 将商城 Logo 上传方式从 `SelectFile` 图片库选择改为 `a-upload` + `uploadOss` 直接上传,与商品编辑弹窗的商品图片上传方式保持一致。
- 已上传图片使用 `a-image` 预览,下方提供"上传"按钮覆盖替换。
- 删除不再使用的 `SelectFile` 导入、`logoList` 变量、`chooseImage`/`onDeleteImage` 回调。
- 使用 `ele-admin-pro``messageLoading` 保持上传加载提示与商品编辑弹窗一致。
## 2026-07-14 商品列表增加三价格展示
### 改动文件
- `src/views/shop/shopGoods/index.vue`
### 列表页列配置
- 原来只显示一列「价格」(`price`)
- 新增「市场价」列 (`salePrice`),数据为空时显示 `-`
- 新增「会员价」列 (`dealerPrice`),数据为空时显示 `-`
- 三列均设置 `width: 110``align: 'center'``customRender``¥` 前缀
### 导出功能同步更新
- 导出表头增加「市场价」「会员价」两列
- 数据行对应增加 `goods.salePrice``goods.dealerPrice` 输出
- 列宽配置同步增加两列 `{ wch: 12 }`
### 备注
- 编辑表单 `shopGoodsEdit.vue` 已支持三个价格编辑(价格/市场价/会员价),无需修改
- 数据模型字段映射:价格=price, 市场价=salePrice, 会员价=dealerPrice

View File

@@ -2,8 +2,9 @@
* 统计数据 store * 统计数据 store
*/ */
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import dayjs from 'dayjs';
import { pageUsers } from '@/api/system/user'; import { pageUsers } from '@/api/system/user';
import { pageShopOrder, shopOrderTotal } from '@/api/shop/shopOrder'; import { pageShopOrder, shopOrderTotal, listShopOrder } from '@/api/shop/shopOrder';
import { import {
addCmsStatistics, addCmsStatistics,
listCmsStatistics, listCmsStatistics,
@@ -23,6 +24,8 @@ export interface StatisticsState {
cacheExpiry: number; cacheExpiry: number;
// 自动刷新定时器 // 自动刷新定时器
refreshTimer: number | null; refreshTimer: number | null;
// 今日使用优惠券数量
couponUsedCount: number;
} }
export const useStatisticsStore = defineStore('statistics', { export const useStatisticsStore = defineStore('statistics', {
@@ -32,7 +35,8 @@ export const useStatisticsStore = defineStore('statistics', {
lastUpdateTime: null, lastUpdateTime: null,
// 默认缓存5分钟 // 默认缓存5分钟
cacheExpiry: 5 * 60 * 1000, cacheExpiry: 5 * 60 * 1000,
refreshTimer: null refreshTimer: null,
couponUsedCount: 0
}), }),
getters: { getters: {
@@ -85,6 +89,13 @@ export const useStatisticsStore = defineStore('statistics', {
return safeNumber(state.statistics?.todayUsers); return safeNumber(state.statistics?.todayUsers);
}, },
/**
* 获取今日使用优惠券数量
*/
couponUsedCount: (state): number => {
return safeNumber(state.couponUsedCount);
},
/** /**
* 检查缓存是否有效 * 检查缓存是否有效
*/ */
@@ -183,38 +194,49 @@ export const useStatisticsStore = defineStore('statistics', {
return 0; return 0;
})(); })();
// 安全获取今日销售额 // 实时计算今日数据(不依赖可能未更新的统计表)
const todaySales = (() => { const todayStart = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
if (statisticsData && statisticsData.length > 0) { const todayEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
const stats = statisticsData[0];
if (stats.todaySales !== undefined && stats.todaySales !== null) {
return safeNumber(stats.todaySales);
}
}
return 0;
})();
// 安全获取今日订单 // 安全获取今日订单列表、销售额和优惠券使用量
const todayOrders = (() => { let todayOrders = 0;
if (statisticsData && statisticsData.length > 0) { let todaySales = 0;
const stats = statisticsData[0]; let couponUsedCount = 0;
if (stats.todayOrders !== undefined && stats.todayOrders !== null) { try {
return safeNumber(stats.todayOrders); const todayOrderList = await listShopOrder({
createTimeStart: todayStart,
createTimeEnd: todayEnd
});
if (Array.isArray(todayOrderList)) {
todayOrders = todayOrderList.length;
todaySales = todayOrderList.reduce((acc, order) => {
return acc + (order.payStatus ? safeNumber(order.payPrice) : 0);
}, 0);
couponUsedCount = todayOrderList.filter((order) => {
const couponType = order.couponType;
return couponType !== undefined && couponType !== null && couponType !== 0;
}).length;
} }
} catch (e) {
console.warn('⚠️ 获取今日订单列表失败:', e);
} }
return 0;
})();
// 安全获取今日新增用户 // 安全获取今日新增用户
const todayUsers = (() => { let todayUsers = 0;
if (statisticsData && statisticsData.length > 0) { try {
const stats = statisticsData[0]; const todayUsersResult = await pageUsers({
if (stats.todayUsers !== undefined && stats.todayUsers !== null) { page: 1,
return safeNumber(stats.todayUsers); limit: 1,
createTimeStart: todayStart,
createTimeEnd: todayEnd
});
if (todayUsersResult && typeof todayUsersResult === 'object' && 'count' in todayUsersResult) {
todayUsers = safeNumber(todayUsersResult.count);
} }
} catch (e) {
console.warn('⚠️ 获取今日新增用户失败:', e);
} }
return 0;
})();
const totalSales = (() => { const totalSales = (() => {
if (!total) { if (!total) {
console.warn('⚠️ 订单总额API返回空数据'); console.warn('⚠️ 订单总额API返回空数据');
@@ -244,7 +266,10 @@ export const useStatisticsStore = defineStore('statistics', {
id: existingStatistics.id, id: existingStatistics.id,
userCount: userCount, userCount: userCount,
orderCount: orderCount, orderCount: orderCount,
totalSales: totalSales totalSales: totalSales,
todaySales: todaySales,
todayOrders: todayOrders,
todayUsers: todayUsers
}; };
// 异步更新数据库 // 异步更新数据库
@@ -293,6 +318,7 @@ export const useStatisticsStore = defineStore('statistics', {
} }
this.statistics = statistics; this.statistics = statistics;
this.couponUsedCount = couponUsedCount;
this.lastUpdateTime = Date.now(); this.lastUpdateTime = Date.now();
return statistics; return statistics;

View File

@@ -99,7 +99,7 @@
<div class="today-stat"> <div class="today-stat">
<div class="today-stat-icon">💰</div> <div class="today-stat-icon">💰</div>
<div class="today-stat-info"> <div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.salesAmount || '0.00' }}</div> <div class="today-stat-value">{{ formatMoney(todayStats.salesAmount) }}</div>
<div class="today-stat-label">今日销售额</div> <div class="today-stat-label">今日销售额</div>
</div> </div>
</div> </div>
@@ -267,7 +267,7 @@ const coreStats = computed(() => [
// 待处理事项数据 // 待处理事项数据
const pendingShipmentCount = ref(0); const pendingShipmentCount = ref(0);
const pendingRefundCount = ref(0); const pendingRefundCount = ref(0);
const couponUsedCount = ref(0); const couponUsedCount = computed(() => statisticsStore.couponUsedCount);
// 待处理事项使用computed确保响应式更新 // 待处理事项使用computed确保响应式更新
const todoItems = computed(() => [ const todoItems = computed(() => [
@@ -301,6 +301,12 @@ const navigateTo = (path: string) => {
} }
}; };
// 金额格式化(保留两位小数)
const formatMoney = (value?: number | string) => {
const num = Number(value);
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
};
// 清除缓存 // 清除缓存
const handleClearCache = () => { const handleClearCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then( removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
@@ -358,22 +364,6 @@ const loadData = async () => {
pendingRefundCount.value = 0; pendingRefundCount.value = 0;
} }
try {
// 获取今日订单总数(用于计算优惠券使用率)
const today = new Date().toISOString().split('T')[0];
const todayOrderResult = await pageShopOrder({
createTimeStart: today,
createTimeEnd: today,
page: 1,
limit: 1
});
// 暂时使用今日订单数作为优惠券使用参考
couponUsedCount.value = todayOrderResult?.count || 0;
} catch (e) {
console.warn('获取今日订单数失败:', e);
couponUsedCount.value = 0;
}
} catch (error) { } catch (error) {
console.error('加载数据失败:', error); console.error('加载数据失败:', error);
} }

View File

@@ -66,7 +66,7 @@
allow-clear allow-clear
:tree-data="navigationList" :tree-data="navigationList"
tree-default-expand-all tree-default-expand-all
style="width: 240px" style="width: 180px"
:listHeight="700" :listHeight="700"
placeholder="请选择分类" placeholder="请选择分类"
:value="where.categoryId || undefined" :value="where.categoryId || undefined"
@@ -77,7 +77,7 @@
<a-input-search <a-input-search
allow-clear allow-clear
placeholder="请输入关键词" placeholder="请输入关键词"
style="width: 360px" style="width: 220px"
v-model:value="where.keywords" v-model:value="where.keywords"
@pressEnter="reload" @pressEnter="reload"
@search="reload" @search="reload"

View File

@@ -33,7 +33,7 @@
<template v-if="column.key === 'name'"> <template v-if="column.key === 'name'">
<a-space class="flex items-center cursor-pointer"> <a-space class="flex items-center cursor-pointer">
<a-image <a-image
:src="record.image" :src="getCompressedImageUrl(record.image)"
v-if="record.image" v-if="record.image"
:preview="false" :preview="false"
:width="50" :width="50"
@@ -132,6 +132,7 @@
} from '@/api/shop/shopGoods'; } from '@/api/shop/shopGoods';
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'; import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model';
import { getPageTitle } from '@/utils/common'; import { getPageTitle } from '@/utils/common';
import { getCompressedImageUrl } from '@/utils/image';
import { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model'; import { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory'; import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory';
import { utils, writeFile } from 'xlsx'; import { utils, writeFile } from 'xlsx';
@@ -199,13 +200,30 @@
key: 'code', key: 'code',
align: 'center', align: 'center',
}, },
{
title: '市场价',
dataIndex: 'salePrice',
key: 'salePrice',
align: 'center',
width: 110,
customRender: ({ text }) => (text ? `${text}` : '-')
},
{ {
title: '价格', title: '价格',
dataIndex: 'price', dataIndex: 'price',
key: 'price', key: 'price',
align: 'center', align: 'center',
width: 110,
customRender: ({ text }) => `${text}` customRender: ({ text }) => `${text}`
}, },
{
title: '会员价',
dataIndex: 'dealerPrice',
key: 'dealerPrice',
align: 'center',
width: 110,
customRender: ({ text }) => (text ? `${text}` : '-')
},
{ {
title: '销量', title: '销量',
dataIndex: 'sales', dataIndex: 'sales',
@@ -347,6 +365,8 @@
'商品ID', '商品ID',
'商品名称', '商品名称',
'价格', '价格',
'市场价',
'会员价',
'销量', '销量',
'库存', '库存',
'状态', '状态',
@@ -368,6 +388,8 @@
`${goods.goodsId || ''}`, `${goods.goodsId || ''}`,
`${goods.name || ''}`, `${goods.name || ''}`,
`${goods.price || 0}`, `${goods.price || 0}`,
`${goods.salePrice ? `${goods.salePrice}` : ''}`,
`${goods.dealerPrice ? `${goods.dealerPrice}` : ''}`,
`${goods.sales || 0}`, `${goods.sales || 0}`,
`${goods.stock || 0}`, `${goods.stock || 0}`,
statusMap[goods.status || 0] || '', statusMap[goods.status || 0] || '',
@@ -388,6 +410,8 @@
{ wch: 10 }, { wch: 10 },
{ wch: 30 }, { wch: 30 },
{ wch: 12 }, { wch: 12 },
{ wch: 12 },
{ wch: 12 },
{ wch: 10 }, { wch: 10 },
{ wch: 10 }, { wch: 10 },
{ wch: 12 }, { wch: 12 },

View File

@@ -16,13 +16,27 @@
/> />
</a-form-item> </a-form-item>
<a-form-item label="商城Logo" name="shopLogo"> <a-form-item label="商城Logo" name="shopLogo">
<SelectFile <a-image
:placeholder="'请选择商城Logo'" v-if="form.shopLogo"
:limit="1" width="100px"
:data="logoList" height="100px"
@done="chooseImage" :src="form.shopLogo"
@del="onDeleteImage" style="margin-right: 10px; border-radius: 6px;"
/> />
<a-upload
:show-upload-list="false"
:customRequest="onUploadLogo"
>
<a-button class="ele-btn-icon">
<template #icon>
<UploadOutlined/>
</template>
<span>上传</span>
</a-button>
</a-upload>
<div class="ele-text-placeholder" style="margin-top: 6px;">
建议上传 400x400 像素的正方形 Logo图片大小不超过 10MB
</div>
</a-form-item> </a-form-item>
<a-form-item label="商城描述" name="shopDesc"> <a-form-item label="商城描述" name="shopDesc">
<a-textarea <a-textarea
@@ -59,7 +73,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, watch, onMounted } from 'vue'; import { UploadOutlined } from '@ant-design/icons-vue';
import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@@ -67,8 +82,8 @@ import { FormInstance } from 'ant-design-vue/es/form';
import useFormData from '@/utils/use-form-data'; import useFormData from '@/utils/use-form-data';
import { batchSaveShopSetting, getShopSettingCategoryValues } from '@/api/shop/shopSetting'; import { batchSaveShopSetting, getShopSettingCategoryValues } from '@/api/shop/shopSetting';
import type { ShopSetting } from '@/api/shop/shopSetting/model'; import type { ShopSetting } from '@/api/shop/shopSetting/model';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types'; import { uploadOss } from '@/api/system/file';
import { FileRecord } from '@/api/system/file/model'; import { messageLoading } from 'ele-admin-pro';
const category = 'basic'; const category = 'basic';
@@ -77,7 +92,6 @@ const { styleResponsive } = storeToRefs(themeStore);
const loading = ref(false); const loading = ref(false);
const formRef = ref<FormInstance | null>(null); const formRef = ref<FormInstance | null>(null);
const logoList = ref<ItemType[]>([]);
const { form, assignFields } = useFormData<ShopSetting>({ const { form, assignFields } = useFormData<ShopSetting>({
shopName: '', shopName: '',
@@ -99,16 +113,27 @@ const rules = reactive({
] ]
}); });
// 选择图片 // 上传 Logo
const chooseImage = (data: FileRecord) => { const onUploadLogo = (item: any) => {
logoList.value.push({ uid: data.id, url: data.path, status: 'done' }); const { file } = item;
form.shopLogo = data.path; if (file.size / 1024 / 1024 > 10) {
}; message.error('图片大小不能超过 10MB');
return;
// 删除图片 }
const onDeleteImage = (index: number) => { const hide = messageLoading({
logoList.value.splice(index, 1); content: '上传中..',
form.shopLogo = ''; duration: 0,
mask: true
});
uploadOss(file)
.then((res) => {
hide();
form.shopLogo = res.path;
})
.catch((e) => {
hide();
message.error(e.message || '上传失败');
});
}; };
const load = async () => { const load = async () => {
@@ -119,11 +144,6 @@ const load = async () => {
// 布尔值转换(后端可能返回字符串 '1'/'0'/'true'/'false' // 布尔值转换(后端可能返回字符串 '1'/'0'/'true'/'false'
const raw = (values as any).shopEnabled; const raw = (values as any).shopEnabled;
form.shopEnabled = raw === true || raw === 'true' || raw === '1' || raw === 1; form.shopEnabled = raw === true || raw === 'true' || raw === '1' || raw === 1;
// 回显 Logo
logoList.value = [];
if (form.shopLogo) {
logoList.value.push({ uid: 1, url: form.shopLogo as string, status: '' });
}
} }
} catch (e: any) { } catch (e: any) {
// 未配置时忽略 // 未配置时忽略