diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx
index 86d1122..7b80897 100644
--- a/apps/admin-web/src/App.tsx
+++ b/apps/admin-web/src/App.tsx
@@ -18,6 +18,7 @@ import HqAccountsPage from './pages/HqAccountsPage';
import CitiesPage from './pages/CitiesPage';
import CityPartnersPage from './pages/CityPartnersPage';
import CityWarehousesPage from './pages/CityWarehousesPage';
+import FulfillmentProvidersPage from './pages/FulfillmentProvidersPage';
import StoreMediaPage from './pages/StoreMediaPage';
import PromoCodesPage from './pages/PromoCodesPage';
import PromoCodeDetailLayout from './pages/promo/PromoCodeDetailLayout';
@@ -74,6 +75,7 @@ export default function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx
index fc5b93b..ea65028 100644
--- a/apps/admin-web/src/layouts/AdminLayout.tsx
+++ b/apps/admin-web/src/layouts/AdminLayout.tsx
@@ -56,6 +56,7 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/cities', label: '城市' },
{ key: '/city-partners', label: '城市合伙人' },
{ key: '/city-warehouses', label: '仓库' },
+ { key: '/fulfillment-providers', label: '仓配管理' },
],
},
{
diff --git a/apps/admin-web/src/pages/CityWarehousesPage.tsx b/apps/admin-web/src/pages/CityWarehousesPage.tsx
index c063654..095f2a7 100644
--- a/apps/admin-web/src/pages/CityWarehousesPage.tsx
+++ b/apps/admin-web/src/pages/CityWarehousesPage.tsx
@@ -6,6 +6,7 @@ import {
Descriptions,
Form,
Input,
+ InputNumber,
Modal,
Popconfirm,
Select,
@@ -17,10 +18,13 @@ import {
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
+ WAREHOUSE_FULFILLMENT_MODE_LABELS,
WAREHOUSE_MANAGER_LABELS,
WAREHOUSE_STATUS_LABELS,
+ WarehouseFulfillmentMode,
WarehouseManagerType,
WarehouseStatus,
+ type FulfillmentProviderDto,
} from '@dukang/shared-types';
import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
@@ -39,6 +43,13 @@ type Row = {
partnerAccountId: string | null;
partnerCompanyName?: string | null;
status: string;
+ fulfillmentMode?: string;
+ fulfillmentProviderId?: string | null;
+ fulfillmentProviderName?: string | null;
+ manualCarrierLabel?: string | null;
+ manualQueryUrlTemplate?: string | null;
+ lng?: number | null;
+ lat?: number | null;
createdAt: string;
};
@@ -48,6 +59,54 @@ type PartnerOption = { id: string; companyName: string };
const MANAGER_OPTIONS = Object.entries(WAREHOUSE_MANAGER_LABELS).map(([value, label]) => ({ value, label }));
const STATUS_OPTIONS = Object.entries(WAREHOUSE_STATUS_LABELS).map(([value, label]) => ({ value, label }));
+const FULFILLMENT_MODE_OPTIONS = Object.entries(WAREHOUSE_FULFILLMENT_MODE_LABELS).map(([value, label]) => ({
+ value,
+ label,
+}));
+
+function FulfillmentFields({
+ mode,
+ providerOptions,
+}: {
+ mode: WarehouseFulfillmentMode;
+ providerOptions: FulfillmentProviderDto[];
+}) {
+ return (
+ <>
+ {mode === WarehouseFulfillmentMode.API_AUTO && (
+
+
+ )}
+ {mode === WarehouseFulfillmentMode.MANUAL && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
export default function CityWarehousesPage() {
const [filterForm] = Form.useForm();
const [createForm] = Form.useForm();
@@ -73,6 +132,13 @@ export default function CityWarehousesPage() {
const [createManagerType, setCreateManagerType] = useState(WarehouseManagerType.HQ);
const [editManagerType, setEditManagerType] = useState(WarehouseManagerType.HQ);
const [createCityId, setCreateCityId] = useState();
+ const [createFulfillmentMode, setCreateFulfillmentMode] = useState(
+ WarehouseFulfillmentMode.MANUAL,
+ );
+ const [editFulfillmentMode, setEditFulfillmentMode] = useState(
+ WarehouseFulfillmentMode.MANUAL,
+ );
+ const [providerOptions, setProviderOptions] = useState([]);
const loadCities = useCallback(async () => {
const res = await request>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
@@ -88,6 +154,9 @@ export default function CityWarehousesPage() {
useEffect(() => {
void loadCities();
+ void request('/admin/fulfillment-providers/active-api')
+ .then(setProviderOptions)
+ .catch(() => {});
}, [loadCities]);
async function openEdit(row: Row) {
@@ -102,7 +171,14 @@ export default function CityWarehousesPage() {
managerType: row.managerType,
partnerAccountId: row.partnerAccountId,
status: row.status,
+ fulfillmentMode: row.fulfillmentMode ?? WarehouseFulfillmentMode.MANUAL,
+ fulfillmentProviderId: row.fulfillmentProviderId ?? undefined,
+ manualCarrierLabel: row.manualCarrierLabel ?? undefined,
+ manualQueryUrlTemplate: row.manualQueryUrlTemplate ?? undefined,
+ lng: row.lng ?? undefined,
+ lat: row.lat ?? undefined,
});
+ setEditFulfillmentMode((row.fulfillmentMode as WarehouseFulfillmentMode) ?? WarehouseFulfillmentMode.MANUAL);
setEditOpen(true);
}
@@ -149,6 +225,14 @@ export default function CityWarehousesPage() {
'—'
),
},
+ {
+ title: '履约',
+ width: 120,
+ render: (_, row) =>
+ row.fulfillmentMode === 'API_AUTO'
+ ? row.fulfillmentProviderName || 'API'
+ : WAREHOUSE_FULFILLMENT_MODE_LABELS[WarehouseFulfillmentMode.MANUAL],
+ },
{
title: '状态',
dataIndex: 'status',
@@ -341,6 +425,13 @@ export default function CityWarehousesPage() {
+
+
+
@@ -399,6 +490,13 @@ export default function CityWarehousesPage() {
+
+
+
diff --git a/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx b/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx
new file mode 100644
index 0000000..6533f96
--- /dev/null
+++ b/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx
@@ -0,0 +1,196 @@
+import { useCallback, useEffect, useState } from 'react';
+import {
+ Button,
+ Form,
+ Input,
+ Modal,
+ Select,
+ Space,
+ Table,
+ Tag,
+ Typography,
+ message,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import {
+ FULFILLMENT_PROVIDER_STATUS_LABELS,
+ FULFILLMENT_PROVIDER_TYPE_LABELS,
+ FulfillmentProviderStatus,
+ FulfillmentProviderType,
+ type FulfillmentProviderDto,
+} from '@dukang/shared-types';
+import { request } from '../lib/api';
+import { fmtTime } from '../lib/constants';
+
+const TYPE_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_TYPE_LABELS).map(([value, label]) => ({
+ value,
+ label,
+}));
+const STATUS_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_STATUS_LABELS).map(([value, label]) => ({
+ value,
+ label,
+}));
+
+export default function FulfillmentProvidersPage() {
+ const [rows, setRows] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [editRow, setEditRow] = useState(null);
+ const [form] = Form.useForm();
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const res = await request('/admin/fulfillment-providers');
+ setRows(res);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ function openCreate() {
+ setEditRow(null);
+ form.resetFields();
+ form.setFieldsValue({
+ type: FulfillmentProviderType.API,
+ status: FulfillmentProviderStatus.ACTIVE,
+ capabilitiesJson: JSON.stringify(
+ { createShipment: true, getTrack: true, callback: true },
+ null,
+ 2,
+ ),
+ });
+ setOpen(true);
+ }
+
+ function openEdit(row: FulfillmentProviderDto) {
+ setEditRow(row);
+ form.setFieldsValue({
+ code: row.code,
+ name: row.name,
+ type: row.type,
+ status: row.status,
+ capabilitiesJson: row.capabilities ? JSON.stringify(row.capabilities, null, 2) : '',
+ configJson: '',
+ });
+ setOpen(true);
+ }
+
+ async function submit() {
+ const v = await form.validateFields();
+ if (editRow) {
+ await request(`/admin/fulfillment-providers/${editRow.id}`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ name: v.name,
+ type: v.type,
+ status: v.status,
+ configJson: v.configJson || undefined,
+ capabilitiesJson: v.capabilitiesJson || undefined,
+ }),
+ });
+ message.success('已更新');
+ } else {
+ await request('/admin/fulfillment-providers', {
+ method: 'POST',
+ body: JSON.stringify(v),
+ });
+ message.success('已创建');
+ }
+ setOpen(false);
+ void load();
+ }
+
+ const columns: ColumnsType = [
+ { title: '编码', dataIndex: 'code', width: 100 },
+ { title: '名称', dataIndex: 'name' },
+ {
+ title: '类型',
+ dataIndex: 'type',
+ render: (v) => FULFILLMENT_PROVIDER_TYPE_LABELS[v as FulfillmentProviderType] || v,
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ render: (v) => (
+
+ {FULFILLMENT_PROVIDER_STATUS_LABELS[v as FulfillmentProviderStatus] || v}
+
+ ),
+ },
+ {
+ title: '能力',
+ render: (_, row) => {
+ const caps = row.capabilities;
+ if (!caps) return '—';
+ return Object.entries(caps)
+ .filter(([, on]) => on)
+ .map(([k]) => k)
+ .join('、') || '—';
+ },
+ },
+ { title: '更新时间', dataIndex: 'updatedAt', width: 170, render: fmtTime },
+ {
+ title: '操作',
+ width: 80,
+ render: (_, row) => (
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/apps/admin-web/src/pages/OrdersPage.tsx b/apps/admin-web/src/pages/OrdersPage.tsx
index b518c7f..405b0c6 100644
--- a/apps/admin-web/src/pages/OrdersPage.tsx
+++ b/apps/admin-web/src/pages/OrdersPage.tsx
@@ -51,6 +51,8 @@ type OrderDetail = AdminOrderRow & {
productAmount?: number;
freightAmount?: number;
benefitAmount?: number;
+ deliveryType?: string;
+ fulfillmentWarehouseId?: string | null;
paidAt?: string | null;
payExpireAt?: string | null;
items?: Array>;
@@ -63,6 +65,7 @@ type OrderDetail = AdminOrderRow & {
export default function OrdersPage() {
const [form] = Form.useForm();
const [shipForm] = Form.useForm();
+ const [logisticsForm] = Form.useForm();
const [data, setData] = useState | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
@@ -74,6 +77,7 @@ export default function OrdersPage() {
const [batchDeleting, setBatchDeleting] = useState(false);
const [shipDefaults, setShipDefaults] = useState(null);
const [shipping, setShipping] = useState(false);
+ const [logisticsShipping, setLogisticsShipping] = useState(false);
const selectedOrders = useMemo(
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
@@ -143,6 +147,25 @@ export default function OrdersPage() {
}
}
+ async function submitLogisticsShip() {
+ if (!detail) return;
+ const values = await logisticsForm.validateFields();
+ setLogisticsShipping(true);
+ try {
+ const res = await request(`/admin/orders/${detail.id}/logistics-ship`, {
+ method: 'POST',
+ body: JSON.stringify(values),
+ });
+ message.success('快递单已录入');
+ setDetail(res);
+ void load();
+ } catch (e) {
+ message.error(e instanceof Error ? e.message : '填单失败');
+ } finally {
+ setLogisticsShipping(false);
+ }
+ }
+
async function confirmBatchDelete() {
if (!selectedRowKeys.length) return;
setBatchDeleting(true);
@@ -324,16 +347,55 @@ export default function OrdersPage() {
{detail.delivery && (
- {DELIVERY_PROVIDER_LABELS[detail.delivery.provider] || detail.delivery.provider}
+ {detail.delivery.logisticsCompany ||
+ DELIVERY_PROVIDER_LABELS[detail.delivery.provider] ||
+ detail.delivery.provider}
{detail.delivery.trackingNo || '—'}
{detail.delivery.providerOrderNo || '—'}
+ {detail.delivery.manualQueryUrl && (
+
+
+ 打开物流查询
+
+
+ )}
)}
{['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(detail.status) && !detail.delivery?.trackingNo && (
- 发货
+ {(detail.deliveryType === 'CROSS_CITY' || !detail.fulfillmentWarehouseId) && (
+ <>
+ 总部快递填单
+
+ 适用于跨城订单或同城无仓订单
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {detail.fulfillmentWarehouseId && (
+ <>
+
+ 仓配小飞侠重试
+
+
+ 仓配订单通常支付后自动推单;失败时可手动重试
+
)}
diff --git a/apps/h5-partner/src/PartnerAppRoutes.tsx b/apps/h5-partner/src/PartnerAppRoutes.tsx
index 4b4f3d4..3c0f461 100644
--- a/apps/h5-partner/src/PartnerAppRoutes.tsx
+++ b/apps/h5-partner/src/PartnerAppRoutes.tsx
@@ -1,6 +1,6 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import { usePartnerSession } from './contexts/PartnerSessionContext';
-import { isSubAccount } from './lib/partnerAccess';
+import { getPartnerNavKind, isSubAccount } from './lib/partnerAccess';
import TabLayout from './layouts/TabLayout';
import SubAccountLayout from './layouts/SubAccountLayout';
import HomePage from './pages/HomePage';
@@ -26,7 +26,6 @@ function PrimaryRoutes() {
}>
} />
} />
- } />
} />
} />
@@ -39,6 +38,7 @@ function PrimaryRoutes() {
} />
} />
} />
+ } />
} />
} />
@@ -46,16 +46,30 @@ function PrimaryRoutes() {
}
function SubAccountRoutes() {
+ const { account } = usePartnerSession();
+ const navKind = getPartnerNavKind(account);
+ const isWarehouse = navKind === 'warehouse_staff';
+
return (
- }>
- } />
- } />
+ }>
+ } />
+ {isWarehouse ? (
+ } />
+ ) : (
+ } />
+ )}
} />
- {/* 与主账号一致:录入门店全屏,避免 sticky 底栏被 TabBar 挡住 */}
- } />
- } />
+ {!isWarehouse && (
+ <>
+ } />
+ } />
+ } />
+ >
+ )}
+ {isWarehouse && } />}
+ } />
);
}
diff --git a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx
index 81428af..dd20eb2 100644
--- a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx
+++ b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx
@@ -50,6 +50,8 @@ function accountFromProfile(profile: PartnerSessionProfile): PartnerAccount {
staffRole: profile.staffRole,
permissions: profile.permissions,
primaryAccountId: profile.primaryAccountId,
+ primaryPhone: profile.primaryPhone,
+ primaryName: profile.primaryName,
hasWechat: profile.hasWechat,
};
}
diff --git a/apps/h5-partner/src/layouts/SubAccountLayout.tsx b/apps/h5-partner/src/layouts/SubAccountLayout.tsx
index f1cf4f3..69a0f9e 100644
--- a/apps/h5-partner/src/layouts/SubAccountLayout.tsx
+++ b/apps/h5-partner/src/layouts/SubAccountLayout.tsx
@@ -1,17 +1,30 @@
import { NavLink, Outlet } from 'react-router-dom';
+import type { PartnerNavKind } from '../lib/partnerAccess';
-const TABS = [
- { to: '/stores', end: true, icon: 'store', label: '门店管理' },
- { to: '/stores/new', icon: 'add_business', label: '录入门店' },
- { to: '/me', icon: 'person', label: '我的' },
+const STORE_STAFF_TABS = [
+ { to: '/', end: true, icon: 'dashboard', label: '首页' },
+ { to: '/stores', icon: 'store', label: '门店管理' },
+ { to: '/me', icon: 'person', label: '个人中心' },
] as const;
-export default function SubAccountLayout() {
+const WAREHOUSE_STAFF_TABS = [
+ { to: '/', end: true, icon: 'dashboard', label: '首页' },
+ { to: '/orders', icon: 'receipt_long', label: '订单管理' },
+ { to: '/me', icon: 'person', label: '个人中心' },
+] as const;
+
+type SubAccountLayoutProps = {
+ navKind: PartnerNavKind;
+};
+
+export default function SubAccountLayout({ navKind }: SubAccountLayoutProps) {
+ const tabs = navKind === 'warehouse_staff' ? WAREHOUSE_STAFF_TABS : STORE_STAFF_TABS;
+
return (
<>