修复:统一前后端的订单状态

This commit is contained in:
2025-08-10 21:18:08 +08:00
parent 7074da30f4
commit f36d794a2a
10 changed files with 1603 additions and 31 deletions

View File

@@ -1,3 +1,3 @@
VITE_APP_NAME=后台管理(开发环境)
VITE_API_URL=http://127.0.0.1:9200/api
#VITE_API_URL=http://127.0.0.1:9200/api
#VITE_SERVER_API_URL=http://127.0.0.1:8000/api

View File

@@ -0,0 +1,245 @@
# 订单筛选逻辑对比
## 修改前后对比
### 移动端筛选逻辑
#### 修改前(复杂的前端筛选)
```typescript
// 原有的复杂筛选逻辑
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
params.userId = Taro.getStorageSync('UserId');
const indexStr = String(index);
switch (indexStr) {
case '1': // 待付款
params.payStatus = 0; // 0表示未付款
break;
case '2': // 待发货
params.payStatus = 1; // 1表示已付款
params.deliveryStatus = 10; // 10表示未发货
break;
case '3': // 待收货
params.deliveryStatus = 20; // 20表示已发货
break;
case '4': // 已完成
params.orderStatus = 1; // 1表示已完成
break;
case '5': // 已取消
// 对于已取消的订单,获取所有数据然后在前端筛选
break;
case '0': // 全部
default:
// 全部订单,不添加额外的筛选条件
break;
}
return params;
};
// 前端二次筛选
const filterOrdersByTab = (orders: OrderWithGoods[], tabIndex: number) => {
const indexStr = String(tabIndex);
return orders.filter(order => {
switch (indexStr) {
case '1': // 待付款
return !order.payStatus && !isCancelledOrder(order);
case '2': // 待发货
return order.payStatus && order.deliveryStatus === 10 && !isCancelledOrder(order);
case '3': // 待收货
return order.deliveryStatus === 20 && !isCancelledOrder(order);
case '4': // 已完成
return order.orderStatus === 1;
case '5': // 已取消
return isCancelledOrder(order);
case '0': // 全部
default:
return true;
}
});
};
// 复杂的取消状态判断
const isCancelledOrder = (order: ShopOrder) => {
const cancelledStatuses = [2, 3, 4, 6, 7];
return cancelledStatuses.includes(order.orderStatus || 0);
};
```
#### 修改后(统一的后端筛选)
```typescript
// 简化的统一筛选逻辑
const tabs = [
{
index: 0,
statusFilter: undefined // 全部
},
{
index: 1,
statusFilter: 0 // 待付款
},
{
index: 2,
statusFilter: 1 // 待发货
},
{
index: 3,
statusFilter: 3 // 待收货
},
{
index: 4,
statusFilter: 5 // 已完成
},
{
index: 5,
statusFilter: 6 // 已取消/已退款
}
];
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
params.userId = Taro.getStorageSync('UserId');
const currentTab = tabs.find(tab => tab.index === Number(index));
if (currentTab && currentTab.statusFilter !== undefined) {
params.statusFilter = currentTab.statusFilter;
}
return params;
};
// 不再需要前端二次筛选,直接使用后端返回的数据
```
### 后台管理系统筛选逻辑
#### 修改前
```typescript
// 注释不够清晰
// 根据文档statusFilter的值对应
// -1全部0待付款1待发货2待核销3待收货4待评价5已完成6已退款7已删除
switch (activeKey.value) {
case 'all':
filterParams.statusFilter = -1; // 使用-1表示全部
break;
// ... 其他case
}
```
#### 修改后
```typescript
// 更清晰的注释和实现
// 根据后端 statusFilter 的值对应:
// undefined全部0待付款1待发货2待核销3待收货4待评价5已完成6已退款7已删除
switch (activeKey.value) {
case 'all':
// 全部订单不传statusFilter参数
// filterParams.statusFilter = undefined; // 不设置该字段
break;
// ... 其他case保持一致
}
```
## 主要改进点
### 1. 性能优化
| 方面 | 修改前 | 修改后 |
|------|--------|--------|
| 数据筛选位置 | 前端 + 后端 | 纯后端 |
| 网络传输 | 传输所有数据再筛选 | 只传输筛选后的数据 |
| 前端处理 | 复杂的二次筛选逻辑 | 直接使用后端数据 |
| 查询效率 | 低(多次查询+前端筛选) | 高(单次精确查询) |
### 2. 代码复杂度
| 方面 | 修改前 | 修改后 |
|------|--------|--------|
| 筛选逻辑行数 | ~80行 | ~20行 |
| 筛选条件数量 | 多个字段组合 | 单个statusFilter |
| 维护难度 | 高(前后端都要维护) | 低(只需维护后端) |
| 出错概率 | 高(逻辑复杂) | 低(逻辑简单) |
### 3. 一致性保证
| 方面 | 修改前 | 修改后 |
|------|--------|--------|
| 前端移动端 | 自定义筛选逻辑 | 使用statusFilter |
| 后台管理系统 | 使用statusFilter | 使用statusFilter |
| 数据一致性 | 可能不一致 | 完全一致 |
| 维护成本 | 高(两套逻辑) | 低(统一逻辑) |
## 具体的筛选条件对比
### 待付款订单
**修改前(移动端):**
```typescript
// 后端查询
params.payStatus = 0;
// 前端再筛选
return !order.payStatus && !isCancelledOrder(order);
```
**修改后:**
```typescript
// 只需后端查询
params.statusFilter = 0; // 对应 pay_status = false
```
### 待发货订单
**修改前(移动端):**
```typescript
// 后端查询
params.payStatus = 1;
params.deliveryStatus = 10;
// 前端再筛选
return order.payStatus && order.deliveryStatus === 10 && !isCancelledOrder(order);
```
**修改后:**
```typescript
// 只需后端查询
params.statusFilter = 1; // 对应 pay_status = true AND delivery_status = 10
```
### 已取消订单
**修改前(移动端):**
```typescript
// 后端查询所有数据
// 无特定筛选条件
// 前端复杂筛选
const isCancelledOrder = (order: ShopOrder) => {
const cancelledStatuses = [2, 3, 4, 6, 7];
return cancelledStatuses.includes(order.orderStatus || 0);
};
return isCancelledOrder(order);
```
**修改后:**
```typescript
// 只需后端查询
params.statusFilter = 6; // 对应 order_status = 6 (已退款)
```
## 迁移建议
1. **立即可用**:新的移动端组件已经创建,可以直接使用
2. **逐步替换**:可以先在新功能中使用,再逐步替换现有页面
3. **测试验证**:建议先在测试环境验证各个筛选条件的正确性
4. **性能监控**:关注查询性能的改善情况
## 注意事项
1. **移动端显示逻辑**虽然筛选使用statusFilter但显示的状态文本仍可以根据业务需求自定义
2. **特殊状态处理**:某些复杂的状态组合可能需要在显示层面进行适配
3. **向后兼容**:确保现有功能不受影响

