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 - 请根据实际情况修改
|
// 租户ID - 请根据实际情况修改
|
||||||
export const TenantId = '10584';
|
export const TenantId = '10584';
|
||||||
|
// 租户名称
|
||||||
|
export const TenantName = '桂乐淘';
|
||||||
// 接口地址 - 请根据实际情况修改
|
// 接口地址 - 请根据实际情况修改
|
||||||
export const BaseUrl = API_BASE_URL;
|
export const BaseUrl = API_BASE_URL;
|
||||||
// 当前版本
|
// 当前版本
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ export interface ShopOrder {
|
|||||||
merchantName?: string;
|
merchantName?: string;
|
||||||
// 商户编号
|
// 商户编号
|
||||||
merchantCode?: string;
|
merchantCode?: string;
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 归属门店名称
|
||||||
|
storeName?: string;
|
||||||
|
// 配送员用户ID(优先级派单)
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
// 使用的优惠券id
|
// 使用的优惠券id
|
||||||
couponId?: number;
|
couponId?: number;
|
||||||
// 使用的会员卡id
|
// 使用的会员卡id
|
||||||
@@ -61,6 +69,8 @@ export interface ShopOrder {
|
|||||||
sendStartTime?: string;
|
sendStartTime?: string;
|
||||||
// 配送结束时间
|
// 配送结束时间
|
||||||
sendEndTime?: string;
|
sendEndTime?: string;
|
||||||
|
// 配送员送达拍照(选填)
|
||||||
|
sendEndImg?: string;
|
||||||
// 发货店铺id
|
// 发货店铺id
|
||||||
expressMerchantId?: number;
|
expressMerchantId?: number;
|
||||||
// 发货店铺
|
// 发货店铺
|
||||||
@@ -165,6 +175,14 @@ export interface OrderGoodsItem {
|
|||||||
export interface OrderCreateRequest {
|
export interface OrderCreateRequest {
|
||||||
// 商品信息列表
|
// 商品信息列表
|
||||||
goodsItems: OrderGoodsItem[];
|
goodsItems: OrderGoodsItem[];
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 归属门店名称(可选)
|
||||||
|
storeName?: string;
|
||||||
|
// 配送员用户ID(优先级派单)
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
// 收货地址ID
|
// 收货地址ID
|
||||||
addressId?: number;
|
addressId?: number;
|
||||||
// 支付方式
|
// 支付方式
|
||||||
@@ -197,6 +215,14 @@ export interface OrderGoodsItem {
|
|||||||
export interface OrderCreateRequest {
|
export interface OrderCreateRequest {
|
||||||
// 商品信息列表
|
// 商品信息列表
|
||||||
goodsItems: OrderGoodsItem[];
|
goodsItems: OrderGoodsItem[];
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 归属门店名称(可选)
|
||||||
|
storeName?: string;
|
||||||
|
// 配送员用户ID(优先级派单)
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
// 收货地址ID
|
// 收货地址ID
|
||||||
addressId?: number;
|
addressId?: number;
|
||||||
// 支付方式
|
// 支付方式
|
||||||
@@ -223,6 +249,12 @@ export interface ShopOrderParam extends PageParam {
|
|||||||
payType?: number;
|
payType?: number;
|
||||||
isInvoice?: boolean;
|
isInvoice?: boolean;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
// 归属门店ID(shop_store.id)
|
||||||
|
storeId?: number;
|
||||||
|
// 配送员用户ID
|
||||||
|
riderId?: number;
|
||||||
|
// 发货仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
deliveryStatus?: number;
|
deliveryStatus?: number;
|
||||||
statusFilter?: 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/detail",
|
||||||
"gift/add",
|
"gift/add",
|
||||||
"store/verification",
|
"store/verification",
|
||||||
|
"store/orders/index",
|
||||||
"theme/index",
|
"theme/index",
|
||||||
"poster/poster",
|
"poster/poster",
|
||||||
"chat/conversation/index",
|
"chat/conversation/index",
|
||||||
@@ -91,10 +92,18 @@ export default {
|
|||||||
'comments/index',
|
'comments/index',
|
||||||
'search/index']
|
'search/index']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"root": "store",
|
||||||
|
"pages": [
|
||||||
|
"index",
|
||||||
|
"orders/index"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"root": "rider",
|
"root": "rider",
|
||||||
"pages": [
|
"pages": [
|
||||||
"index"
|
"index",
|
||||||
|
"orders/index"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="grid grid-cols-3 gap-3">
|
<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
|
background: businessGradients.money.available
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
@@ -140,7 +140,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||||
</View>
|
</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
|
background: businessGradients.money.frozen
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
@@ -148,7 +148,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||||
</View>
|
</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
|
background: businessGradients.money.total
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Taro from '@tarojs/taro';
|
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 {TriangleDown} from '@nutui/icons-react-taro'
|
||||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
||||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||||
import {TenantId} from "@/config/app";
|
import {TenantId, TenantName} from "@/config/app";
|
||||||
import {getOrganization} from "@/api/system/organization";
|
import {getOrganization} from "@/api/system/organization";
|
||||||
import {myUserVerify} from "@/api/system/userVerify";
|
import {myUserVerify} from "@/api/system/userVerify";
|
||||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||||
@@ -13,6 +13,9 @@ import {View,Text} from '@tarojs/components'
|
|||||||
import MySearch from "./MySearch";
|
import MySearch from "./MySearch";
|
||||||
import './Header.scss';
|
import './Header.scss';
|
||||||
import {User} from "@/api/system/user/model";
|
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) => {
|
const Header = (_: any) => {
|
||||||
// 使用新的useShopInfo Hook
|
// 使用新的useShopInfo Hook
|
||||||
@@ -25,8 +28,124 @@ const Header = (_: any) => {
|
|||||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||||
const [userInfo] = useState<User>()
|
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 = () => {
|
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 () => {
|
const reload = async () => {
|
||||||
@@ -187,6 +306,7 @@ const Header = (_: any) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload().then()
|
reload().then()
|
||||||
|
initStoreSelection().then()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -216,31 +336,95 @@ const Header = (_: any) => {
|
|||||||
onBackClick={() => {
|
onBackClick={() => {
|
||||||
}}
|
}}
|
||||||
left={
|
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 ? (
|
!IsLogin ? (
|
||||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
<Button
|
||||||
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
size="small"
|
||||||
<Space>
|
fill="none"
|
||||||
<Avatar
|
style={{color: '#ffffff'}}
|
||||||
size="22"
|
open-type="getPhoneNumber"
|
||||||
src={getWebsiteLogo()}
|
onGetPhoneNumber={handleGetPhoneNumber}
|
||||||
/>
|
>
|
||||||
<Text style={{color: '#ffffff'}}>{getTenantName()}</Text>
|
登录
|
||||||
<TriangleDown size={9} className={'text-white'}/>
|
</Button>
|
||||||
</Space>
|
) : null
|
||||||
</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>
|
|
||||||
)}>
|
|
||||||
<Text className={'text-white'}>{getTenantName()}</Text>
|
<Text className={'text-white'}>{getTenantName()}</Text>
|
||||||
</NavBar>
|
</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>
|
</Sticky>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import {
|
|||||||
People,
|
People,
|
||||||
// AfterSaleService,
|
// AfterSaleService,
|
||||||
Logout,
|
Logout,
|
||||||
ShoppingAdd,
|
Shop,
|
||||||
|
Jdl,
|
||||||
Service
|
Service
|
||||||
} from '@nutui/icons-react-taro'
|
} from '@nutui/icons-react-taro'
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
|
|
||||||
const UserCell = () => {
|
const UserCell = () => {
|
||||||
const {logoutUser} = useUser();
|
const {logoutUser, hasRole} = useUser();
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
@@ -49,10 +50,49 @@ const UserCell = () => {
|
|||||||
border: 'none'
|
border: 'none'
|
||||||
} as React.CSSProperties}
|
} 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="text-center">
|
||||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
<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>
|
||||||
</View>
|
</View>
|
||||||
</Grid.Item>
|
</Grid.Item>
|
||||||
@@ -71,14 +111,6 @@ const UserCell = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid.Item>
|
</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)}>
|
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
<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>*/}
|
{/* </View>*/}
|
||||||
{/*</Grid.Item>*/}
|
{/*</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')}>
|
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
@@ -189,4 +214,3 @@ const UserCell = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default 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 {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 Taro from '@tarojs/taro'
|
||||||
import {View} from '@tarojs/components'
|
import {View} from '@tarojs/components'
|
||||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||||
@@ -13,6 +13,7 @@ import './index.scss'
|
|||||||
const OrderDetail = () => {
|
const OrderDetail = () => {
|
||||||
const [order, setOrder] = useState<ShopOrder | null>(null);
|
const [order, setOrder] = useState<ShopOrder | null>(null);
|
||||||
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
|
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
|
||||||
|
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
||||||
const router = Taro.getCurrentInstance().router;
|
const router = Taro.getCurrentInstance().router;
|
||||||
const orderId = router?.params?.orderId;
|
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) => {
|
const getOrderStatusText = (order: ShopOrder) => {
|
||||||
// 优先检查订单状态
|
// 优先检查订单状态
|
||||||
if (order.orderStatus === 2) return '已取消';
|
if (order.orderStatus === 2) return '已取消';
|
||||||
@@ -81,8 +101,15 @@ const OrderDetail = () => {
|
|||||||
|
|
||||||
// 已付款后检查发货状态
|
// 已付款后检查发货状态
|
||||||
if (order.deliveryStatus === 10) return '待发货';
|
if (order.deliveryStatus === 10) return '待发货';
|
||||||
if (order.deliveryStatus === 20) return '待收货';
|
if (order.deliveryStatus === 20) {
|
||||||
if (order.deliveryStatus === 30) return '已收货';
|
// 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据
|
||||||
|
if (order.riderId) {
|
||||||
|
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
|
||||||
|
return '配送中';
|
||||||
|
}
|
||||||
|
return '待收货';
|
||||||
|
}
|
||||||
|
if (order.deliveryStatus === 30) return '部分发货';
|
||||||
|
|
||||||
// 最后检查订单完成状态
|
// 最后检查订单完成状态
|
||||||
if (order.orderStatus === 1) return '已完成';
|
if (order.orderStatus === 1) return '已完成';
|
||||||
@@ -133,6 +160,15 @@ const OrderDetail = () => {
|
|||||||
return <div>加载中...</div>;
|
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 (
|
return (
|
||||||
<div className={'order-detail-page'}>
|
<div className={'order-detail-page'}>
|
||||||
{/* 支付倒计时显示 - 详情页实时更新 */}
|
{/* 支付倒计时显示 - 详情页实时更新 */}
|
||||||
@@ -190,11 +226,25 @@ const OrderDetail = () => {
|
|||||||
{!order.payStatus && <Button onClick={() => console.log('取消订单')}>取消订单</Button>}
|
{!order.payStatus && <Button onClick={() => console.log('取消订单')}>取消订单</Button>}
|
||||||
{!order.payStatus && <Button type="primary" onClick={() => console.log('立即支付')}>立即支付</Button>}
|
{!order.payStatus && <Button type="primary" onClick={() => console.log('立即支付')}>立即支付</Button>}
|
||||||
{order.orderStatus === 1 && <Button onClick={handleApplyRefund}>申请退款</Button>}
|
{order.orderStatus === 1 && <Button onClick={handleApplyRefund}>申请退款</Button>}
|
||||||
{order.deliveryStatus === 20 &&
|
{canConfirmReceive && (
|
||||||
<Button type="primary" onClick={() => console.log('确认收货')}>确认收货</Button>}
|
<Button type="primary" onClick={() => setConfirmReceiveDialogVisible(true)}>
|
||||||
|
确认收货
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
title="确认收货"
|
||||||
|
visible={confirmReceiveDialogVisible}
|
||||||
|
confirmText="确认收货"
|
||||||
|
cancelText="我再想想"
|
||||||
|
onConfirm={handleConfirmReceive}
|
||||||
|
onCancel={() => setConfirmReceiveDialogVisible(false)}
|
||||||
|
>
|
||||||
|
确定已经收到商品了吗?确认后订单将完成。
|
||||||
|
</Dialog>
|
||||||
</div>
|
</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;
|
searchParams?: ShopOrderParam;
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
|
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
|
||||||
|
// 订单视图模式:用户/门店/骑手
|
||||||
|
mode?: 'user' | 'store' | 'rider';
|
||||||
|
// 固定过滤条件(例如 storeId / riderId),会合并到每次请求里
|
||||||
|
baseParams?: ShopOrderParam;
|
||||||
|
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrderList(props: OrderListProps) {
|
function OrderList(props: OrderListProps) {
|
||||||
@@ -115,6 +121,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
const [orderToCancel, setOrderToCancel] = useState<ShopOrder | null>(null)
|
const [orderToCancel, setOrderToCancel] = useState<ShopOrder | null>(null)
|
||||||
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
|
||||||
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
|
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
|
||||||
|
const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider'
|
||||||
|
|
||||||
// 获取订单状态文本
|
// 获取订单状态文本
|
||||||
const getOrderStatusText = (order: ShopOrder) => {
|
const getOrderStatusText = (order: ShopOrder) => {
|
||||||
@@ -131,8 +138,14 @@ function OrderList(props: OrderListProps) {
|
|||||||
|
|
||||||
// 已付款后检查发货状态
|
// 已付款后检查发货状态
|
||||||
if (order.deliveryStatus === 10) return '待发货';
|
if (order.deliveryStatus === 10) return '待发货';
|
||||||
if (order.deliveryStatus === 20) return '待收货';
|
if (order.deliveryStatus === 20) {
|
||||||
if (order.deliveryStatus === 30) return '已完成';
|
// 若订单没有配送员,沿用原“待收货”语义
|
||||||
|
if (!order.riderId) return '待收货';
|
||||||
|
// 配送员确认送达后(sendEndTime有值),才进入“待确认收货”
|
||||||
|
if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货';
|
||||||
|
return '配送中';
|
||||||
|
}
|
||||||
|
if (order.deliveryStatus === 30) return '部分发货';
|
||||||
|
|
||||||
// 最后检查订单完成状态
|
// 最后检查订单完成状态
|
||||||
if (order.orderStatus === 1) 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 === 10) return 'text-blue-500'; // 待发货
|
||||||
if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
|
if (order.deliveryStatus === 20) {
|
||||||
if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
|
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'; // 已完成
|
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
|
||||||
@@ -167,9 +184,13 @@ function OrderList(props: OrderListProps) {
|
|||||||
|
|
||||||
// 使用后端统一的 statusFilter 进行筛选
|
// 使用后端统一的 statusFilter 进行筛选
|
||||||
const getOrderStatusParams = (index: string | number) => {
|
const getOrderStatusParams = (index: string | number) => {
|
||||||
let params: ShopOrderParam = {};
|
let params: ShopOrderParam = {
|
||||||
// 添加用户ID过滤
|
...(props.baseParams || {})
|
||||||
params.userId = Taro.getStorageSync('UserId');
|
};
|
||||||
|
// 默认是用户视图:添加 userId 过滤;门店/骑手视图由 baseParams 控制
|
||||||
|
if (!props.mode || props.mode === 'user') {
|
||||||
|
params.userId = Taro.getStorageSync('UserId');
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前tab的statusFilter配置
|
// 获取当前tab的statusFilter配置
|
||||||
const currentTab = tabs.find(tab => tab.index === Number(index));
|
const currentTab = tabs.find(tab => tab.index === Number(index));
|
||||||
@@ -190,7 +211,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
// 合并搜索条件,tab的statusFilter优先级更高
|
// 合并搜索条件,tab的statusFilter优先级更高
|
||||||
const searchConditions: any = {
|
const searchConditions: any = {
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
userId: statusParams.userId, // 用户ID
|
...statusParams,
|
||||||
...props.searchParams, // 搜索关键词等其他条件
|
...props.searchParams, // 搜索关键词等其他条件
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,7 +306,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
|
|
||||||
await updateShopOrder({
|
await updateShopOrder({
|
||||||
...orderToConfirmReceive,
|
...orderToConfirmReceive,
|
||||||
deliveryStatus: 20, // 已收货
|
deliveryStatus: orderToConfirmReceive.deliveryStatus, // 10未发货 20已发货 30部分发货(收货由orderStatus控制)
|
||||||
orderStatus: 1 // 已完成
|
orderStatus: 1 // 已完成
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -764,6 +785,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
<Text className={'w-full text-right'}>实付金额:¥{item.payPrice}</Text>
|
<Text className={'w-full text-right'}>实付金额:¥{item.payPrice}</Text>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
|
{!isReadOnly && (
|
||||||
<Space className={'btn flex justify-end'}>
|
<Space className={'btn flex justify-end'}>
|
||||||
{/* 待付款状态:显示取消订单和立即支付 */}
|
{/* 待付款状态:显示取消订单和立即支付 */}
|
||||||
{(!item.payStatus) && item.orderStatus !== 2 && (
|
{(!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>
|
<Space>
|
||||||
<Button size={'small'} onClick={(e) => {
|
<Button size={'small'} onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -837,6 +859,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
}}>再次购买</Button>
|
}}>再次购买</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Cell>
|
</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 Taro from '@tarojs/taro';
|
||||||
import { createOrder, WxPayResult } from '@/api/shop/shopOrder';
|
import { createOrder, WxPayResult } from '@/api/shop/shopOrder';
|
||||||
import { OrderCreateRequest } from '@/api/shop/shopOrder/model';
|
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 {
|
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: '支付中...' });
|
Taro.showLoading({ title: '支付中...' });
|
||||||
|
|
||||||
try {
|
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;
|
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