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

@@ -99,7 +99,7 @@
<div class="today-stat">
<div class="today-stat-icon">💰</div>
<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>
</div>
@@ -267,7 +267,7 @@ const coreStats = computed(() => [
// 待处理事项数据
const pendingShipmentCount = ref(0);
const pendingRefundCount = ref(0);
const couponUsedCount = ref(0);
const couponUsedCount = computed(() => statisticsStore.couponUsedCount);
// 待处理事项使用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 = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
@@ -358,22 +364,6 @@ const loadData = async () => {
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) {
console.error('加载数据失败:', error);
}

View File

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

View File

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

View File

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