View File

@@ -0,0 +1,192 @@
# 订单状态筛选统一方案
## 问题描述
原先前端移动端和后台管理系统的订单状态筛选逻辑不一致:
### 移动端原有问题:
1. 使用多个字段组合进行筛选payStatus, orderStatus, deliveryStatus
2. 在前端进行二次过滤,效率低下
3. 筛选逻辑复杂,容易出错
4. 与后端设计的 statusFilter 字段不一致
### 后台管理系统:
1. 已经使用 statusFilter 字段
2. 但注释和实现有些不一致的地方
## 解决方案
统一使用后端的 `statusFilter` 字段进行订单状态筛选,所有前端页面都采用相同的筛选逻辑。
## 后端 statusFilter 字段定义
根据后端 `ShopOrderParam.java``ShopOrderMapper.xml` 的定义:
```java
@Schema(description = "订单状态筛选:-1全部0待支付1待发货2待核销3待收货4待评价5已完成6已退款7已删除")
private Integer statusFilter;
```
## 统一的状态筛选映射表
| statusFilter | 标签名称 | 后端筛选条件 | 说明 |
|-------------|---------|-------------|------|
| undefined | 全部 | 无筛选条件 | 显示所有订单(包括已删除的) |
| 0 | 待付款 | pay_status = false | 未付款的订单 |
| 1 | 待发货 | pay_status = true AND delivery_status = 10 | 已付款但未发货 |
| 2 | 待核销 | pay_status = true AND delivery_status = 10 | 与待发货相同(特定业务场景) |
| 3 | 待收货 | pay_status = true AND delivery_status = 20 | 已发货待收货 |
| 4 | 待评价 | order_status = 1 | 与已完成相同(特定业务场景) |
| 5 | 已完成 | order_status = 1 | 订单已完成 |
| 6 | 已退款 | order_status = 6 | 退款成功的订单 |
| 7 | 已删除 | deleted = 1 | 已删除的订单 |
## 前端实现
### 移动端React/Taro
```typescript
// 统一的订单状态标签配置
const tabs = [
{
index: 0,
key: '全部',
title: '全部',
description: '所有订单',
statusFilter: undefined // 不传statusFilter显示所有订单
},
{
index: 1,
key: '待付款',
title: '待付款',
description: '等待付款的订单',
statusFilter: 0 // 对应后端pay_status = false
},
{
index: 2,
key: '待发货',
title: '待发货',
description: '已付款待发货的订单',
statusFilter: 1 // 对应后端pay_status = true AND delivery_status = 10
},
{
index: 3,
key: '待收货',
title: '待收货',
description: '已发货待收货的订单',
statusFilter: 3 // 对应后端pay_status = true AND delivery_status = 20
},
{
index: 4,
key: '已完成',
title: '已完成',
description: '已完成的订单',
statusFilter: 5 // 对应后端order_status = 1
},
{
index: 5,
key: '已取消',
title: '已取消',
description: '已取消/退款的订单',
statusFilter: 6 // 对应后端order_status = 6 (已退款)
}
]
// 筛选参数生成
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
params.userId = Taro.getStorageSync('UserId');
const currentTab = tabs.find(tab => tab.index === Number(index));
if (currentTab && currentTab.statusFilter !== undefined) {
params.statusFilter = currentTab.statusFilter;
}
return params;
};
```
### 后台管理系统Vue
```typescript
const onTabs = () => {
const filterParams: Record<string, any> = {};
switch (activeKey.value) {
case 'all':
// 全部订单不传statusFilter参数
break;
case 'unpaid':
filterParams.statusFilter = 0;
break;
case 'undelivered':
filterParams.statusFilter = 1;
break;
case 'unreceived':
filterParams.statusFilter = 3;
break;
case 'completed':
filterParams.statusFilter = 5;
break;
case 'refunded':
filterParams.statusFilter = 6;
break;
case 'deleted':
filterParams.statusFilter = 7;
break;
}
reload(filterParams);
}
```
## 优化效果
### 1. 性能提升
- 筛选逻辑在后端执行,减少前端数据处理
- 减少网络传输的数据量
- 提高查询效率
### 2. 代码简化
- 移除前端复杂的筛选逻辑
- 统一前后端筛选标准
- 减少维护成本
### 3. 一致性保证
- 前端移动端和后台管理系统使用相同的筛选逻辑
- 与后端设计保持一致
- 避免数据不一致的问题
## 注意事项
### 1. 移动端特殊处理
移动端可能需要将某些后端状态合并显示:
- "已取消" 标签可以包含多种取消状态orderStatus: 2,3,4,6,7
- 可以在前端显示时进行状态文本的转换,但筛选仍使用 statusFilter
### 2. 向后兼容
- 保持现有API接口不变
- 逐步迁移现有代码
- 确保不影响现有功能
### 3. 测试验证
- 验证各个状态筛选的正确性
- 确认前后端数据一致性
- 测试边界情况和异常处理
## 迁移步骤
1. **创建新的移动端组件**:使用统一的 statusFilter 逻辑
2. **更新后台管理系统**:完善注释和实现细节
3. **测试验证**:确保所有筛选功能正常工作
4. **逐步替换**:将旧的移动端组件替换为新组件
5. **清理代码**:移除不再使用的筛选逻辑
## 相关文件
- 移动端新组件:`src/views/shop/shopOrder/mobile/index.tsx`
- 后台管理系统:`src/views/shop/shopOrder/index.vue`
- API接口`src/api/shop/shopOrder/index.ts`
- 数据模型:`src/api/shop/shopOrder/model/index.ts`
- 后端参数:`ShopOrderParam.java`
- 后端SQL`ShopOrderMapper.xml`

