diff --git a/config/app.ts b/config/app.ts index 4ef7855..adbcde7 100644 --- a/config/app.ts +++ b/config/app.ts @@ -2,6 +2,8 @@ import { API_BASE_URL } from './env' // 租户ID - 请根据实际情况修改 export const TenantId = '10584'; +// 租户名称 +export const TenantName = '桂乐淘'; // 接口地址 - 请根据实际情况修改 export const BaseUrl = API_BASE_URL; // 当前版本 diff --git a/src/api/shop/shopOrder/model/index.ts b/src/api/shop/shopOrder/model/index.ts index 92709f8..557f33e 100644 --- a/src/api/shop/shopOrder/model/index.ts +++ b/src/api/shop/shopOrder/model/index.ts @@ -27,6 +27,14 @@ export interface ShopOrder { merchantName?: string; // 商户编号 merchantCode?: string; + // 归属门店ID(shop_store.id) + storeId?: number; + // 归属门店名称 + storeName?: string; + // 配送员用户ID(优先级派单) + riderId?: number; + // 发货仓库ID + warehouseId?: number; // 使用的优惠券id couponId?: number; // 使用的会员卡id @@ -61,6 +69,8 @@ export interface ShopOrder { sendStartTime?: string; // 配送结束时间 sendEndTime?: string; + // 配送员送达拍照(选填) + sendEndImg?: string; // 发货店铺id expressMerchantId?: number; // 发货店铺 @@ -165,6 +175,14 @@ export interface OrderGoodsItem { export interface OrderCreateRequest { // 商品信息列表 goodsItems: OrderGoodsItem[]; + // 归属门店ID(shop_store.id) + storeId?: number; + // 归属门店名称(可选) + storeName?: string; + // 配送员用户ID(优先级派单) + riderId?: number; + // 发货仓库ID + warehouseId?: number; // 收货地址ID addressId?: number; // 支付方式 @@ -197,6 +215,14 @@ export interface OrderGoodsItem { export interface OrderCreateRequest { // 商品信息列表 goodsItems: OrderGoodsItem[]; + // 归属门店ID(shop_store.id) + storeId?: number; + // 归属门店名称(可选) + storeName?: string; + // 配送员用户ID(优先级派单) + riderId?: number; + // 发货仓库ID + warehouseId?: number; // 收货地址ID addressId?: number; // 支付方式 @@ -223,6 +249,12 @@ export interface ShopOrderParam extends PageParam { payType?: number; isInvoice?: boolean; userId?: number; + // 归属门店ID(shop_store.id) + storeId?: number; + // 配送员用户ID + riderId?: number; + // 发货仓库ID + warehouseId?: number; keywords?: string; deliveryStatus?: number; statusFilter?: number; diff --git a/src/api/shop/shopStore/index.ts b/src/api/shop/shopStore/index.ts new file mode 100644 index 0000000..e67303f --- /dev/null +++ b/src/api/shop/shopStore/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-store/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopStore/model/index.ts b/src/api/shop/shopStore/model/index.ts new file mode 100644 index 0000000..3e5bcd1 --- /dev/null +++ b/src/api/shop/shopStore/model/index.ts @@ -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; +} diff --git a/src/api/shop/shopStoreRider/index.ts b/src/api/shop/shopStoreRider/index.ts new file mode 100644 index 0000000..c411aef --- /dev/null +++ b/src/api/shop/shopStoreRider/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-store-rider/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopStoreRider/model/index.ts b/src/api/shop/shopStoreRider/model/index.ts new file mode 100644 index 0000000..26e51f4 --- /dev/null +++ b/src/api/shop/shopStoreRider/model/index.ts @@ -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; +} diff --git a/src/api/shop/shopStoreUser/index.ts b/src/api/shop/shopStoreUser/index.ts new file mode 100644 index 0000000..0e500ab --- /dev/null +++ b/src/api/shop/shopStoreUser/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-store-user/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopStoreUser/model/index.ts b/src/api/shop/shopStoreUser/model/index.ts new file mode 100644 index 0000000..46151f5 --- /dev/null +++ b/src/api/shop/shopStoreUser/model/index.ts @@ -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; +} diff --git a/src/api/shop/shopWarehouse/index.ts b/src/api/shop/shopWarehouse/index.ts new file mode 100644 index 0000000..59a5f6e --- /dev/null +++ b/src/api/shop/shopWarehouse/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-warehouse/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopWarehouse/model/index.ts b/src/api/shop/shopWarehouse/model/index.ts new file mode 100644 index 0000000..4b96a81 --- /dev/null +++ b/src/api/shop/shopWarehouse/model/index.ts @@ -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; +} diff --git a/src/app.config.ts b/src/app.config.ts index 7a1c168..67a1f2b 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -58,6 +58,7 @@ export default { "gift/detail", "gift/add", "store/verification", + "store/orders/index", "theme/index", "poster/poster", "chat/conversation/index", @@ -91,10 +92,18 @@ export default { 'comments/index', 'search/index'] }, + { + "root": "store", + "pages": [ + "index", + "orders/index" + ] + }, { "root": "rider", "pages": [ - "index" + "index", + "orders/index" ] }, { diff --git a/src/dealer/index.tsx b/src/dealer/index.tsx index 06c514f..2c9d6a0 100644 --- a/src/dealer/index.tsx +++ b/src/dealer/index.tsx @@ -132,7 +132,7 @@ const DealerIndex: React.FC = () => { 佣金统计 - @@ -140,7 +140,7 @@ const DealerIndex: React.FC = () => { 可提现 - @@ -148,7 +148,7 @@ const DealerIndex: React.FC = () => { 冻结中 - diff --git a/src/pages/index/Header.tsx b/src/pages/index/Header.tsx index c9f032e..05d503c 100644 --- a/src/pages/index/Header.tsx +++ b/src/pages/index/Header.tsx @@ -1,10 +1,10 @@ import {useEffect, useState} from "react"; import Taro from '@tarojs/taro'; -import {Button, Space, Sticky} from '@nutui/nutui-react-taro' +import {Button, Sticky, Popup, Cell, CellGroup} from '@nutui/nutui-react-taro' import {TriangleDown} from '@nutui/icons-react-taro' import {Avatar, NavBar} from '@nutui/nutui-react-taro' import {getUserInfo, getWxOpenId} from "@/api/layout"; -import {TenantId} from "@/config/app"; +import {TenantId, TenantName} from "@/config/app"; import {getOrganization} from "@/api/system/organization"; import {myUserVerify} from "@/api/system/userVerify"; import { useShopInfo } from '@/hooks/useShopInfo'; @@ -13,6 +13,9 @@ import {View,Text} from '@tarojs/components' import MySearch from "./MySearch"; import './Header.scss'; import {User} from "@/api/system/user/model"; +import {getShopStore, listShopStore} from "@/api/shop/shopStore"; +import type {ShopStore} from "@/api/shop/shopStore/model"; +import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection"; const Header = (_: any) => { // 使用新的useShopInfo Hook @@ -25,8 +28,124 @@ const Header = (_: any) => { const [stickyStatus, setStickyStatus] = useState(false) const [userInfo] = useState() + // 门店选择:用于首页展示“最近门店”,并在下单时写入订单 storeId + const [storePopupVisible, setStorePopupVisible] = useState(false) + const [stores, setStores] = useState([]) + const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage()) + const [userLocation, setUserLocation] = useState<{lng: number; lat: number} | null>(null) + const getTenantName = () => { - return userInfo?.tenantName || '商城名称' + return userInfo?.tenantName || TenantName + } + + const parseStoreCoords = (s: ShopStore): {lng: number; lat: number} | null => { + const raw = (s.lngAndLat || s.location || '').trim() + if (!raw) return null + + const parts = raw.split(/[,\s]+/).filter(Boolean) + if (parts.length < 2) return null + + const a = parseFloat(parts[0]) + const b = parseFloat(parts[1]) + if (Number.isNaN(a) || Number.isNaN(b)) return null + + // 常见格式是 "lng,lat";这里做一个简单兜底(经度范围更宽) + const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90 + const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180 + if (looksLikeLngLat) return {lng: a, lat: b} + if (looksLikeLatLng) return {lng: b, lat: a} + return null + } + + const distanceMeters = (a: {lng: number; lat: number}, b: {lng: number; lat: number}) => { + const toRad = (x: number) => (x * Math.PI) / 180 + const R = 6371000 // meters + const dLat = toRad(b.lat - a.lat) + const dLng = toRad(b.lng - a.lng) + const lat1 = toRad(a.lat) + const lat2 = toRad(b.lat) + const sin1 = Math.sin(dLat / 2) + const sin2 = Math.sin(dLng / 2) + const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2 + return 2 * R * Math.asin(Math.min(1, Math.sqrt(h))) + } + + const formatDistance = (meters?: number) => { + if (meters === undefined || Number.isNaN(meters)) return '' + if (meters < 1000) return `${Math.round(meters)}m` + return `${(meters / 1000).toFixed(1)}km` + } + + const getStoreDistance = (s: ShopStore) => { + if (!userLocation) return undefined + const coords = parseStoreCoords(s) + if (!coords) return undefined + return distanceMeters(userLocation, coords) + } + + const initStoreSelection = async () => { + // 先读取本地已选门店,避免页面首屏抖动 + const stored = getSelectedStoreFromStorage() + if (stored?.id) { + setSelectedStore(stored) + } + + // 拉取门店列表(失败时允许用户手动重试/继续使用本地门店) + let list: ShopStore[] = [] + try { + list = await listShopStore() + } catch (e) { + console.error('获取门店列表失败:', e) + list = [] + } + const usable = (list || []).filter(s => s?.isDelete !== 1) + setStores(usable) + + // 尝试获取定位,用于计算最近门店 + let loc: {lng: number; lat: number} | null = null + try { + const r = await Taro.getLocation({type: 'gcj02'}) + loc = {lng: r.longitude, lat: r.latitude} + } catch (e) { + // 不强制定位授权;无定位时仍允许用户手动选择 + console.warn('获取定位失败,将不显示最近门店距离:', e) + } + setUserLocation(loc) + + const ensureStoreDetail = async (s: ShopStore) => { + if (!s?.id) return s + // 如果后端已经返回默认仓库等字段,就不额外请求 + if (s.warehouseId) return s + try { + const full = await getShopStore(s.id) + return full || s + } catch (_e) { + return s + } + } + + // 若用户没有选过门店,则自动选择最近门店(或第一个) + const alreadySelected = stored?.id + if (alreadySelected || usable.length === 0) return + + let autoPick: ShopStore | undefined + if (loc) { + autoPick = [...usable] + .map(s => { + const coords = parseStoreCoords(s) + const d = coords ? distanceMeters(loc, coords) : undefined + return {s, d} + }) + .sort((x, y) => (x.d ?? Number.POSITIVE_INFINITY) - (y.d ?? Number.POSITIVE_INFINITY))[0]?.s + } else { + autoPick = usable[0] + } + + if (autoPick?.id) { + const full = await ensureStoreDetail(autoPick) + setSelectedStore(full) + saveSelectedStoreToStorage(full) + } } const reload = async () => { @@ -187,6 +306,7 @@ const Header = (_: any) => { useEffect(() => { reload().then() + initStoreSelection().then() }, []) return ( @@ -216,31 +336,95 @@ const Header = (_: any) => { onBackClick={() => { }} left={ + setStorePopupVisible(true)} + > + + + {selectedStore?.name || '请选择门店'} + + + + } + right={ !IsLogin ? ( - - - - ) : ( - - - {getTenantName()} - - - )}> + + ) : null + } + > {getTenantName()} + + setStorePopupVisible(false)} + > + + + 选择门店 + setStorePopupVisible(false)} + > + 关闭 + + + + + {userLocation ? '已获取定位,按距离排序' : '未获取定位,可手动选择门店'} + + + + {[...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 ( + + {s.name || `门店${s.id}`} + {d !== undefined && {formatDistance(d)}} + + } + 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'}) + }} + /> + ) + })} + + + ) diff --git a/src/pages/user/components/UserGrid.tsx b/src/pages/user/components/UserGrid.tsx index 5c84262..54b6577 100644 --- a/src/pages/user/components/UserGrid.tsx +++ b/src/pages/user/components/UserGrid.tsx @@ -11,13 +11,14 @@ import { People, // AfterSaleService, Logout, - ShoppingAdd, + Shop, + Jdl, Service } from '@nutui/icons-react-taro' import {useUser} from "@/hooks/useUser"; const UserCell = () => { - const {logoutUser} = useUser(); + const {logoutUser, hasRole} = useUser(); const onLogout = () => { Taro.showModal({ @@ -49,10 +50,49 @@ const UserCell = () => { border: 'none' } as React.CSSProperties} > - navTo('/rider/index', true)}> + + {hasRole('store') && ( + navTo('/store/index', true)}> + + + + + + + )} + + {hasRole('rider') && ( + navTo('/rider/index', true)}> + + + + + + + )} + + {(hasRole('staff') || hasRole('admin')) && ( + navTo('/user/store/orders/index', true)}> + + + + + + + )} + + navTo('/user/address/index', true)}> - - + + + + + + + navTo('/user/help/index')}> + + + @@ -71,14 +111,6 @@ const UserCell = () => { - navTo('/user/address/index', true)}> - - - - - - - navTo('/user/userVerify/index', true)}> @@ -111,13 +143,6 @@ const UserCell = () => { {/* */} {/**/} - navTo('/user/help/index')}> - - - - - - navTo('/user/about/index')}> @@ -189,4 +214,3 @@ const UserCell = () => { ) } export default UserCell - diff --git a/src/rider/orders/index.config.ts b/src/rider/orders/index.config.ts new file mode 100644 index 0000000..9606465 --- /dev/null +++ b/src/rider/orders/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '配送订单', + navigationBarTextStyle: 'black' +} diff --git a/src/rider/orders/index.tsx b/src/rider/orders/index.tsx new file mode 100644 index 0000000..e228e13 --- /dev/null +++ b/src/rider/orders/index.tsx @@ -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([]) + const [hasMore, setHasMore] = useState(true) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const [deliverDialogVisible, setDeliverDialogVisible] = useState(false) + const [deliverSubmitting, setDeliverSubmitting] = useState(false) + const [deliverOrder, setDeliverOrder] = useState(null) + const [deliverImg, setDeliverImg] = useState(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 ( + + 请先登录 + + ) + } + + const displayList = filterByTab(list) + + return ( + + + + setTabIndex(Number(paneKey))} + > + {riderTabs.map(t => ( + + ))} + + + + {error ? ( + + {error} + + + ) : ( + 加载中} + loadMoreText={ + displayList.length === 0 ? ( + + ) : ( + 没有更多了 + ) + } + > + {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 ( + o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})} + > + + + {o.orderNo || `订单#${o.orderId}`} + {getOrderStatusText(o)} + + + + 下单时间:{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'} + + + + + 收货点: + {o.selfTakeMerchantName || o.address || '-'} + + + 客户: + {o.realName || '-'} {o.phone ? `(${o.phone})` : ''} + + + 金额: + ¥{o.payPrice || o.totalPrice || '-'} + 数量: + {o.totalNum ?? '-'} + + {o.sendEndTime && ( + + 送达时间: + {dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')} + + )} + + + {/* 配送流程 */} + + 流程: + 1 派单 + {'>'} + 2 配送中 + {'>'} + 3 送达收货点 + {'>'} + 4 客户确认收货 + + {o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && ( + + 若客户未确认,预计 {autoConfirmAt.format('YYYY-MM-DD HH:mm')} 自动确认收货(以后台配置为准) + {typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''} + + )} + + + + + {!!phoneToCall && ( + + )} + {canConfirmDelivered(o) && ( + + )} + + + + + ) + })} + + )} + + + + { + if (deliverSubmitting) return + setDeliverDialogVisible(false) + setDeliverOrder(null) + setDeliverImg(undefined) + }} + > + + 到达收货点后,可选拍照留存,再点确认送达。 + + + + {deliverImg && ( + + + + + + + )} + + + + ) +} diff --git a/src/shop/orderDetail/index.tsx b/src/shop/orderDetail/index.tsx index 7bb5b00..6350bcb 100644 --- a/src/shop/orderDetail/index.tsx +++ b/src/shop/orderDetail/index.tsx @@ -1,5 +1,5 @@ import {useEffect, useState} from "react"; -import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro' +import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-taro' import Taro from '@tarojs/taro' import {View} from '@tarojs/components' import {ShopOrder} from "@/api/shop/shopOrder/model"; @@ -13,6 +13,7 @@ import './index.scss' const OrderDetail = () => { const [order, setOrder] = useState(null); const [orderGoodsList, setOrderGoodsList] = useState([]); + const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false) const router = Taro.getCurrentInstance().router; const orderId = router?.params?.orderId; @@ -67,6 +68,25 @@ const OrderDetail = () => { } }; + // 确认收货(客户) + const handleConfirmReceive = async () => { + if (!order?.orderId) return + try { + setConfirmReceiveDialogVisible(false) + await updateShopOrder({ + orderId: order.orderId, + deliveryStatus: order.deliveryStatus, // 10未发货 20已发货 30部分发货 + orderStatus: 1 // 已完成 + }) + Taro.showToast({title: '确认收货成功', icon: 'success'}) + setOrder(prev => (prev ? {...prev, orderStatus: 1} : prev)) + } catch (e) { + console.error('确认收货失败:', e) + Taro.showToast({title: '确认收货失败', icon: 'none'}) + setConfirmReceiveDialogVisible(true) + } + } + const getOrderStatusText = (order: ShopOrder) => { // 优先检查订单状态 if (order.orderStatus === 2) return '已取消'; @@ -81,8 +101,15 @@ const OrderDetail = () => { // 已付款后检查发货状态 if (order.deliveryStatus === 10) return '待发货'; - if (order.deliveryStatus === 20) return '待收货'; - if (order.deliveryStatus === 30) return '已收货'; + if (order.deliveryStatus === 20) { + // 若订单有配送员,则以配送员送达时间作为“可确认收货”的依据 + if (order.riderId) { + if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货'; + return '配送中'; + } + return '待收货'; + } + if (order.deliveryStatus === 30) return '部分发货'; // 最后检查订单完成状态 if (order.orderStatus === 1) return '已完成'; @@ -133,6 +160,15 @@ const OrderDetail = () => { return
加载中...
; } + 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 (
{/* 支付倒计时显示 - 详情页实时更新 */} @@ -190,11 +226,25 @@ const OrderDetail = () => { {!order.payStatus && } {!order.payStatus && } {order.orderStatus === 1 && } - {order.deliveryStatus === 20 && - } + {canConfirmReceive && ( + + )} + + setConfirmReceiveDialogVisible(false)} + > + 确定已经收到商品了吗?确认后订单将完成。 +
); }; diff --git a/src/store/index.config.ts b/src/store/index.config.ts new file mode 100644 index 0000000..5ec6c4d --- /dev/null +++ b/src/store/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '门店中心' +}) diff --git a/src/store/index.scss b/src/store/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/store/index.tsx b/src/store/index.tsx new file mode 100644 index 0000000..8852533 --- /dev/null +++ b/src/store/index.tsx @@ -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(undefined) + const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage()) + const [store, setStore] = useState(selectedStore) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(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 ( + + + 请先登录后再进入门店中心 + + + + + + ) + } + + return ( + + {/* 头部信息 */} + + + + + + + } + className="mr-4" + style={{border: '2px solid rgba(255, 255, 255, 0.3)'}} + /> + + + {getDisplayName()} + + + {hasRole('store') ? '门店' : hasRole('rider') ? '配送员' : getRoleName()} + + + + + + + {/* 门店信息 */} + + + 当前门店 + Taro.switchTab({url: '/pages/index/index'})} + > + 切换门店 + + + + {!storeId ? ( + + + 未选择门店,请先去首页选择门店。 + + + + + + ) : ( + + + {store?.name || `门店ID: ${storeId}`} + + {!!store?.address && ( + + {store.address} + + )} + {!!store?.warehouseName && ( + + 默认仓库:{store.warehouseName} + + )} + {!!error && ( + + {error} + + )} + + )} + + + {/* 功能入口 */} + + 门店工具 + + + navigateToPage('/store/orders/index')}> + + + + + + + + navigateToPage('/user/store/verification')}> + + + + + + + + + + + + + + + + Taro.switchTab({url: '/pages/index/index'})}> + + + + + + + + + + + + + ) +} + +export default StoreIndex diff --git a/src/store/orders/index.config.ts b/src/store/orders/index.config.ts new file mode 100644 index 0000000..4de19d9 --- /dev/null +++ b/src/store/orders/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '门店订单', + navigationBarTextStyle: 'black' +} diff --git a/src/store/orders/index.tsx b/src/store/orders/index.tsx new file mode 100644 index 0000000..2127715 --- /dev/null +++ b/src/store/orders/index.tsx @@ -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(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 ( + + + 请先登录 + + + + + + ) + } + + return ( + + + + + 当前门店: + + {boundStoreId + ? (selectedStore?.id === boundStoreId ? (selectedStore?.name || `门店ID: ${boundStoreId}`) : `门店ID: ${boundStoreId}`) + : (selectedStore?.name || '未选择门店')} + + + + {!storeId ? ( + + + 请先在首页左上角选择门店,再查看门店订单。 + + + + + + ) : ( + + )} + + + ) +} diff --git a/src/user/order/components/OrderList.tsx b/src/user/order/components/OrderList.tsx index 9cab23d..8e87747 100644 --- a/src/user/order/components/OrderList.tsx +++ b/src/user/order/components/OrderList.tsx @@ -89,6 +89,12 @@ interface OrderListProps { searchParams?: ShopOrderParam; showSearch?: boolean; onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化 + // 订单视图模式:用户/门店/骑手 + mode?: 'user' | 'store' | 'rider'; + // 固定过滤条件(例如 storeId / riderId),会合并到每次请求里 + baseParams?: ShopOrderParam; + // 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮 + readOnly?: boolean; } function OrderList(props: OrderListProps) { @@ -115,6 +121,7 @@ function OrderList(props: OrderListProps) { const [orderToCancel, setOrderToCancel] = useState(null) const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false) const [orderToConfirmReceive, setOrderToConfirmReceive] = useState(null) + const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider' // 获取订单状态文本 const getOrderStatusText = (order: ShopOrder) => { @@ -131,8 +138,14 @@ function OrderList(props: OrderListProps) { // 已付款后检查发货状态 if (order.deliveryStatus === 10) return '待发货'; - if (order.deliveryStatus === 20) return '待收货'; - if (order.deliveryStatus === 30) return '已完成'; + if (order.deliveryStatus === 20) { + // 若订单没有配送员,沿用原“待收货”语义 + if (!order.riderId) return '待收货'; + // 配送员确认送达后(sendEndTime有值),才进入“待确认收货” + if (order.sendEndTime && order.orderStatus !== 1) return '待确认收货'; + return '配送中'; + } + if (order.deliveryStatus === 30) return '部分发货'; // 最后检查订单完成状态 if (order.orderStatus === 1) return '已完成'; @@ -155,8 +168,12 @@ function OrderList(props: OrderListProps) { // 已付款后检查发货状态 if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货 - if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货 - if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货 + if (order.deliveryStatus === 20) { + if (!order.riderId) return 'text-purple-500'; // 待收货 + if (order.sendEndTime && order.orderStatus !== 1) return 'text-purple-500'; // 待确认收货 + return 'text-blue-500'; // 配送中 + } + if (order.deliveryStatus === 30) return 'text-blue-500'; // 部分发货 // 最后检查订单完成状态 if (order.orderStatus === 1) return 'text-green-600'; // 已完成 @@ -167,9 +184,13 @@ function OrderList(props: OrderListProps) { // 使用后端统一的 statusFilter 进行筛选 const getOrderStatusParams = (index: string | number) => { - let params: ShopOrderParam = {}; - // 添加用户ID过滤 - params.userId = Taro.getStorageSync('UserId'); + let params: ShopOrderParam = { + ...(props.baseParams || {}) + }; + // 默认是用户视图:添加 userId 过滤;门店/骑手视图由 baseParams 控制 + if (!props.mode || props.mode === 'user') { + params.userId = Taro.getStorageSync('UserId'); + } // 获取当前tab的statusFilter配置 const currentTab = tabs.find(tab => tab.index === Number(index)); @@ -190,7 +211,7 @@ function OrderList(props: OrderListProps) { // 合并搜索条件,tab的statusFilter优先级更高 const searchConditions: any = { page: currentPage, - userId: statusParams.userId, // 用户ID + ...statusParams, ...props.searchParams, // 搜索关键词等其他条件 }; @@ -285,7 +306,7 @@ function OrderList(props: OrderListProps) { await updateShopOrder({ ...orderToConfirmReceive, - deliveryStatus: 20, // 已收货 + deliveryStatus: orderToConfirmReceive.deliveryStatus, // 10未发货 20已发货 30部分发货(收货由orderStatus控制) orderStatus: 1 // 已完成 }); @@ -764,6 +785,7 @@ function OrderList(props: OrderListProps) { 实付金额:¥{item.payPrice} {/* 操作按钮 */} + {!isReadOnly && ( {/* 待付款状态:显示取消订单和立即支付 */} {(!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 && ( )} + )} ) diff --git a/src/user/store/orders/index.config.ts b/src/user/store/orders/index.config.ts new file mode 100644 index 0000000..4de19d9 --- /dev/null +++ b/src/user/store/orders/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '门店订单', + navigationBarTextStyle: 'black' +} diff --git a/src/user/store/orders/index.tsx b/src/user/store/orders/index.tsx new file mode 100644 index 0000000..d555dcc --- /dev/null +++ b/src/user/store/orders/index.tsx @@ -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(0) + + const [boundStoreId, setBoundStoreId] = useState(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 ( + + + Taro.navigateBack()}/>} + > + 门店订单 + + + + + 当前门店: + {store?.name || (boundStoreId ? `门店ID: ${boundStoreId}` : '未选择门店')} + + + {!storeId ? ( + + + 请先在首页左上角选择门店,再查看门店订单。 + + + + + + ) : ( + + )} + + + ) +} diff --git a/src/utils/payment.ts b/src/utils/payment.ts index 29b5b4f..7cb32c4 100644 --- a/src/utils/payment.ts +++ b/src/utils/payment.ts @@ -1,6 +1,10 @@ import Taro from '@tarojs/taro'; import { createOrder, WxPayResult } from '@/api/shop/shopOrder'; import { OrderCreateRequest } from '@/api/shop/shopOrder/model'; +import { getSelectedStoreFromStorage, getSelectedStoreIdFromStorage } from '@/utils/storeSelection'; +import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model'; +import type { ShopWarehouse } from '@/api/shop/shopWarehouse/model'; +import request from '@/utils/request'; /** * 支付类型枚举 @@ -24,6 +28,9 @@ export interface PaymentCallback { * 统一支付处理类 */ export class PaymentHandler { + // 简单缓存,避免频繁请求(小程序单次运行生命周期内有效) + private static storeRidersCache = new Map(); + private static warehousesCache: ShopWarehouse[] | null = null; /** * 执行支付 @@ -39,6 +46,36 @@ export class PaymentHandler { Taro.showLoading({ title: '支付中...' }); try { + // 若调用方未指定门店,则自动注入“已选门店”,用于订单门店归属/统计。 + if (orderData.storeId === undefined || orderData.storeId === null) { + const storeId = getSelectedStoreIdFromStorage(); + if (storeId) { + orderData.storeId = storeId; + } + } + if (!orderData.storeName) { + const store = getSelectedStoreFromStorage(); + if (store?.name) { + orderData.storeName = store.name; + } + } + + // 自动派单:按门店骑手优先级(dispatchPriority)选择 riderId(不覆盖手动指定) + if ((orderData.riderId === undefined || orderData.riderId === null) && orderData.storeId) { + const riderUserId = await this.pickRiderUserIdForStore(orderData.storeId); + if (riderUserId) { + orderData.riderId = riderUserId; + } + } + + // 仓库选择:若未指定 warehouseId,则按“离门店最近”兜底选择一个(不覆盖手动指定) + if ((orderData.warehouseId === undefined || orderData.warehouseId === null) && orderData.storeId) { + const warehouseId = await this.pickWarehouseIdForStore(orderData.storeId); + if (warehouseId) { + orderData.warehouseId = warehouseId; + } + } + // 设置支付类型 orderData.payType = paymentType; @@ -119,6 +156,127 @@ export class PaymentHandler { } } + private static parseLngLat(raw: string | undefined): { lng: number; lat: number } | null { + const text = (raw || '').trim(); + if (!text) return null; + const parts = text.split(/[,\s]+/).filter(Boolean); + if (parts.length < 2) return null; + const a = parseFloat(parts[0]); + const b = parseFloat(parts[1]); + if (Number.isNaN(a) || Number.isNaN(b)) return null; + const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90; + const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180; + if (looksLikeLngLat) return { lng: a, lat: b }; + if (looksLikeLatLng) return { lng: b, lat: a }; + return null; + } + + private static distanceMeters(a: { lng: number; lat: number }, b: { lng: number; lat: number }) { + const toRad = (x: number) => (x * Math.PI) / 180; + const R = 6371000; + const dLat = toRad(b.lat - a.lat); + const dLng = toRad(b.lng - a.lng); + const lat1 = toRad(a.lat); + const lat2 = toRad(b.lat); + const sin1 = Math.sin(dLat / 2); + const sin2 = Math.sin(dLng / 2); + const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2; + return 2 * R * Math.asin(Math.min(1, Math.sqrt(h))); + } + + private static async getRidersForStore(storeId: number): Promise { + const cached = this.storeRidersCache.get(storeId); + if (cached) return cached; + + // 后端字段可能叫 dealerId 或 storeId,这里都带上,服务端忽略未知字段即可。 + // 这里做一次路径兼容(camel vs kebab),避免接口路径不一致导致整单失败。 + const list = await this.listByCompatEndpoint( + ['/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 { + 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 { + if (this.warehousesCache) return this.warehousesCache; + const list = await this.listByCompatEndpoint( + ['/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 { + 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( + urls: string[], + params: Record + ): Promise { + 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 []; + } + /** * 处理微信支付 */ diff --git a/src/utils/storeSelection.ts b/src/utils/storeSelection.ts new file mode 100644 index 0000000..91438a7 --- /dev/null +++ b/src/utils/storeSelection.ts @@ -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; +} +