forked from gxwebsoft/mp-10550
feat(store): 添加门店管理功能和订单配送功能
- 在app.config.ts中添加门店相关路由配置 - 在config/app.ts中添加租户名称常量 - 在Header.tsx中实现门店选择功能,包括定位、距离计算和门店切换 - 更新ShopOrder模型,添加门店ID、门店名称、配送员ID和仓库ID字段 - 新增ShopStore相关API和服务,支持门店的增删改查 - 新增ShopStoreRider相关API和服务,支持配送员管理 - 新增ShopStoreUser相关API和服务,支持店员管理 - 新增ShopWarehouse相关API和服务,支持仓库管理 - 添加配送订单页面,支持订单状态管理和送达确认功能 - 优化经销商页面的样式布局
This commit is contained in:
@@ -2,6 +2,8 @@ import { API_BASE_URL } from './env'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '10584';
|
||||
// 租户名称
|
||||
export const TenantName = '桂乐淘';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
|
||||
@@ -27,6 +27,14 @@ export interface ShopOrder {
|
||||
merchantName?: string;
|
||||
// 商户编号
|
||||
merchantCode?: string;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 使用的优惠券id
|
||||
couponId?: number;
|
||||
// 使用的会员卡id
|
||||
@@ -61,6 +69,8 @@ export interface ShopOrder {
|
||||
sendStartTime?: string;
|
||||
// 配送结束时间
|
||||
sendEndTime?: string;
|
||||
// 配送员送达拍照(选填)
|
||||
sendEndImg?: string;
|
||||
// 发货店铺id
|
||||
expressMerchantId?: number;
|
||||
// 发货店铺
|
||||
@@ -165,6 +175,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -197,6 +215,14 @@ export interface OrderGoodsItem {
|
||||
export interface OrderCreateRequest {
|
||||
// 商品信息列表
|
||||
goodsItems: OrderGoodsItem[];
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 归属门店名称(可选)
|
||||
storeName?: string;
|
||||
// 配送员用户ID(优先级派单)
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
// 收货地址ID
|
||||
addressId?: number;
|
||||
// 支付方式
|
||||
@@ -223,6 +249,12 @@ export interface ShopOrderParam extends PageParam {
|
||||
payType?: number;
|
||||
isInvoice?: boolean;
|
||||
userId?: number;
|
||||
// 归属门店ID(shop_store.id)
|
||||
storeId?: number;
|
||||
// 配送员用户ID
|
||||
riderId?: number;
|
||||
// 发货仓库ID
|
||||
warehouseId?: number;
|
||||
keywords?: string;
|
||||
deliveryStatus?: number;
|
||||
statusFilter?: number;
|
||||
|
||||
101
src/api/shop/shopStore/index.ts
Normal file
101
src/api/shop/shopStore/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStore, ShopStoreParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询门店
|
||||
*/
|
||||
export async function pageShopStore(params: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
|
||||
'/shop/shop-store/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询门店列表
|
||||
*/
|
||||
export async function listShopStore(params?: ShopStoreParam) {
|
||||
const res = await request.get<ApiResult<ShopStore[]>>(
|
||||
'/shop/shop-store',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加门店
|
||||
*/
|
||||
export async function addShopStore(data: ShopStore) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改门店
|
||||
*/
|
||||
export async function updateShopStore(data: ShopStore) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除门店
|
||||
*/
|
||||
export async function removeShopStore(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除门店
|
||||
*/
|
||||
export async function removeBatchShopStore(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询门店
|
||||
*/
|
||||
export async function getShopStore(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStore>>(
|
||||
'/shop/shop-store/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
63
src/api/shop/shopStore/model/index.ts
Normal file
63
src/api/shop/shopStore/model/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
export interface ShopStore {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 店铺名称
|
||||
name?: string;
|
||||
// 门店地址
|
||||
address?: string;
|
||||
// 手机号码
|
||||
phone?: string;
|
||||
// 邮箱
|
||||
email?: string;
|
||||
// 门店经理
|
||||
managerName?: string;
|
||||
// 门店banner
|
||||
shopBanner?: string;
|
||||
// 所在省份
|
||||
province?: string;
|
||||
// 所在城市
|
||||
city?: string;
|
||||
// 所在辖区
|
||||
region?: string;
|
||||
// 经度和纬度
|
||||
lngAndLat?: string;
|
||||
// 位置
|
||||
location?:string;
|
||||
// 区域
|
||||
district?: string;
|
||||
// 轮廓
|
||||
points?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 默认仓库ID(shop_warehouse.id)
|
||||
warehouseId?: number;
|
||||
// 默认仓库名称(可选)
|
||||
warehouseName?: string;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店搜索条件
|
||||
*/
|
||||
export interface ShopStoreParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
101
src/api/shop/shopStoreRider/index.ts
Normal file
101
src/api/shop/shopStoreRider/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询配送员
|
||||
*/
|
||||
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
|
||||
'/shop/shop-store-rider/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询配送员列表
|
||||
*/
|
||||
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreRider[]>>(
|
||||
'/shop/shop-store-rider',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加配送员
|
||||
*/
|
||||
export async function addShopStoreRider(data: ShopStoreRider) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改配送员
|
||||
*/
|
||||
export async function updateShopStoreRider(data: ShopStoreRider) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配送员
|
||||
*/
|
||||
export async function removeShopStoreRider(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除配送员
|
||||
*/
|
||||
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-rider/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询配送员
|
||||
*/
|
||||
export async function getShopStoreRider(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreRider>>(
|
||||
'/shop/shop-store-rider/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
67
src/api/shop/shopStoreRider/model/index.ts
Normal file
67
src/api/shop/shopStoreRider/model/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 配送员
|
||||
*/
|
||||
export interface ShopStoreRider {
|
||||
// 主键ID
|
||||
id?: string;
|
||||
// 配送点ID(shop_dealer.id)
|
||||
dealerId?: number;
|
||||
// 骑手编号(可选)
|
||||
riderNo?: string;
|
||||
// 姓名
|
||||
realName?: string;
|
||||
// 手机号
|
||||
mobile?: string;
|
||||
// 头像
|
||||
avatar?: string;
|
||||
// 身份证号(可选)
|
||||
idCardNo?: string;
|
||||
// 状态:1启用;0禁用
|
||||
status?: number;
|
||||
// 接单状态:0休息/下线;1在线;2忙碌
|
||||
workStatus?: number;
|
||||
// 是否开启自动派单:1是;0否
|
||||
autoDispatchEnabled?: number;
|
||||
// 派单优先级(同小区多骑手时可用,值越大越优先)
|
||||
dispatchPriority?: number;
|
||||
// 最大同时配送单数(0表示不限制)
|
||||
maxOnhandOrders?: number;
|
||||
// 是否计算工资(提成):1计算;0不计算(如三方配送点可设0)
|
||||
commissionCalcEnabled?: number;
|
||||
// 水每桶提成金额(元/桶)
|
||||
waterBucketUnitFee?: string;
|
||||
// 其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表)
|
||||
otherGoodsCommissionType?: number;
|
||||
// 其他商品提成值:固定金额(元)或比例(%)
|
||||
otherGoodsCommissionValue?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配送员搜索条件
|
||||
*/
|
||||
export interface ShopStoreRiderParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
// 配送点/门店ID(后端可能用 dealerId 或 storeId)
|
||||
dealerId?: number;
|
||||
storeId?: number;
|
||||
status?: number;
|
||||
workStatus?: number;
|
||||
autoDispatchEnabled?: number;
|
||||
}
|
||||
101
src/api/shop/shopStoreUser/index.ts
Normal file
101
src/api/shop/shopStoreUser/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopStoreUser, ShopStoreUserParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询店员
|
||||
*/
|
||||
export async function pageShopStoreUser(params: ShopStoreUserParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
|
||||
'/shop/shop-store-user/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询店员列表
|
||||
*/
|
||||
export async function listShopStoreUser(params?: ShopStoreUserParam) {
|
||||
const res = await request.get<ApiResult<ShopStoreUser[]>>(
|
||||
'/shop/shop-store-user',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加店员
|
||||
*/
|
||||
export async function addShopStoreUser(data: ShopStoreUser) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改店员
|
||||
*/
|
||||
export async function updateShopStoreUser(data: ShopStoreUser) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除店员
|
||||
*/
|
||||
export async function removeShopStoreUser(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除店员
|
||||
*/
|
||||
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-store-user/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询店员
|
||||
*/
|
||||
export async function getShopStoreUser(id: number) {
|
||||
const res = await request.get<ApiResult<ShopStoreUser>>(
|
||||
'/shop/shop-store-user/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
36
src/api/shop/shopStoreUser/model/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 店员
|
||||
*/
|
||||
export interface ShopStoreUser {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 配送点ID(shop_dealer.id)
|
||||
storeId?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店员搜索条件
|
||||
*/
|
||||
export interface ShopStoreUserParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
storeId?: number;
|
||||
userId?: number;
|
||||
isDelete?: number;
|
||||
}
|
||||
101
src/api/shop/shopWarehouse/index.ts
Normal file
101
src/api/shop/shopWarehouse/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopWarehouse, ShopWarehouseParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询仓库
|
||||
*/
|
||||
export async function pageShopWarehouse(params: ShopWarehouseParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopWarehouse>>>(
|
||||
'/shop/shop-warehouse/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仓库列表
|
||||
*/
|
||||
export async function listShopWarehouse(params?: ShopWarehouseParam) {
|
||||
const res = await request.get<ApiResult<ShopWarehouse[]>>(
|
||||
'/shop/shop-warehouse',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加仓库
|
||||
*/
|
||||
export async function addShopWarehouse(data: ShopWarehouse) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改仓库
|
||||
*/
|
||||
export async function updateShopWarehouse(data: ShopWarehouse) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除仓库
|
||||
*/
|
||||
export async function removeShopWarehouse(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除仓库
|
||||
*/
|
||||
export async function removeBatchShopWarehouse(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-warehouse/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询仓库
|
||||
*/
|
||||
export async function getShopWarehouse(id: number) {
|
||||
const res = await request.get<ApiResult<ShopWarehouse>>(
|
||||
'/shop/shop-warehouse/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
53
src/api/shop/shopWarehouse/model/index.ts
Normal file
53
src/api/shop/shopWarehouse/model/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 仓库
|
||||
*/
|
||||
export interface ShopWarehouse {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 仓库名称
|
||||
name?: string;
|
||||
// 唯一标识
|
||||
code?: string;
|
||||
// 类型 中心仓,区域仓,门店仓
|
||||
type?: string;
|
||||
// 仓库地址
|
||||
address?: string;
|
||||
// 真实姓名
|
||||
realName?: string;
|
||||
// 联系电话
|
||||
phone?: string;
|
||||
// 省份
|
||||
province?: string;
|
||||
// 城市
|
||||
city: undefined,
|
||||
// 区域
|
||||
region: undefined,
|
||||
// 经纬度
|
||||
lngAndLat?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除
|
||||
isDelete?: number;
|
||||
// 状态
|
||||
status?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓库搜索条件
|
||||
*/
|
||||
export interface ShopWarehouseParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -58,6 +58,7 @@ export default {
|
||||
"gift/detail",
|
||||
"gift/add",
|
||||
"store/verification",
|
||||
"store/orders/index",
|
||||
"theme/index",
|
||||
"poster/poster",
|
||||
"chat/conversation/index",
|
||||
@@ -91,10 +92,18 @@ export default {
|
||||
'comments/index',
|
||||
'search/index']
|
||||
},
|
||||
{
|
||||
"root": "store",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "rider",
|
||||
"pages": [
|
||||
"index"
|
||||
"index",
|
||||
"orders/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -132,7 +132,7 @@ const DealerIndex: React.FC = () => {
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-3">
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
@@ -140,7 +140,7 @@ const DealerIndex: React.FC = () => {
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
@@ -148,7 +148,7 @@ const DealerIndex: React.FC = () => {
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import {Button, Space, Sticky} from '@nutui/nutui-react-taro'
|
||||
import {Button, Sticky, Popup, Cell, CellGroup} from '@nutui/nutui-react-taro'
|
||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {TenantId, TenantName} from "@/config/app";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
@@ -13,6 +13,9 @@ import {View,Text} from '@tarojs/components'
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||
|
||||
const Header = (_: any) => {
|
||||
// 使用新的useShopInfo Hook
|
||||
@@ -25,8 +28,124 @@ const Header = (_: any) => {
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
const [userInfo] = useState<User>()
|
||||
|
||||
// 门店选择:用于首页展示“最近门店”,并在下单时写入订单 storeId
|
||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||
const [stores, setStores] = useState<ShopStore[]>([])
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||
const [userLocation, setUserLocation] = useState<{lng: number; lat: number} | null>(null)
|
||||
|
||||
const getTenantName = () => {
|
||||
return userInfo?.tenantName || '商城名称'
|
||||
return userInfo?.tenantName || TenantName
|
||||
}
|
||||
|
||||
const parseStoreCoords = (s: ShopStore): {lng: number; lat: number} | null => {
|
||||
const raw = (s.lngAndLat || s.location || '').trim()
|
||||
if (!raw) return null
|
||||
|
||||
const parts = raw.split(/[,\s]+/).filter(Boolean)
|
||||
if (parts.length < 2) return null
|
||||
|
||||
const a = parseFloat(parts[0])
|
||||
const b = parseFloat(parts[1])
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null
|
||||
|
||||
// 常见格式是 "lng,lat";这里做一个简单兜底(经度范围更宽)
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||
if (looksLikeLngLat) return {lng: a, lat: b}
|
||||
if (looksLikeLatLng) return {lng: b, lat: a}
|
||||
return null
|
||||
}
|
||||
|
||||
const distanceMeters = (a: {lng: number; lat: number}, b: {lng: number; lat: number}) => {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180
|
||||
const R = 6371000 // meters
|
||||
const dLat = toRad(b.lat - a.lat)
|
||||
const dLng = toRad(b.lng - a.lng)
|
||||
const lat1 = toRad(a.lat)
|
||||
const lat2 = toRad(b.lat)
|
||||
const sin1 = Math.sin(dLat / 2)
|
||||
const sin2 = Math.sin(dLng / 2)
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
||||
}
|
||||
|
||||
const formatDistance = (meters?: number) => {
|
||||
if (meters === undefined || Number.isNaN(meters)) return ''
|
||||
if (meters < 1000) return `${Math.round(meters)}m`
|
||||
return `${(meters / 1000).toFixed(1)}km`
|
||||
}
|
||||
|
||||
const getStoreDistance = (s: ShopStore) => {
|
||||
if (!userLocation) return undefined
|
||||
const coords = parseStoreCoords(s)
|
||||
if (!coords) return undefined
|
||||
return distanceMeters(userLocation, coords)
|
||||
}
|
||||
|
||||
const initStoreSelection = async () => {
|
||||
// 先读取本地已选门店,避免页面首屏抖动
|
||||
const stored = getSelectedStoreFromStorage()
|
||||
if (stored?.id) {
|
||||
setSelectedStore(stored)
|
||||
}
|
||||
|
||||
// 拉取门店列表(失败时允许用户手动重试/继续使用本地门店)
|
||||
let list: ShopStore[] = []
|
||||
try {
|
||||
list = await listShopStore()
|
||||
} catch (e) {
|
||||
console.error('获取门店列表失败:', e)
|
||||
list = []
|
||||
}
|
||||
const usable = (list || []).filter(s => s?.isDelete !== 1)
|
||||
setStores(usable)
|
||||
|
||||
// 尝试获取定位,用于计算最近门店
|
||||
let loc: {lng: number; lat: number} | null = null
|
||||
try {
|
||||
const r = await Taro.getLocation({type: 'gcj02'})
|
||||
loc = {lng: r.longitude, lat: r.latitude}
|
||||
} catch (e) {
|
||||
// 不强制定位授权;无定位时仍允许用户手动选择
|
||||
console.warn('获取定位失败,将不显示最近门店距离:', e)
|
||||
}
|
||||
setUserLocation(loc)
|
||||
|
||||
const ensureStoreDetail = async (s: ShopStore) => {
|
||||
if (!s?.id) return s
|
||||
// 如果后端已经返回默认仓库等字段,就不额外请求
|
||||
if (s.warehouseId) return s
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
return full || s
|
||||
} catch (_e) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// 若用户没有选过门店,则自动选择最近门店(或第一个)
|
||||
const alreadySelected = stored?.id
|
||||
if (alreadySelected || usable.length === 0) return
|
||||
|
||||
let autoPick: ShopStore | undefined
|
||||
if (loc) {
|
||||
autoPick = [...usable]
|
||||
.map(s => {
|
||||
const coords = parseStoreCoords(s)
|
||||
const d = coords ? distanceMeters(loc, coords) : undefined
|
||||
return {s, d}
|
||||
})
|
||||
.sort((x, y) => (x.d ?? Number.POSITIVE_INFINITY) - (y.d ?? Number.POSITIVE_INFINITY))[0]?.s
|
||||
} else {
|
||||
autoPick = usable[0]
|
||||
}
|
||||
|
||||
if (autoPick?.id) {
|
||||
const full = await ensureStoreDetail(autoPick)
|
||||
setSelectedStore(full)
|
||||
saveSelectedStoreToStorage(full)
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
@@ -187,6 +306,7 @@ const Header = (_: any) => {
|
||||
|
||||
useEffect(() => {
|
||||
reload().then()
|
||||
initStoreSelection().then()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -216,31 +336,95 @@ const Header = (_: any) => {
|
||||
onBackClick={() => {
|
||||
}}
|
||||
left={
|
||||
<View
|
||||
style={{display: 'flex', alignItems: 'center', gap: '8px'}}
|
||||
onClick={() => setStorePopupVisible(true)}
|
||||
>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={getWebsiteLogo()}
|
||||
/>
|
||||
<Text className={'text-white'}>
|
||||
{selectedStore?.name || '请选择门店'}
|
||||
</Text>
|
||||
<TriangleDown className={'text-white'} size={9}/>
|
||||
</View>
|
||||
}
|
||||
right={
|
||||
!IsLogin ? (
|
||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
||||
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
||||
<Space>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={getWebsiteLogo()}
|
||||
/>
|
||||
<Text style={{color: '#ffffff'}}>{getTenantName()}</Text>
|
||||
<TriangleDown size={9} className={'text-white'}/>
|
||||
</Space>
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={getWebsiteLogo()}
|
||||
/>
|
||||
<Text className={'text-white'}>{getTenantName()}</Text>
|
||||
<TriangleDown className={'text-white'} size={9}/>
|
||||
</View>
|
||||
)}>
|
||||
<Button
|
||||
size="small"
|
||||
fill="none"
|
||||
style={{color: '#ffffff'}}
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<Text className={'text-white'}>{getTenantName()}</Text>
|
||||
</NavBar>
|
||||
|
||||
<Popup
|
||||
visible={storePopupVisible}
|
||||
position="bottom"
|
||||
style={{height: '70vh'}}
|
||||
onClose={() => setStorePopupVisible(false)}
|
||||
>
|
||||
<View className="p-4">
|
||||
<View className="flex justify-between items-center mb-3">
|
||||
<Text className="text-base font-medium">选择门店</Text>
|
||||
<Text
|
||||
className="text-sm text-gray-500"
|
||||
onClick={() => setStorePopupVisible(false)}
|
||||
>
|
||||
关闭
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-500 mb-2">
|
||||
{userLocation ? '已获取定位,按距离排序' : '未获取定位,可手动选择门店'}
|
||||
</View>
|
||||
|
||||
<CellGroup>
|
||||
{[...stores]
|
||||
.sort((a, b) => (getStoreDistance(a) ?? Number.POSITIVE_INFINITY) - (getStoreDistance(b) ?? Number.POSITIVE_INFINITY))
|
||||
.map((s) => {
|
||||
const d = getStoreDistance(s)
|
||||
const isActive = !!selectedStore?.id && selectedStore.id === s.id
|
||||
return (
|
||||
<Cell
|
||||
key={s.id}
|
||||
title={
|
||||
<View className="flex items-center justify-between">
|
||||
<Text className={isActive ? 'text-green-600' : ''}>{s.name || `门店${s.id}`}</Text>
|
||||
{d !== undefined && <Text className="text-xs text-gray-500">{formatDistance(d)}</Text>}
|
||||
</View>
|
||||
}
|
||||
description={s.address || ''}
|
||||
onClick={async () => {
|
||||
let storeToSave = s
|
||||
if (s?.id) {
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
if (full) storeToSave = full
|
||||
} catch (_e) {
|
||||
// keep base item
|
||||
}
|
||||
}
|
||||
setSelectedStore(storeToSave)
|
||||
saveSelectedStoreToStorage(storeToSave)
|
||||
setStorePopupVisible(false)
|
||||
Taro.showToast({title: '门店已切换', icon: 'success'})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</CellGroup>
|
||||
</View>
|
||||
</Popup>
|
||||
</Sticky>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
People,
|
||||
// AfterSaleService,
|
||||
Logout,
|
||||
ShoppingAdd,
|
||||
Shop,
|
||||
Jdl,
|
||||
Service
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const {logoutUser} = useUser();
|
||||
const {logoutUser, hasRole} = useUser();
|
||||
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
@@ -49,10 +50,49 @@ const UserCell = () => {
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="配送订单" onClick={() => navTo('/rider/index', true)}>
|
||||
|
||||
{hasRole('store') && (
|
||||
<Grid.Item text="门店中心" onClick={() => navTo('/store/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{hasRole('rider') && (
|
||||
<Grid.Item text="配送中心" onClick={() => navTo('/rider/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Jdl color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{(hasRole('staff') || hasRole('admin')) && (
|
||||
<Grid.Item text="门店订单" onClick={() => navTo('/user/store/orders/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
@@ -71,14 +111,6 @@ const UserCell = () => {
|
||||
</Button>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
@@ -111,13 +143,6 @@ const UserCell = () => {
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||
<View className="text-center">
|
||||
@@ -189,4 +214,3 @@ const UserCell = () => {
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
|
||||
|
||||
4
src/rider/orders/index.config.ts
Normal file
4
src/rider/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '配送订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
378
src/rider/orders/index.tsx
Normal file
378
src/rider/orders/index.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Tabs, TabPane, Cell, Space, Button, Dialog, Image, Empty, InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import dayjs from 'dayjs'
|
||||
import {pageShopOrder, updateShopOrder} from '@/api/shop/shopOrder'
|
||||
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
|
||||
import {uploadFile} from '@/api/system/file'
|
||||
|
||||
export default function RiderOrders() {
|
||||
|
||||
const riderId = useMemo(() => {
|
||||
const v = Number(Taro.getStorageSync('UserId'))
|
||||
return Number.isFinite(v) && v > 0 ? v : undefined
|
||||
}, [])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [deliverDialogVisible, setDeliverDialogVisible] = useState(false)
|
||||
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
|
||||
const [deliverOrder, setDeliverOrder] = useState<ShopOrder | null>(null)
|
||||
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||
|
||||
// 前端展示用:后台可配置实际自动确认收货时长
|
||||
const AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK = 24
|
||||
|
||||
const riderTabs = useMemo(
|
||||
() => [
|
||||
{index: 0, title: '全部', statusFilter: -1},
|
||||
{index: 1, title: '配送中', statusFilter: 3}, // 后端:deliveryStatus=20
|
||||
{index: 2, title: '待客户确认', statusFilter: 3}, // 同上,前端再按 sendEndTime 细分
|
||||
{index: 3, title: '已完成', statusFilter: 5}, // 后端:orderStatus=1
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const isAbnormalOrder = (order: ShopOrder) => {
|
||||
const s = order.orderStatus
|
||||
return s === 2 || s === 3 || s === 4 || s === 5 || s === 6 || s === 7
|
||||
}
|
||||
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
if (order.orderStatus === 2) return '已取消'
|
||||
if (order.orderStatus === 3) return '取消中'
|
||||
if (order.orderStatus === 4) return '退款申请中'
|
||||
if (order.orderStatus === 5) return '退款被拒绝'
|
||||
if (order.orderStatus === 6) return '退款成功'
|
||||
if (order.orderStatus === 7) return '客户申请退款'
|
||||
if (!order.payStatus) return '未付款'
|
||||
if (order.orderStatus === 1) return '已完成'
|
||||
|
||||
// 配送员页:用 sendEndTime 表示“已送达收货点”
|
||||
if (order.deliveryStatus === 20) {
|
||||
if (order.sendEndTime) return '待客户确认收货'
|
||||
return '配送中'
|
||||
}
|
||||
if (order.deliveryStatus === 10) return '待发货'
|
||||
if (order.deliveryStatus === 30) return '部分发货'
|
||||
return '处理中'
|
||||
}
|
||||
|
||||
const getOrderStatusColor = (order: ShopOrder) => {
|
||||
if (isAbnormalOrder(order)) return 'text-orange-500'
|
||||
if (order.orderStatus === 1) return 'text-green-600'
|
||||
if (order.sendEndTime) return 'text-purple-600'
|
||||
return 'text-blue-600'
|
||||
}
|
||||
|
||||
const canConfirmDelivered = (order: ShopOrder) => {
|
||||
if (!order.payStatus) return false
|
||||
if (order.orderStatus === 1) return false
|
||||
if (isAbnormalOrder(order)) return false
|
||||
// 只允许在“配送中”阶段确认送达
|
||||
if (order.deliveryStatus !== 20) return false
|
||||
return !order.sendEndTime
|
||||
}
|
||||
|
||||
const filterByTab = useCallback(
|
||||
(orders: ShopOrder[]) => {
|
||||
if (tabIndex === 1) {
|
||||
// 配送中:未确认送达
|
||||
return orders.filter(o => o.deliveryStatus === 20 && !o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1)
|
||||
}
|
||||
if (tabIndex === 2) {
|
||||
// 待客户确认:已确认送达
|
||||
return orders.filter(o => o.deliveryStatus === 20 && !!o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1)
|
||||
}
|
||||
if (tabIndex === 3) {
|
||||
return orders.filter(o => o.orderStatus === 1)
|
||||
}
|
||||
return orders
|
||||
},
|
||||
[tabIndex]
|
||||
)
|
||||
|
||||
const reload = useCallback(
|
||||
async (resetPage = false) => {
|
||||
if (!riderId) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const currentPage = resetPage ? 1 : pageRef.current
|
||||
const currentTab = riderTabs.find(t => t.index === tabIndex) || riderTabs[0]
|
||||
|
||||
const params: ShopOrderParam = {
|
||||
page: currentPage,
|
||||
riderId,
|
||||
statusFilter: currentTab.statusFilter,
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await pageShopOrder(params)
|
||||
const incoming = (res?.list || []) as ShopOrder[]
|
||||
setList(prev => (resetPage ? incoming : prev.concat(incoming)))
|
||||
setHasMore(incoming.length >= 10)
|
||||
pageRef.current = currentPage
|
||||
} catch (e) {
|
||||
console.error('加载配送订单失败:', e)
|
||||
setError('加载失败,请重试')
|
||||
setHasMore(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[riderId, riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
pageRef.current += 1
|
||||
await reload(false)
|
||||
}, [hasMore, loading, reload])
|
||||
|
||||
const openDeliverDialog = (order: ShopOrder) => {
|
||||
setDeliverOrder(order)
|
||||
setDeliverImg(order.sendEndImg)
|
||||
setDeliverDialogVisible(true)
|
||||
}
|
||||
|
||||
const handleChooseDeliverImg = async () => {
|
||||
try {
|
||||
const file = await uploadFile()
|
||||
setDeliverImg(file?.url)
|
||||
} catch (e) {
|
||||
console.error('上传送达照片失败:', e)
|
||||
Taro.showToast({title: '上传失败,请重试', icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelivered = async () => {
|
||||
if (!deliverOrder?.orderId) return
|
||||
if (deliverSubmitting) return
|
||||
setDeliverSubmitting(true)
|
||||
try {
|
||||
await updateShopOrder({
|
||||
orderId: deliverOrder.orderId,
|
||||
// 用于前端/后端识别“配送员已送达收货点”
|
||||
sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
sendEndImg: deliverImg,
|
||||
})
|
||||
Taro.showToast({title: '已确认送达', icon: 'success'})
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
pageRef.current = 1
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
console.error('确认送达失败:', e)
|
||||
Taro.showToast({title: '确认送达失败', icon: 'none'})
|
||||
} finally {
|
||||
setDeliverSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current = 1
|
||||
void reload(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tabIndex, riderId])
|
||||
|
||||
if (!riderId) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<Text>请先登录</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const displayList = filterByTab(list)
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
|
||||
<View>
|
||||
<Tabs
|
||||
align="left"
|
||||
className="fixed left-0"
|
||||
style={{zIndex: 998, borderBottom: '1px solid #e5e5e5'}}
|
||||
tabStyle={{backgroundColor: '#ffffff'}}
|
||||
value={tabIndex}
|
||||
onChange={(paneKey) => setTabIndex(Number(paneKey))}
|
||||
>
|
||||
{riderTabs.map(t => (
|
||||
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title}></TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<View style={{height: '84vh', width: '100%', padding: '0', overflowY: 'auto', overflowX: 'hidden'}} id="rider-order-scroll">
|
||||
{error ? (
|
||||
<View className="flex flex-col items-center justify-center h-64">
|
||||
<Text className="text-gray-500 mb-4">{error}</Text>
|
||||
<Button size="small" type="primary" onClick={() => reload(true)}>
|
||||
重新加载
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="rider-order-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
loadingText={<>加载中</>}
|
||||
loadMoreText={
|
||||
displayList.length === 0 ? (
|
||||
<Empty style={{backgroundColor: 'transparent'}} description="暂无配送订单"/>
|
||||
) : (
|
||||
<View className="h-24">没有更多了</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayList.map((o, idx) => {
|
||||
const phoneToCall = o.phone || o.mobile
|
||||
const flow1Done = !!o.riderId
|
||||
const flow2Done = o.deliveryStatus === 20 || o.deliveryStatus === 30
|
||||
const flow3Done = !!o.sendEndTime
|
||||
const flow4Done = o.orderStatus === 1
|
||||
|
||||
const autoConfirmAt = o.sendEndTime
|
||||
? dayjs(o.sendEndTime).add(AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK, 'hour')
|
||||
: null
|
||||
const autoConfirmLeftMin = autoConfirmAt ? autoConfirmAt.diff(dayjs(), 'minute') : null
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={`${o.orderId || idx}`}
|
||||
style={{padding: '16px'}}
|
||||
onClick={() => o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})}
|
||||
>
|
||||
<View className="w-full">
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-gray-800 font-bold text-sm">{o.orderNo || `订单#${o.orderId}`}</Text>
|
||||
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-gray-400 text-xs mt-1">
|
||||
下单时间:{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 bg-white rounded-lg">
|
||||
<View className="text-sm text-gray-700">
|
||||
<Text className="text-gray-500">收货点:</Text>
|
||||
<Text>{o.selfTakeMerchantName || o.address || '-'}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">客户:</Text>
|
||||
<Text>{o.realName || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">金额:</Text>
|
||||
<Text>¥{o.payPrice || o.totalPrice || '-'}</Text>
|
||||
<Text className="text-gray-500 ml-3">数量:</Text>
|
||||
<Text>{o.totalNum ?? '-'}</Text>
|
||||
</View>
|
||||
{o.sendEndTime && (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">送达时间:</Text>
|
||||
<Text>{dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 配送流程 */}
|
||||
<View className="mt-3 bg-gray-50 rounded-lg p-2 text-xs">
|
||||
<Text className="text-gray-600">流程:</Text>
|
||||
<Text className={flow1Done ? 'text-green-600 font-medium' : 'text-gray-400'}>1 派单</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow2Done ? (flow3Done ? 'text-green-600 font-medium' : 'text-blue-600 font-medium') : 'text-gray-400'}>2 配送中</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow3Done ? (flow4Done ? 'text-green-600 font-medium' : 'text-purple-600 font-medium') : 'text-gray-400'}>3 送达收货点</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 客户确认收货</Text>
|
||||
|
||||
{o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && (
|
||||
<View className="mt-1 text-gray-500">
|
||||
若客户未确认,预计 {autoConfirmAt.format('YYYY-MM-DD HH:mm')} 自动确认收货(以后台配置为准)
|
||||
{typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex justify-end">
|
||||
<Space>
|
||||
{!!phoneToCall && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({phoneNumber: phoneToCall})
|
||||
}}
|
||||
>
|
||||
联系客户
|
||||
</Button>
|
||||
)}
|
||||
{canConfirmDelivered(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o)
|
||||
}}
|
||||
>
|
||||
确认送达
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog
|
||||
title="确认送达"
|
||||
visible={deliverDialogVisible}
|
||||
confirmText={deliverSubmitting ? '提交中...' : '确认送达'}
|
||||
cancelText="取消"
|
||||
onConfirm={handleConfirmDelivered}
|
||||
onCancel={() => {
|
||||
if (deliverSubmitting) return
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
}}
|
||||
>
|
||||
<View className="text-sm text-gray-700">
|
||||
<View>到达收货点后,可选拍照留存,再点确认送达。</View>
|
||||
<View className="mt-3">
|
||||
<Button size="small" onClick={handleChooseDeliverImg}>
|
||||
{deliverImg ? '重新拍照/上传' : '拍照/上传(选填)'}
|
||||
</Button>
|
||||
</View>
|
||||
{deliverImg && (
|
||||
<View className="mt-3">
|
||||
<Image src={deliverImg} width="100%" height="120" />
|
||||
<View className="mt-2 flex justify-end">
|
||||
<Button size="small" onClick={() => setDeliverImg(undefined)}>
|
||||
移除照片
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Dialog>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
@@ -13,6 +13,7 @@ import './index.scss'
|
||||
const OrderDetail = () => {
|
||||
const [order, setOrder] = useState<ShopOrder | null>(null);
|
||||
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
|
||||
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const orderId = router?.params?.orderId;
|
||||
|
||||
@@ -67,6 +68,25 @@ const OrderDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 确认收货(客户)
|
||||
const handleConfirmReceive = async () => {
|
||||
if (!order?.orderId) return
|
||||
try {
|
||||
setConfirmReceiveDialogVisible(false)
|
||||
await updateShopOrder({
|
||||
orderId: order.orderId,
|
||||
deliveryStatus: order.deliveryStatus, // 10未发货 20已发货 30部分发货
|
||||
orderStatus: 1 // 已完成
|
||||
})
|
||||
Taro.showToast({title: '确认收货成功', icon: 'success'})
|
||||
setOrder(prev => (prev ? {...prev, orderStatus: 1} : prev))
|
||||
} catch (e) {
|
||||
console.error('确认收货失败:', e)
|
||||
Taro.showToast({title: '确认收货失败', icon: 'none'})
|
||||
setConfirmReceiveDialogVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
// 优先检查订单状态
|
||||
if (order.orderStatus === 2) return '已取消';
|
||||
@@ -81,8 +101,15 @@ const OrderDetail = () => {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return '待发货';
|
||||
if (order.deliveryStatus === 20) return '待收货';
|
||||
if (order.deliveryStatus === 30) return '已收货';
|
||||
if (order.deliveryStatus === 20) {
|
||||
// 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据
|
||||
if (order.riderId) {
|
||||
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
|
||||
return '配送中';
|
||||
}
|
||||
return '待收货';
|
||||
}
|
||||
if (order.deliveryStatus === 30) return '部分发货';
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return '已完成';
|
||||
@@ -133,6 +160,15 @@ const OrderDetail = () => {
|
||||
return <div>加载中...</div>;
|
||||
}
|
||||
|
||||
const currentUserId = Number(Taro.getStorageSync('UserId'))
|
||||
const isOwner = !!currentUserId && currentUserId === order.userId
|
||||
const canConfirmReceive =
|
||||
isOwner &&
|
||||
order.payStatus &&
|
||||
order.orderStatus !== 1 &&
|
||||
order.deliveryStatus === 20 &&
|
||||
(!order.riderId || !!order.sendEndTime)
|
||||
|
||||
return (
|
||||
<div className={'order-detail-page'}>
|
||||
{/* 支付倒计时显示 - 详情页实时更新 */}
|
||||
@@ -190,11 +226,25 @@ const OrderDetail = () => {
|
||||
{!order.payStatus && <Button onClick={() => console.log('取消订单')}>取消订单</Button>}
|
||||
{!order.payStatus && <Button type="primary" onClick={() => console.log('立即支付')}>立即支付</Button>}
|
||||
{order.orderStatus === 1 && <Button onClick={handleApplyRefund}>申请退款</Button>}
|
||||
{order.deliveryStatus === 20 &&
|
||||
<Button type="primary" onClick={() => console.log('确认收货')}>确认收货</Button>}
|
||||
{canConfirmReceive && (
|
||||
<Button type="primary" onClick={() => setConfirmReceiveDialogVisible(true)}>
|
||||
确认收货
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog
|
||||
title="确认收货"
|
||||
visible={confirmReceiveDialogVisible}
|
||||
confirmText="确认收货"
|
||||
cancelText="我再想想"
|
||||
onConfirm={handleConfirmReceive}
|
||||
onCancel={() => setConfirmReceiveDialogVisible(false)}
|
||||
>
|
||||
确定已经收到商品了吗?确认后订单将完成。
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
3
src/store/index.config.ts
Normal file
3
src/store/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '门店中心'
|
||||
})
|
||||
0
src/store/index.scss
Normal file
0
src/store/index.scss
Normal file
281
src/store/index.tsx
Normal file
281
src/store/index.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Avatar, Button, ConfigProvider, Grid} from '@nutui/nutui-react-taro'
|
||||
import {Location, Scan, Shop, Shopping, User} from '@nutui/icons-react-taro'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {useThemeStyles} from '@/hooks/useTheme'
|
||||
import {useUser} from '@/hooks/useUser'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
import {getShopStore} from '@/api/shop/shopStore'
|
||||
import type {ShopStore as ShopStoreModel} from '@/api/shop/shopStore/model'
|
||||
|
||||
const StoreIndex: React.FC = () => {
|
||||
const themeStyles = useThemeStyles()
|
||||
const {isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName, getRoleName, hasRole} = useUser()
|
||||
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStoreModel | null>(getSelectedStoreFromStorage())
|
||||
const [store, setStore] = useState<ShopStoreModel | null>(selectedStore)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const storeId = boundStoreId || selectedStore?.id
|
||||
|
||||
const parseStoreCoords = (s: ShopStoreModel): {lng: number; lat: number} | null => {
|
||||
const raw = (s.lngAndLat || s.location || '').trim()
|
||||
if (!raw) return null
|
||||
|
||||
const parts = raw.split(/[,\s]+/).filter(Boolean)
|
||||
if (parts.length < 2) return null
|
||||
|
||||
const a = parseFloat(parts[0])
|
||||
const b = parseFloat(parts[1])
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null
|
||||
|
||||
// 常见格式是 "lng,lat";这里做一个简单兜底
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||
if (looksLikeLngLat) return {lng: a, lat: b}
|
||||
if (looksLikeLatLng) return {lng: b, lat: a}
|
||||
return null
|
||||
}
|
||||
|
||||
const navigateToPage = (url: string) => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.showToast({title: '请先登录', icon: 'none', duration: 1500})
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({url})
|
||||
}
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const latestSelectedStore = getSelectedStoreFromStorage()
|
||||
setSelectedStore(latestSelectedStore)
|
||||
|
||||
const userIdRaw = Number(Taro.getStorageSync('UserId'))
|
||||
const userId = Number.isFinite(userIdRaw) && userIdRaw > 0 ? userIdRaw : undefined
|
||||
|
||||
let foundStoreId: number | undefined = undefined
|
||||
if (userId) {
|
||||
// 优先按“店员绑定关系”确定门店归属
|
||||
try {
|
||||
const list = await listShopStoreUser({userId})
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
foundStoreId = first?.storeId
|
||||
setBoundStoreId(foundStoreId)
|
||||
} catch {
|
||||
// fallback to SelectedStore
|
||||
foundStoreId = undefined
|
||||
setBoundStoreId(undefined)
|
||||
}
|
||||
} else {
|
||||
foundStoreId = undefined
|
||||
setBoundStoreId(undefined)
|
||||
}
|
||||
|
||||
const nextStoreId = (foundStoreId || latestSelectedStore?.id)
|
||||
if (!nextStoreId) {
|
||||
setStore(latestSelectedStore)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取门店详情(用于展示门店名称/地址/仓库等)
|
||||
const full = await getShopStore(nextStoreId)
|
||||
setStore(full || (latestSelectedStore?.id === nextStoreId ? latestSelectedStore : ({id: nextStoreId} as ShopStoreModel)))
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || '获取门店信息失败'
|
||||
setError(msg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 返回/切换到该页面时,同步最新的已选门店与绑定门店
|
||||
useDidShow(() => {
|
||||
refresh().catch(() => {})
|
||||
})
|
||||
|
||||
const openStoreLocation = () => {
|
||||
if (!store?.id) {
|
||||
return Taro.showToast({title: '请先选择门店', icon: 'none'})
|
||||
}
|
||||
const coords = parseStoreCoords(store)
|
||||
if (!coords) {
|
||||
return Taro.showToast({title: '门店未配置定位', icon: 'none'})
|
||||
}
|
||||
Taro.openLocation({
|
||||
latitude: coords.lat,
|
||||
longitude: coords.lng,
|
||||
name: store.name || '门店',
|
||||
address: store.address || ''
|
||||
})
|
||||
}
|
||||
|
||||
if (!isLoggedIn && !userLoading) {
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen p-4">
|
||||
<View className="bg-white rounded-xl p-4">
|
||||
<Text className="text-gray-700">请先登录后再进入门店中心</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
{/* 头部信息 */}
|
||||
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
|
||||
<View
|
||||
className="absolute w-32 h-32 rounded-full"
|
||||
style={{backgroundColor: 'rgba(255, 255, 255, 0.1)', top: '-16px', right: '-16px'}}
|
||||
></View>
|
||||
<View
|
||||
className="absolute w-24 h-24 rounded-full"
|
||||
style={{backgroundColor: 'rgba(255, 255, 255, 0.08)', bottom: '-12px', left: '-12px'}}
|
||||
></View>
|
||||
<View
|
||||
className="absolute w-16 h-16 rounded-full"
|
||||
style={{backgroundColor: 'rgba(255, 255, 255, 0.05)', top: '60px', left: '120px'}}
|
||||
></View>
|
||||
|
||||
<View className="flex items-center justify-between relative z-10">
|
||||
<Avatar
|
||||
size="50"
|
||||
src={getAvatarUrl()}
|
||||
icon={<User />}
|
||||
className="mr-4"
|
||||
style={{border: '2px solid rgba(255, 255, 255, 0.3)'}}
|
||||
/>
|
||||
<View className="flex-1 flex-col">
|
||||
<View className="text-white text-lg font-bold mb-1">
|
||||
{getDisplayName()}
|
||||
</View>
|
||||
<View className="text-sm" style={{color: 'rgba(255, 255, 255, 0.8)'}}>
|
||||
{hasRole('store') ? '门店' : hasRole('rider') ? '配送员' : getRoleName()}
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.18)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255, 255, 255, 0.25)'
|
||||
}}
|
||||
loading={loading}
|
||||
onClick={refresh}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 门店信息 */}
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10 bg-white">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="font-semibold text-gray-800">当前门店</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm"
|
||||
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
|
||||
>
|
||||
切换门店
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
未选择门店,请先去首页选择门店。
|
||||
</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" size="small" onClick={() => Taro.switchTab({url: '/pages/index/index'})}>
|
||||
去首页选择门店
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<View className="text-base font-medium text-gray-900">
|
||||
{store?.name || `门店ID: ${storeId}`}
|
||||
</View>
|
||||
{!!store?.address && (
|
||||
<View className="text-sm text-gray-600 mt-1">
|
||||
{store.address}
|
||||
</View>
|
||||
)}
|
||||
{!!store?.warehouseName && (
|
||||
<View className="text-sm text-gray-500 mt-1">
|
||||
默认仓库:{store.warehouseName}
|
||||
</View>
|
||||
)}
|
||||
{!!error && (
|
||||
<View className="mt-2">
|
||||
<Text className="text-sm text-red-600">{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 功能入口 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="font-semibold mb-4 text-gray-800">门店工具</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="门店订单" onClick={() => navigateToPage('/store/orders/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shopping color="#3b82f6" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="礼品卡核销" onClick={() => navigateToPage('/user/store/verification')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Scan color="#10b981" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="门店导航" onClick={openStoreLocation}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#f59e0b" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="首页选店" onClick={() => Taro.switchTab({url: '/pages/index/index'})}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#8b5cf6" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
|
||||
<View className="h-20"></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default StoreIndex
|
||||
4
src/store/orders/index.config.ts
Normal file
4
src/store/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '门店订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
83
src/store/orders/index.tsx
Normal file
83
src/store/orders/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import OrderList from '@/user/order/components/OrderList'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
|
||||
export default function StoreOrders() {
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
|
||||
const isLoggedIn = useMemo(() => {
|
||||
return !!Taro.getStorageSync('access_token') && !!Taro.getStorageSync('UserId')
|
||||
}, [])
|
||||
|
||||
const selectedStore = useMemo(() => getSelectedStoreFromStorage(), [])
|
||||
const storeId = boundStoreId || selectedStore?.id
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单
|
||||
const userId = Number(Taro.getStorageSync('UserId'))
|
||||
if (!Number.isFinite(userId) || userId <= 0) return
|
||||
listShopStoreUser({userId}).then(list => {
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
if (first?.storeId) setBoundStoreId(first.storeId)
|
||||
}).catch(() => {
|
||||
// fallback to SelectedStore
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-700">请先登录</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" size="small" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
|
||||
<View className="px-3">
|
||||
<View className="bg-white rounded-lg p-3 mb-3">
|
||||
<Text className="text-sm text-gray-600">当前门店:</Text>
|
||||
<Text className="text-base font-medium">
|
||||
{boundStoreId
|
||||
? (selectedStore?.id === boundStoreId ? (selectedStore?.name || `门店ID: ${boundStoreId}`) : `门店ID: ${boundStoreId}`)
|
||||
: (selectedStore?.name || '未选择门店')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-600">
|
||||
请先在首页左上角选择门店,再查看门店订单。
|
||||
</Text>
|
||||
<View className="mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
|
||||
>
|
||||
去首页选择门店
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<OrderList mode="store" baseParams={{storeId}} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -89,6 +89,12 @@ interface OrderListProps {
|
||||
searchParams?: ShopOrderParam;
|
||||
showSearch?: boolean;
|
||||
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
|
||||
// 订单视图模式:用户/门店/骑手
|
||||
mode?: 'user' | 'store' | 'rider';
|
||||
// 固定过滤条件(例如 storeId / riderId),会合并到每次请求里
|
||||
baseParams?: ShopOrderParam;
|
||||
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
function OrderList(props: OrderListProps) {
|
||||
@@ -115,6 +121,7 @@ function OrderList(props: OrderListProps) {
|
||||
const [orderToCancel, setOrderToCancel] = useState<ShopOrder | null>(null)
|
||||
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
||||
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
|
||||
const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider'
|
||||
|
||||
// 获取订单状态文本
|
||||
const getOrderStatusText = (order: ShopOrder) => {
|
||||
@@ -131,8 +138,14 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return '待发货';
|
||||
if (order.deliveryStatus === 20) return '待收货';
|
||||
if (order.deliveryStatus === 30) return '已完成';
|
||||
if (order.deliveryStatus === 20) {
|
||||
// 若订单没有配送员,沿用原“待收货”语义
|
||||
if (!order.riderId) return '待收货';
|
||||
// 配送员确认送达后(sendEndTime有值),才进入“待确认收货”
|
||||
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
|
||||
return '配送中';
|
||||
}
|
||||
if (order.deliveryStatus === 30) return '部分发货';
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return '已完成';
|
||||
@@ -155,8 +168,12 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
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.deliveryStatus === 20) {
|
||||
if (!order.riderId) return 'text-purple-500'; // 待收货
|
||||
if (order.sendEndTime && order.orderStatus !== 1) return 'text-purple-500'; // 待确认收货
|
||||
return 'text-blue-500'; // 配送中
|
||||
}
|
||||
if (order.deliveryStatus === 30) return 'text-blue-500'; // 部分发货
|
||||
|
||||
// 最后检查订单完成状态
|
||||
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
|
||||
@@ -167,9 +184,13 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 使用后端统一的 statusFilter 进行筛选
|
||||
const getOrderStatusParams = (index: string | number) => {
|
||||
let params: ShopOrderParam = {};
|
||||
// 添加用户ID过滤
|
||||
params.userId = Taro.getStorageSync('UserId');
|
||||
let params: ShopOrderParam = {
|
||||
...(props.baseParams || {})
|
||||
};
|
||||
// 默认是用户视图:添加 userId 过滤;门店/骑手视图由 baseParams 控制
|
||||
if (!props.mode || props.mode === 'user') {
|
||||
params.userId = Taro.getStorageSync('UserId');
|
||||
}
|
||||
|
||||
// 获取当前tab的statusFilter配置
|
||||
const currentTab = tabs.find(tab => tab.index === Number(index));
|
||||
@@ -190,7 +211,7 @@ function OrderList(props: OrderListProps) {
|
||||
// 合并搜索条件,tab的statusFilter优先级更高
|
||||
const searchConditions: any = {
|
||||
page: currentPage,
|
||||
userId: statusParams.userId, // 用户ID
|
||||
...statusParams,
|
||||
...props.searchParams, // 搜索关键词等其他条件
|
||||
};
|
||||
|
||||
@@ -285,7 +306,7 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
await updateShopOrder({
|
||||
...orderToConfirmReceive,
|
||||
deliveryStatus: 20, // 已收货
|
||||
deliveryStatus: orderToConfirmReceive.deliveryStatus, // 10未发货 20已发货 30部分发货(收货由orderStatus控制)
|
||||
orderStatus: 1 // 已完成
|
||||
});
|
||||
|
||||
@@ -764,6 +785,7 @@ function OrderList(props: OrderListProps) {
|
||||
<Text className={'w-full text-right'}>实付金额:¥{item.payPrice}</Text>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{!isReadOnly && (
|
||||
<Space className={'btn flex justify-end'}>
|
||||
{/* 待付款状态:显示取消订单和立即支付 */}
|
||||
{(!item.payStatus) && item.orderStatus !== 2 && (
|
||||
@@ -788,7 +810,7 @@ function OrderList(props: OrderListProps) {
|
||||
)}
|
||||
|
||||
{/* 待收货状态:显示查看物流和确认收货 */}
|
||||
{item.deliveryStatus === 20 && item.orderStatus !== 2 && (
|
||||
{item.deliveryStatus === 20 && (!item.riderId || !!item.sendEndTime) && item.orderStatus !== 2 && (
|
||||
<Space>
|
||||
<Button size={'small'} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -837,6 +859,7 @@ function OrderList(props: OrderListProps) {
|
||||
}}>再次购买</Button>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
|
||||
4
src/user/store/orders/index.config.ts
Normal file
4
src/user/store/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '门店订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
73
src/user/store/orders/index.tsx
Normal file
73
src/user/store/orders/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {NavBar, Button} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft} from '@nutui/icons-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import OrderList from '@/user/order/components/OrderList'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
|
||||
export default function StoreOrders() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0)
|
||||
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
const store = useMemo(() => getSelectedStoreFromStorage(), [])
|
||||
const storeId = boundStoreId || store?.id
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => setStatusBarHeight(res.statusBarHeight ?? 0)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单
|
||||
const userId = Number(Taro.getStorageSync('UserId'))
|
||||
if (!Number.isFinite(userId) || userId <= 0) return
|
||||
listShopStoreUser({userId}).then(list => {
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
if (first?.storeId) setBoundStoreId(first.storeId)
|
||||
}).catch(() => {
|
||||
// fallback to SelectedStore
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<View style={{height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}></View>
|
||||
<NavBar
|
||||
fixed
|
||||
style={{marginTop: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}
|
||||
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
|
||||
>
|
||||
<span>门店订单</span>
|
||||
</NavBar>
|
||||
|
||||
<View className="pt-14 px-3">
|
||||
<View className="bg-white rounded-lg p-3 mb-3">
|
||||
<Text className="text-sm text-gray-600">当前门店:</Text>
|
||||
<Text className="text-base font-medium">{store?.name || (boundStoreId ? `门店ID: ${boundStoreId}` : '未选择门店')}</Text>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-600">
|
||||
请先在首页左上角选择门店,再查看门店订单。
|
||||
</Text>
|
||||
<View className="mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
|
||||
>
|
||||
去首页选择门店
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<OrderList mode="store" baseParams={{storeId}} readOnly />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { createOrder, WxPayResult } from '@/api/shop/shopOrder';
|
||||
import { OrderCreateRequest } from '@/api/shop/shopOrder/model';
|
||||
import { getSelectedStoreFromStorage, getSelectedStoreIdFromStorage } from '@/utils/storeSelection';
|
||||
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
|
||||
import type { ShopWarehouse } from '@/api/shop/shopWarehouse/model';
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 支付类型枚举
|
||||
@@ -24,6 +28,9 @@ export interface PaymentCallback {
|
||||
* 统一支付处理类
|
||||
*/
|
||||
export class PaymentHandler {
|
||||
// 简单缓存,避免频繁请求(小程序单次运行生命周期内有效)
|
||||
private static storeRidersCache = new Map<number, ShopStoreRider[]>();
|
||||
private static warehousesCache: ShopWarehouse[] | null = null;
|
||||
|
||||
/**
|
||||
* 执行支付
|
||||
@@ -39,6 +46,36 @@ export class PaymentHandler {
|
||||
Taro.showLoading({ title: '支付中...' });
|
||||
|
||||
try {
|
||||
// 若调用方未指定门店,则自动注入“已选门店”,用于订单门店归属/统计。
|
||||
if (orderData.storeId === undefined || orderData.storeId === null) {
|
||||
const storeId = getSelectedStoreIdFromStorage();
|
||||
if (storeId) {
|
||||
orderData.storeId = storeId;
|
||||
}
|
||||
}
|
||||
if (!orderData.storeName) {
|
||||
const store = getSelectedStoreFromStorage();
|
||||
if (store?.name) {
|
||||
orderData.storeName = store.name;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动派单:按门店骑手优先级(dispatchPriority)选择 riderId(不覆盖手动指定)
|
||||
if ((orderData.riderId === undefined || orderData.riderId === null) && orderData.storeId) {
|
||||
const riderUserId = await this.pickRiderUserIdForStore(orderData.storeId);
|
||||
if (riderUserId) {
|
||||
orderData.riderId = riderUserId;
|
||||
}
|
||||
}
|
||||
|
||||
// 仓库选择:若未指定 warehouseId,则按“离门店最近”兜底选择一个(不覆盖手动指定)
|
||||
if ((orderData.warehouseId === undefined || orderData.warehouseId === null) && orderData.storeId) {
|
||||
const warehouseId = await this.pickWarehouseIdForStore(orderData.storeId);
|
||||
if (warehouseId) {
|
||||
orderData.warehouseId = warehouseId;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置支付类型
|
||||
orderData.payType = paymentType;
|
||||
|
||||
@@ -119,6 +156,127 @@ export class PaymentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static parseLngLat(raw: string | undefined): { lng: number; lat: number } | null {
|
||||
const text = (raw || '').trim();
|
||||
if (!text) return null;
|
||||
const parts = text.split(/[,\s]+/).filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
const a = parseFloat(parts[0]);
|
||||
const b = parseFloat(parts[1]);
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90;
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180;
|
||||
if (looksLikeLngLat) return { lng: a, lat: b };
|
||||
if (looksLikeLatLng) return { lng: b, lat: a };
|
||||
return null;
|
||||
}
|
||||
|
||||
private static distanceMeters(a: { lng: number; lat: number }, b: { lng: number; lat: number }) {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180;
|
||||
const R = 6371000;
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLng = toRad(b.lng - a.lng);
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
const sin1 = Math.sin(dLat / 2);
|
||||
const sin2 = Math.sin(dLng / 2);
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2;
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||
}
|
||||
|
||||
private static async getRidersForStore(storeId: number): Promise<ShopStoreRider[]> {
|
||||
const cached = this.storeRidersCache.get(storeId);
|
||||
if (cached) return cached;
|
||||
|
||||
// 后端字段可能叫 dealerId 或 storeId,这里都带上,服务端忽略未知字段即可。
|
||||
// 这里做一次路径兼容(camel vs kebab),避免接口路径不一致导致整单失败。
|
||||
const list = await this.listByCompatEndpoint<ShopStoreRider>(
|
||||
['/shop/shopStoreRider', '/shop/shop-store-rider'],
|
||||
{
|
||||
dealerId: storeId,
|
||||
storeId: storeId,
|
||||
status: 1
|
||||
}
|
||||
);
|
||||
|
||||
const usable = (list || []).filter(r => r?.isDelete !== 1 && (r.status === undefined || r.status === 1));
|
||||
this.storeRidersCache.set(storeId, usable);
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static async pickRiderUserIdForStore(storeId: number): Promise<number | undefined> {
|
||||
const riders = await this.getRidersForStore(storeId);
|
||||
if (!riders.length) return undefined;
|
||||
|
||||
// 优先:启用 + 在线 + 自动派单,再按 dispatchPriority 由高到低
|
||||
const score = (r: ShopStoreRider) => {
|
||||
const enabled = (r.status === undefined || r.status === 1) ? 1 : 0;
|
||||
const online = r.workStatus === 1 ? 1 : 0;
|
||||
const auto = r.autoDispatchEnabled === 1 ? 1 : 0;
|
||||
const p = typeof r.dispatchPriority === 'number' ? r.dispatchPriority : 0;
|
||||
return enabled * 1000 + online * 100 + auto * 10 + p;
|
||||
};
|
||||
|
||||
const sorted = [...riders].sort((a, b) => score(b) - score(a));
|
||||
return sorted[0]?.userId;
|
||||
}
|
||||
|
||||
private static async getWarehouses(): Promise<ShopWarehouse[]> {
|
||||
if (this.warehousesCache) return this.warehousesCache;
|
||||
const list = await this.listByCompatEndpoint<ShopWarehouse>(
|
||||
['/shop/shopWarehouse', '/shop/shop-warehouse'],
|
||||
{}
|
||||
);
|
||||
const usable = (list || []).filter(w => w?.isDelete !== 1 && (w.status === undefined || w.status === 1));
|
||||
this.warehousesCache = usable;
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static async pickWarehouseIdForStore(storeId: number): Promise<number | undefined> {
|
||||
const store = getSelectedStoreFromStorage();
|
||||
if (!store?.id || store.id !== storeId) return undefined;
|
||||
|
||||
// 一门店一默认仓库:优先使用门店自带的 warehouseId
|
||||
if (store.warehouseId) return store.warehouseId;
|
||||
|
||||
const storeCoords = this.parseLngLat(store.lngAndLat || store.location);
|
||||
if (!storeCoords) return undefined;
|
||||
|
||||
const warehouses = await this.getWarehouses();
|
||||
if (!warehouses.length) return undefined;
|
||||
|
||||
// 优先选择“门店仓”,否则选最近的任意仓库
|
||||
const candidates = warehouses.filter(w => w.type?.includes('门店') || w.type?.includes('门店仓'));
|
||||
const list = candidates.length ? candidates : warehouses;
|
||||
|
||||
const withDistance = list
|
||||
.map(w => {
|
||||
const coords = this.parseLngLat(w.lngAndLat);
|
||||
if (!coords) return { w, d: Number.POSITIVE_INFINITY };
|
||||
return { w, d: this.distanceMeters(storeCoords, coords) };
|
||||
})
|
||||
.sort((a, b) => a.d - b.d);
|
||||
|
||||
return withDistance[0]?.w?.id;
|
||||
}
|
||||
|
||||
private static async listByCompatEndpoint<T>(
|
||||
urls: string[],
|
||||
params: Record<string, any>
|
||||
): Promise<T[]> {
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res: any = await (request as any).get(url, params, { showError: false });
|
||||
if (res?.code === 0 && Array.isArray(res?.data)) {
|
||||
return res.data as T[];
|
||||
}
|
||||
} catch (_e) {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信支付
|
||||
*/
|
||||
|
||||
27
src/utils/storeSelection.ts
Normal file
27
src/utils/storeSelection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import type { ShopStore } from '@/api/shop/shopStore/model';
|
||||
|
||||
export const SELECTED_STORE_STORAGE_KEY = 'SelectedStore';
|
||||
|
||||
export function getSelectedStoreFromStorage(): ShopStore | null {
|
||||
try {
|
||||
const raw = Taro.getStorageSync(SELECTED_STORE_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
return (typeof raw === 'string' ? JSON.parse(raw) : raw) as ShopStore;
|
||||
} catch (_e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSelectedStoreToStorage(store: ShopStore | null) {
|
||||
if (!store) {
|
||||
Taro.removeStorageSync(SELECTED_STORE_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
Taro.setStorageSync(SELECTED_STORE_STORAGE_KEY, store);
|
||||
}
|
||||
|
||||
export function getSelectedStoreIdFromStorage(): number | undefined {
|
||||
return getSelectedStoreFromStorage()?.id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user