View File

@@ -1,13 +1,14 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ApiResult, PageResult } from '@/api/index';
import type { ShopMerchant, ShopMerchantParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询商户
*/
export async function pageShopMerchant(params: ShopMerchantParam) {
const res = await request.get<ApiResult<PageResult<ShopMerchant>>>(
'/shop/shop-merchant/page',
MODULES_API_URL + '/shop/shop-merchant/page',
{
params
}
@@ -23,7 +24,7 @@ export async function pageShopMerchant(params: ShopMerchantParam) {
*/
export async function listShopMerchant(params?: ShopMerchantParam) {
const res = await request.get<ApiResult<ShopMerchant[]>>(
'/shop/shop-merchant',
MODULES_API_URL + '/shop/shop-merchant',
{
params
}
@@ -39,7 +40,7 @@ export async function listShopMerchant(params?: ShopMerchantParam) {
*/
export async function addShopMerchant(data: ShopMerchant) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-merchant',
MODULES_API_URL + '/shop/shop-merchant',
data
);
if (res.data.code === 0) {
@@ -53,7 +54,7 @@ export async function addShopMerchant(data: ShopMerchant) {
*/
export async function updateShopMerchant(data: ShopMerchant) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-merchant',
MODULES_API_URL + '/shop/shop-merchant',
data
);
if (res.data.code === 0) {
@@ -67,7 +68,7 @@ export async function updateShopMerchant(data: ShopMerchant) {
*/
export async function removeShopMerchant(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-merchant/' + id
MODULES_API_URL + '/shop/shop-merchant/' + id
);
if (res.data.code === 0) {
return res.data.message;
@@ -80,7 +81,7 @@ export async function removeShopMerchant(id?: number) {
*/
export async function removeBatchShopMerchant(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-merchant/batch',
MODULES_API_URL + '/shop/shop-merchant/batch',
{
data
}
@@ -96,7 +97,7 @@ export async function removeBatchShopMerchant(data: (number | undefined)[]) {
*/
export async function getShopMerchant(id: number) {
const res = await request.get<ApiResult<ShopMerchant>>(
'/shop/shop-merchant/' + id
MODULES_API_URL + '/shop/shop-merchant/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 商户
@@ -56,8 +56,16 @@ export interface ShopMerchant {
price?: string;
// 是否自营
ownStore?: number;
// 是否可以快递
canExpress?: string;
// 是否推荐
recommend?: number;
// 是否营业
isOn?: number;
//
startTime?: string;
//
endTime?: string;
// 是否需要审核
goodsReview?: number;
// 管理入口
@@ -83,8 +91,5 @@ export interface ShopMerchant {
*/
export interface ShopMerchantParam extends PageParam {
merchantId?: number;
phone?: string;
userId?: number;
shopType?: string;
keywords?: string;
}

View File

@@ -0,0 +1,41 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: any): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,454 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑商户' : '添加商户'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="商户名称" name="merchantName">
<a-input
allow-clear
placeholder="请输入商户名称"
v-model:value="form.merchantName"
/>
</a-form-item>
<a-form-item label="商户编号" name="merchantCode">
<a-input
allow-clear
placeholder="请输入商户编号"
v-model:value="form.merchantCode"
/>
</a-form-item>
<a-form-item label="商户类型" name="type">
<a-input
allow-clear
placeholder="请输入商户类型"
v-model:value="form.type"
/>
</a-form-item>
<a-form-item
label="商户图标"
name="image">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="商户手机号" name="phone">
<a-input
allow-clear
placeholder="请输入商户手机号"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="商户姓名" name="realName">
<a-input
allow-clear
placeholder="请输入商户姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="店铺类型" name="shopType">
<a-input
allow-clear
placeholder="请输入店铺类型"
v-model:value="form.shopType"
/>
</a-form-item>
<a-form-item label="项目分类" name="itemType">
<a-input
allow-clear
placeholder="请输入项目分类"
v-model:value="form.itemType"
/>
</a-form-item>
<a-form-item label="商户分类" name="category">
<a-input
allow-clear
placeholder="请输入商户分类"
v-model:value="form.category"
/>
</a-form-item>
<a-form-item label="商户经营分类" name="merchantCategoryId">
<a-input
allow-clear
placeholder="请输入商户经营分类"
v-model:value="form.merchantCategoryId"
/>
</a-form-item>
<a-form-item label="商户分类" name="merchantCategoryTitle">
<a-input
allow-clear
placeholder="请输入商户分类"
v-model:value="form.merchantCategoryTitle"
/>
</a-form-item>
<a-form-item label="经纬度" name="lngAndLat">
<a-input
allow-clear
placeholder="请输入经纬度"
v-model:value="form.lngAndLat"
/>
</a-form-item>
<a-form-item label="" name="lng">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.lng"
/>
</a-form-item>
<a-form-item label="" name="lat">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.lat"
/>
</a-form-item>
<a-form-item label="所在省份" name="province">
<a-input
allow-clear
placeholder="请输入所在省份"
v-model:value="form.province"
/>
</a-form-item>
<a-form-item label="所在城市" name="city">
<a-input
allow-clear
placeholder="请输入所在城市"
v-model:value="form.city"
/>
</a-form-item>
<a-form-item label="所在辖区" name="region">
<a-input
allow-clear
placeholder="请输入所在辖区"
v-model:value="form.region"
/>
</a-form-item>
<a-form-item label="详细地址" name="address">
<a-input
allow-clear
placeholder="请输入详细地址"
v-model:value="form.address"
/>
</a-form-item>
<a-form-item label="手续费" name="commission">
<a-input
allow-clear
placeholder="请输入手续费"
v-model:value="form.commission"
/>
</a-form-item>
<a-form-item label="关键字" name="keywords">
<a-input
allow-clear
placeholder="请输入关键字"
v-model:value="form.keywords"
/>
</a-form-item>
<a-form-item label="资质图片" name="files">
<a-input
allow-clear
placeholder="请输入资质图片"
v-model:value="form.files"
/>
</a-form-item>
<a-form-item label="营业时间" name="businessTime">
<a-input
allow-clear
placeholder="请输入营业时间"
v-model:value="form.businessTime"
/>
</a-form-item>
<a-form-item label="文章内容" name="content">
<a-input
allow-clear
placeholder="请输入文章内容"
v-model:value="form.content"
/>
</a-form-item>
<a-form-item label="每小时价格" name="price">
<a-input
allow-clear
placeholder="请输入每小时价格"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="是否自营" name="ownStore">
<a-input
allow-clear
placeholder="请输入是否自营"
v-model:value="form.ownStore"
/>
</a-form-item>
<a-form-item label="是否可以快递" name="canExpress">
<a-input
allow-clear
placeholder="请输入是否可以快递"
v-model:value="form.canExpress"
/>
</a-form-item>
<a-form-item label="是否推荐" name="recommend">
<a-input
allow-clear
placeholder="请输入是否推荐"
v-model:value="form.recommend"
/>
</a-form-item>
<a-form-item label="是否营业" name="isOn">
<a-input
allow-clear
placeholder="请输入是否营业"
v-model:value="form.isOn"
/>
</a-form-item>
<a-form-item label="" name="startTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.startTime"
/>
</a-form-item>
<a-form-item label="" name="endTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.endTime"
/>
</a-form-item>
<a-form-item label="是否需要审核" name="goodsReview">
<a-input
allow-clear
placeholder="请输入是否需要审核"
v-model:value="form.goodsReview"
/>
</a-form-item>
<a-form-item label="管理入口" name="adminUrl">
<a-input
allow-clear
placeholder="请输入管理入口"
v-model:value="form.adminUrl"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="所有人" name="userId">
<a-input
allow-clear
placeholder="请输入所有人"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addShopMerchant, updateShopMerchant } from '@/api/shop/shopMerchant';
import { ShopMerchant } from '@/api/shop/shopMerchant/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopMerchant | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<ShopMerchant>({
merchantId: undefined,
merchantName: undefined,
merchantCode: undefined,
type: undefined,
image: undefined,
phone: undefined,
realName: undefined,
shopType: undefined,
itemType: undefined,
category: undefined,
merchantCategoryId: undefined,
merchantCategoryTitle: undefined,
lngAndLat: undefined,
lng: undefined,
lat: undefined,
province: undefined,
city: undefined,
region: undefined,
address: undefined,
commission: undefined,
keywords: undefined,
files: undefined,
businessTime: undefined,
content: undefined,
price: undefined,
ownStore: undefined,
canExpress: undefined,
recommend: undefined,
isOn: undefined,
startTime: undefined,
endTime: undefined,
goodsReview: undefined,
adminUrl: undefined,
comments: undefined,
userId: undefined,
deleted: undefined,
status: undefined,
sortNumber: 100,
tenantId: undefined,
createTime: undefined,
shopMerchantId: undefined,
shopMerchantName: '',
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopMerchantName: [
{
required: true,
type: 'string',
message: '请填写商户名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateShopMerchant : addShopMerchant;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,244 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="merchantId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<Search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopMerchantEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopMerchantEdit from './components/shopMerchantEdit.vue';
import { pageShopMerchant, removeShopMerchant, removeBatchShopMerchant } from '@/api/shop/shopMerchant';
import type { ShopMerchant, ShopMerchantParam } from '@/api/shop/shopMerchant/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopMerchant[]>([]);
// 当前编辑数据
const current = ref<ShopMerchant | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopMerchant({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '商户ID',
dataIndex: 'merchantId',
key: 'merchantId',
align: 'center',
width: 90,
},
{
title: '商户名称',
dataIndex: 'merchantName',
key: 'merchantName',
align: 'center',
},
// {
// title: '店铺类型',
// dataIndex: 'shopType',
// key: 'shopType',
// align: 'center',
// },
{
title: '商户分类',
dataIndex: 'category',
key: 'category',
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
}
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]);
/* 搜索 */
const reload = (where?: ShopMerchantParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopMerchant) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopMerchant) => {
const hide = message.loading('请求中..', 0);
removeShopMerchant(row.shopMerchantId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopMerchant(selection.value.map((d) => d.merchantId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopMerchant) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopMerchant'
};
</script>
<style lang="less" scoped></style>

View File

@@ -12,14 +12,13 @@
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
<a-tab-pane key="all" tab="全部"/>
<a-tab-pane key="unpaid" tab="待付"/>
<a-tab-pane key="unpaid" tab="待付"/>
<a-tab-pane key="undelivered" tab="待发货"/>
<a-tab-pane key="unverified" tab="待核销"/>
<a-tab-pane key="unreceived" tab="待收货"/>
<a-tab-pane key="unevaluated" tab="待评价"/>
<a-tab-pane key="completed" tab="已完成"/>
<a-tab-pane key="refunded" tab="已退款"/>
<a-tab-pane key="deleted" tab="已取消"/>
<!-- <a-tab-pane key="unevaluated" tab="待评价"/>-->
<!-- <a-tab-pane key="refunded" tab="已退款"/>-->
</a-tabs>
<ele-pro-table
ref="tableRef"
@@ -268,43 +267,43 @@ const onTabs = () => {
// 使用statusFilter进行筛选这是后端专门为订单状态筛选设计的字段
const filterParams: Record<string, any> = {};
// 根据文档,statusFilter的值对应
// -1全部0待1待发货2待核销3待收货4待评价5已完成6已退款7已删除
// 根据后端 statusFilter 的值对应:
// undefined全部0待付1待发货2待核销3待收货4待评价5已完成6已退款7已删除
switch (activeKey.value) {
case 'all':
// 全部订单(排除已删除)
filterParams.statusFilter = -1;
// 全部订单不传statusFilter参数
// filterParams.statusFilter = undefined; // 不设置该字段
break;
case 'unpaid':
// 待
// 待付pay_status = false
filterParams.statusFilter = 0;
break;
case 'undelivered':
// 待发货
// 待发货pay_status = true AND delivery_status = 10
filterParams.statusFilter = 1;
break;
case 'unverified':
// 待核销
// 待核销pay_status = true AND delivery_status = 10 (与待发货相同)
filterParams.statusFilter = 2;
break;
case 'unreceived':
// 待收货
// 待收货pay_status = true AND delivery_status = 20
filterParams.statusFilter = 3;
break;
case 'unevaluated':
// 待评价
// 待评价order_status = 1 (与已完成相同)
filterParams.statusFilter = 4;
break;
case 'completed':
// 已完成
// 已完成order_status = 1
filterParams.statusFilter = 5;
break;
case 'refunded':
// 已退款
// 已退款order_status = 6
filterParams.statusFilter = 6;
break;
case 'deleted':
// 已删除/已取消
// 已删除deleted = 1
filterParams.statusFilter = 7;
break;
}

View File

@@ -0,0 +1,391 @@
import {Avatar, Cell, Space, Tabs, Button, TabPane, Image} from '@nutui/nutui-react-taro'
import {useEffect, useState, CSSProperties} from "react";
import {View} from '@tarojs/components'
import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
import {pageShopOrder, removeShopOrder, updateShopOrder} from "@/api/shop/shopOrder";
import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import {copyText} from "@/utils/common";
const getInfiniteUlStyle = (showSearch: boolean = false): CSSProperties => ({
marginTop: showSearch ? '65px' : '44px', // 如果显示搜索框,增加更多的上边距
height: showSearch ? '75vh' : '82vh', // 相应调整高度
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
})
// 统一的订单状态标签配置,与后端 statusFilter 保持一致
const tabs = [
{
index: 0,
key: '全部',
title: '全部',
description: '所有订单',
statusFilter: undefined // 不传statusFilter显示所有订单
},
{
index: 1,
key: '待付款',
title: '待付款',
description: '等待付款的订单',
statusFilter: 0 // 对应后端pay_status = false
},
{
index: 2,
key: '待发货',
title: '待发货',
description: '已付款待发货的订单',
statusFilter: 1 // 对应后端pay_status = true AND delivery_status = 10
},
{
index: 3,
key: '待收货',
title: '待收货',
description: '已发货待收货的订单',
statusFilter: 3 // 对应后端pay_status = true AND delivery_status = 20
},
{
index: 4,
key: '已完成',
title: '已完成',
description: '已完成的订单',
statusFilter: 5 // 对应后端order_status = 1
},
{
index: 5,
key: '已取消',
title: '已取消',
description: '已取消/退款的订单',
statusFilter: 6 // 对应后端order_status = 6 (已退款)
}
]
// 扩展订单接口,包含商品信息
interface OrderWithGoods extends ShopOrder {
orderGoods?: ShopOrderGoods[];
}
interface OrderListProps {
data: ShopOrder[];
onReload?: () => void;
searchParams?: ShopOrderParam;
showSearch?: boolean;
}
function OrderList(props: OrderListProps) {
const [list, setList] = useState<OrderWithGoods[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [tapIndex, setTapIndex] = useState<string | number>(0)
const [loading, setLoading] = useState(false)
// 获取订单状态文本
const getOrderStatusText = (order: ShopOrder) => {
console.log(order,'order')
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 4) return '退款申请中';
if (order.orderStatus === 5) return '退款被拒绝';
if (order.orderStatus === 6) return '退款成功';
if (order.orderStatus === 7) return '客户端申请退款';
// 检查支付状态 (payStatus为boolean类型false/0表示未付款true/1表示已付款)
if (!order.payStatus) return '等待买家付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已收货';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
if (order.orderStatus === 0) return '未使用';
return '未知状态';
};
// 获取订单状态颜色
const getOrderStatusColor = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return 'text-gray-500'; // 已取消
if (order.orderStatus === 4) return 'text-orange-500'; // 退款申请中
if (order.orderStatus === 5) return 'text-red-500'; // 退款被拒绝
if (order.orderStatus === 6) return 'text-green-500'; // 退款成功
if (order.orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
// 检查支付状态
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
// 最后检查订单完成状态
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
if (order.orderStatus === 0) return 'text-gray-500'; // 未使用
return 'text-gray-600'; // 默认颜色
};
// 使用后端统一的 statusFilter 进行筛选
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
// 添加用户ID过滤
params.userId = Taro.getStorageSync('UserId');
// 获取当前tab的statusFilter配置
const currentTab = tabs.find(tab => tab.index === Number(index));
if (currentTab && currentTab.statusFilter !== undefined) {
params.statusFilter = currentTab.statusFilter;
}
console.log(`Tab ${index} (${currentTab?.title}) 筛选参数:`, params);
return params;
};
const reload = async (resetPage = false) => {
setLoading(true);
const currentPage = resetPage ? 1 : page;
const statusParams = getOrderStatusParams(tapIndex);
const searchConditions = {
page: currentPage,
...statusParams,
...props.searchParams
};
console.log('订单筛选条件:', {
tapIndex,
statusParams,
searchConditions
});
try {
const res = await pageShopOrder(searchConditions);
let newList: OrderWithGoods[] = [];
if (res?.list && res?.list.length > 0) {
// 为每个订单获取商品信息
const ordersWithGoods = await Promise.all(
res.list.map(async (order) => {
try {
const orderGoods = await listShopOrderGoods({ orderId: order.orderId });
return {
...order,
orderGoods: orderGoods || []
};
} catch (error) {
console.error('获取订单商品失败:', error);
return {
...order,
orderGoods: []
};
}
})
);
// 合并数据
newList = resetPage ? ordersWithGoods : list?.concat(ordersWithGoods);
setHasMore(true);
} else {
newList = [];
setHasMore(false);
}
setList(newList || []);
setPage(currentPage);
setLoading(false);
} catch (error) {
console.error('加载订单失败:', error);
setLoading(false);
}
};
const reloadMore = async () => {
setPage(page + 1);
reload();
};
// 确认收货
const confirmReceive = async (order: ShopOrder) => {
try {
await updateShopOrder({
...order,
deliveryStatus: 30, // 已收货
orderStatus: 1 // 已完成
});
Taro.showToast({
title: '确认收货成功',
});
reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
} catch (error) {
Taro.showToast({
title: '确认收货失败',
});
}
};
// 取消订单
const cancelOrder = async (order: ShopOrder) => {
try {
await removeShopOrder(order.orderId);
Taro.showToast({
title: '订单已删除',
});
reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
} catch (error) {
console.error('取消订单失败:', error);
Taro.showToast({
title: '取消订单失败',
});
}
};
useEffect(() => {
reload(true); // 首次加载或tab切换时重置页码
}, [tapIndex]); // 监听tapIndex变化
useEffect(() => {
reload(true); // 搜索参数变化时重置页码
}, [props.searchParams]); // 监听搜索参数变化
return (
<>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{
top: '44px',
zIndex: 998,
borderBottom: '1px solid #e5e5e5'
}}
tabStyle={{
backgroundColor: '#ffffff',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
value={tapIndex}
onChange={(paneKey) => {
console.log('Tab切换到:', paneKey, '对应状态:', tabs[paneKey]?.title);
setTapIndex(paneKey)
}}
>
{
tabs?.map((item, index) => {
return (
<TabPane
key={index}
title={loading && tapIndex === index ? `${item.title}...` : item.title}
></TabPane>
)
})
}
</Tabs>
<div style={getInfiniteUlStyle(props.showSearch)} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
{list?.map((item, index) => {
return (
<Cell key={index} style={{padding: '16px'}} onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}>
<View className={'order-no flex justify-between'}>
<View className={'text-gray-600 font-bold text-sm'}
onClick={(e) => {e.stopPropagation(); copyText(`${item.orderNo}`)}}>{item.orderNo}</View>
<View className={`${getOrderStatusColor(item)} font-medium`}>{getOrderStatusText(item)}</View>
</View>
<div
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
{/* 商品信息 */}
<div className={'goods-info'}>
{item.orderGoods && item.orderGoods.length > 0 ? (
item.orderGoods.map((goods, goodsIndex) => (
<div key={goodsIndex} className={'flex items-center mb-2'}>
<Image
src={goods.image || '/default-goods.png'}
width="50"
height="50"
lazyLoad={false}
className={'rounded'}
/>
<div className={'ml-2 flex-1'}>
<div className={'text-sm font-bold'}>{goods.goodsName}</div>
{goods.spec && <div className={'text-gray-500 text-xs'}>{goods.spec}</div>}
<div className={'text-gray-500 text-xs'}>{goods.totalNum}</div>
</div>
<div className={'text-sm'}>{goods.price}</div>
</div>
))
) : (
<div className={'flex items-center'}>
<Avatar
src='/default-goods.png'
size={'50'}
shape={'square'}
/>
<div className={'ml-2'}>
<div className={'text-sm'}>{item.title || '订单商品'}</div>
<div className={'text-gray-400 text-xs'}>{item.totalNum}</div>
</div>
</div>
)}
</div>
<div className={'w-full text-right'}>{item.payPrice}</div>
{/* 操作按钮 */}
<Space className={'btn flex justify-end'}>
{/* 待付款状态:显示取消订单和立即支付 */}
{(!item.payStatus) && item.orderStatus !== 2 && (
<Space>
<Button size={'small'} onClick={(e) => {e.stopPropagation(); cancelOrder(item)}}></Button>
<Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); console.log('立即支付')}}></Button>
</Space>
)}
{/* 待收货状态:显示确认收货 */}
{item.deliveryStatus === 20 && (
<Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); confirmReceive(item)}}></Button>
)}
{/* 已完成状态:显示申请退款 */}
{item.orderStatus === 1 && (
<Button size={'small'} onClick={(e) => {e.stopPropagation(); console.log('申请退款')}}>退</Button>
)}
{/* 退款相关状态的按钮可以在这里添加 */}
</Space>
</Space>
</Cell>
)
})}
</InfiniteLoading>
</div>
</>
)
}
export default OrderList