This commit is contained in:
@@ -13,7 +13,6 @@ import CenterPage from './pages/CenterPage';
|
||||
import PartnerMePage from './pages/PartnerMePage';
|
||||
import BillsPage from './pages/BillsPage';
|
||||
import SettlementPage from './pages/SettlementPage';
|
||||
import ReshipPage from './pages/ReshipPage';
|
||||
import WeeklyReportPage from './pages/WeeklyReportPage';
|
||||
import ProxyOrderPage from './pages/ProxyOrderPage';
|
||||
import StaffListPage from './pages/StaffListPage';
|
||||
@@ -34,7 +33,6 @@ function PrimaryRoutes() {
|
||||
<Route path="/center/staff" element={<StaffListPage />} />
|
||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
||||
<Route path="/reshipments" element={<ReshipPage />} />
|
||||
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
||||
<Route path="/leaderboard" element={<LeaderboardPage />} />
|
||||
<Route path="/stores/:id" element={<StoreDetailPage />} />
|
||||
|
||||
@@ -55,6 +55,8 @@ function accountFromProfile(profile: PartnerSessionProfile): PartnerAccount {
|
||||
hasWechat: profile.hasWechat,
|
||||
wxNickname: profile.wxNickname,
|
||||
wxAvatarUrl: profile.wxAvatarUrl,
|
||||
managedWarehouseId: profile.managedWarehouseId,
|
||||
hasWarehouseAccess: profile.hasWarehouseAccess,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ export type PartnerSessionProfile = Pick<
|
||||
| 'hasWechat'
|
||||
| 'wxNickname'
|
||||
| 'wxAvatarUrl'
|
||||
| 'managedWarehouseId'
|
||||
| 'hasWarehouseAccess'
|
||||
> & {
|
||||
staffRole?: PartnerStaffRole;
|
||||
};
|
||||
@@ -125,6 +127,8 @@ function profileFromMe(me: PartnerMe): PartnerSessionProfile {
|
||||
hasWechat: me.hasWechat,
|
||||
wxNickname: me.wxNickname,
|
||||
wxAvatarUrl: me.wxAvatarUrl,
|
||||
managedWarehouseId: me.managedWarehouseId,
|
||||
hasWarehouseAccess: me.hasWarehouseAccess,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,15 @@ export function getPartnerNavKind(account: PartnerMe | null | undefined): Partne
|
||||
return 'store_staff';
|
||||
}
|
||||
|
||||
/** 是否已配置管仓:主账号看 hasWarehouseAccess;子账号还需有仓库相关权限 */
|
||||
export function hasWarehouseAccess(account: PartnerMe | null | undefined): boolean {
|
||||
if (!account) return false;
|
||||
if (account.hasWarehouseAccess === false) return false;
|
||||
if (account.hasWarehouseAccess === true) return true;
|
||||
// 旧缓存无字段时保守:主账号未知视为无,避免误展示全城订单
|
||||
return false;
|
||||
}
|
||||
|
||||
export function partnerHomePath(account: PartnerMe | null | undefined): string {
|
||||
return '/';
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
||||
import { getPartnerNavKind } from '../lib/partnerAccess';
|
||||
import { getPartnerNavKind, hasWarehouseAccess } from '../lib/partnerAccess';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
|
||||
function fmtMoney(n: number) {
|
||||
@@ -135,6 +135,7 @@ export default function HomePage() {
|
||||
const navKind = getPartnerNavKind(account);
|
||||
const isPrimary = navKind === 'primary';
|
||||
const isWarehouse = navKind === 'warehouse_staff';
|
||||
const warehouseOk = hasWarehouseAccess(account);
|
||||
|
||||
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
||||
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
|
||||
@@ -147,10 +148,12 @@ export default function HomePage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||
if (isWarehouse || isPrimary) {
|
||||
if ((isWarehouse || isPrimary) && warehouseOk) {
|
||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders')
|
||||
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
||||
.catch(() => setOrders([]));
|
||||
} else {
|
||||
setOrders([]);
|
||||
}
|
||||
if (!isWarehouse) {
|
||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
||||
@@ -165,7 +168,7 @@ export default function HomePage() {
|
||||
} else {
|
||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
||||
}
|
||||
}, [navigate, isWarehouse, isPrimary]);
|
||||
}, [navigate, isWarehouse, isPrimary, warehouseOk]);
|
||||
|
||||
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
||||
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
||||
@@ -190,7 +193,6 @@ export default function HomePage() {
|
||||
const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0);
|
||||
const revenue = orderCount * 128.45;
|
||||
const profit = revenue * 0.25;
|
||||
const pendingShipBadge = orderStats.pendingShip;
|
||||
|
||||
return (
|
||||
<div className="page partner-home partner-home--flush-top">
|
||||
@@ -224,13 +226,6 @@ export default function HomePage() {
|
||||
</div>
|
||||
<span className="partner-quick-action-label">录入新店</span>
|
||||
</Link>
|
||||
<Link to="/reshipments" className="partner-quick-action">
|
||||
<div className="partner-quick-action-icon partner-quick-action-icon--amber">
|
||||
<span className="material-symbols-outlined">assignment_return</span>
|
||||
{pendingShipBadge > 0 && <span className="partner-quick-badge-count">{pendingShipBadge}</span>}
|
||||
</div>
|
||||
<span className="partner-quick-action-label">补发处理</span>
|
||||
</Link>
|
||||
<Link to="/center/settlement" className="partner-quick-action">
|
||||
<div className="partner-quick-action-icon partner-quick-action-icon--green">
|
||||
<span className="material-symbols-outlined">account_balance</span>
|
||||
@@ -252,7 +247,7 @@ export default function HomePage() {
|
||||
<p className="headline-md" style={{ marginBottom: 8 }}>订单管理</p>
|
||||
)}
|
||||
|
||||
{(isPrimary || isWarehouse) && (
|
||||
{(isPrimary || isWarehouse) && warehouseOk && (
|
||||
<OrderSummarySection
|
||||
title={isWarehouse ? '今日订单' : undefined}
|
||||
todayCount={orderStats.todayCount}
|
||||
@@ -262,6 +257,18 @@ export default function HomePage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isPrimary || isWarehouse) && !warehouseOk && (
|
||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||
<div className="partner-warehouse-denied">
|
||||
<span className="material-symbols-outlined text-primary">warehouse</span>
|
||||
<p className="body-md" style={{ fontWeight: 600, marginTop: 8 }}>未配置仓库管理权限</p>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4, lineHeight: 1.5 }}>
|
||||
购酒订单由总部履约,不会推送到本账号。如需管仓发货,请联系总部配置仓库。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!isWarehouse && (
|
||||
<>
|
||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { hasWarehouseAccess } from '../lib/partnerAccess';
|
||||
|
||||
type DateFilter = 'today' | '7d' | '30d';
|
||||
type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL';
|
||||
@@ -34,15 +36,24 @@ type OrderListPageProps = {
|
||||
tabRoot?: boolean;
|
||||
};
|
||||
|
||||
type OrdersResponse = {
|
||||
list: Array<Record<string, unknown>>;
|
||||
hasWarehouseAccess?: boolean;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<{ list: Array<Record<string, unknown>> }>({ list: [] });
|
||||
const { account } = usePartnerSession();
|
||||
const warehouseOk = hasWarehouseAccess(account);
|
||||
const [data, setData] = useState<OrdersResponse>({ list: [] });
|
||||
const [tab, setTab] = useState<'orders' | 'coupons'>('orders');
|
||||
const [dateFilter, setDateFilter] = useState<DateFilter>('today');
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
|
||||
const [shippingId, setShippingId] = useState<string | null>(null);
|
||||
const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
|
||||
const [shipModalId, setShipModalId] = useState<string | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '订单管理';
|
||||
@@ -50,8 +61,22 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders').then(setData);
|
||||
}, [navigate]);
|
||||
if (!warehouseOk) {
|
||||
setData({ list: [], hasWarehouseAccess: false, message: '未配置仓库管理权限' });
|
||||
setLoaded(true);
|
||||
return;
|
||||
}
|
||||
request<OrdersResponse>('PARTNER_H5', '/partner/orders')
|
||||
.then((res) => {
|
||||
setData({
|
||||
list: Array.isArray(res.list) ? res.list : [],
|
||||
hasWarehouseAccess: res.hasWarehouseAccess,
|
||||
message: res.message,
|
||||
});
|
||||
})
|
||||
.catch(() => setData({ list: [] }))
|
||||
.finally(() => setLoaded(true));
|
||||
}, [navigate, warehouseOk]);
|
||||
|
||||
const filtered = useMemo(() => data.list.filter((o) => {
|
||||
if (statusFilter === 'ALL') return true;
|
||||
@@ -83,8 +108,12 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
||||
body: JSON.stringify(shipForm),
|
||||
});
|
||||
setShipModalId(null);
|
||||
const next = await request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders');
|
||||
setData(next);
|
||||
const next = await request<OrdersResponse>('PARTNER_H5', '/partner/orders');
|
||||
setData({
|
||||
list: Array.isArray(next.list) ? next.list : [],
|
||||
hasWarehouseAccess: next.hasWarehouseAccess,
|
||||
message: next.message,
|
||||
});
|
||||
} catch (err) {
|
||||
window.alert(err instanceof Error ? err.message : '发货失败');
|
||||
} finally {
|
||||
@@ -97,6 +126,21 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
||||
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
|
||||
}
|
||||
|
||||
if (loaded && !warehouseOk) {
|
||||
return (
|
||||
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
|
||||
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
||||
<div className="partner-warehouse-denied" style={{ margin: 20, padding: 32, textAlign: 'center' }}>
|
||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: 40 }}>warehouse</span>
|
||||
<p className="headline-md" style={{ marginTop: 12 }}>未配置仓库管理权限</p>
|
||||
<p className="body-md text-muted" style={{ marginTop: 8, lineHeight: 1.6 }}>
|
||||
购酒订单由总部履约,不会推送到本账号。如需管仓发货,请联系总部配置仓库。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
|
||||
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
||||
|
||||
@@ -3278,8 +3278,17 @@ body {
|
||||
border-top: 1px dashed rgba(166, 29, 36, 0.15);
|
||||
}
|
||||
|
||||
.partner-home--flush-top .partner-home-body {
|
||||
padding-top: 20px;
|
||||
.partner-warehouse-denied {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 20px 12px;
|
||||
}
|
||||
|
||||
.partner-warehouse-denied .material-symbols-outlined {
|
||||
font-size: 36px;
|
||||
font-variation-settings: 'FILL' 0;
|
||||
}
|
||||
|
||||
.partner-home--flush-top.partner-store-page,
|
||||
|
||||
@@ -17,6 +17,10 @@ export interface PartnerMe {
|
||||
/** 子账号联系主账号用 */
|
||||
primaryPhone?: string;
|
||||
primaryName?: string;
|
||||
/** 主账号绑定的管仓 ID(有则本城酒单推送至该仓) */
|
||||
managedWarehouseId?: string | null;
|
||||
/** 是否已配置仓库管理(无则不展示/不推送酒订单) */
|
||||
hasWarehouseAccess?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdatePartnerMeRequest {
|
||||
|
||||
@@ -103,10 +103,16 @@ export class PartnerCityService {
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
|
||||
const warehouseIds = await this.resolveManagedWarehouseIds(primary.id);
|
||||
if (warehouseIds.length > 0) {
|
||||
return { fulfillmentWarehouseId: { in: warehouseIds } };
|
||||
// 未配置管仓:不推送/不展示酒订单(同城无仓由总部履约)
|
||||
if (warehouseIds.length === 0) {
|
||||
return { id: -1n };
|
||||
}
|
||||
return { cityId: primary.cityId };
|
||||
return { fulfillmentWarehouseId: { in: warehouseIds } };
|
||||
}
|
||||
|
||||
async hasManagedWarehouse(partnerAccountId: bigint): Promise<boolean> {
|
||||
const ids = await this.resolveManagedWarehouseIds(partnerAccountId);
|
||||
return ids.length > 0;
|
||||
}
|
||||
|
||||
/** 合伙人可管仓库:主账号 managedWarehouseId + 绑定为管仓合伙人的仓 */
|
||||
|
||||
@@ -9,6 +9,7 @@ import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
|
||||
class UpdatePartnerMeDto {
|
||||
@IsOptional()
|
||||
@@ -243,7 +244,10 @@ export class AdminWineryBillController {
|
||||
@Controller('partner/me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class PartnerMeController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async me(@CurrentUser() user: AuthUser) {
|
||||
@@ -274,6 +278,7 @@ export class PartnerMeController {
|
||||
where: { id: account.parentAccountId },
|
||||
});
|
||||
}
|
||||
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
|
||||
return {
|
||||
id: account.id.toString(),
|
||||
name: account.name,
|
||||
@@ -288,6 +293,8 @@ export class PartnerMeController {
|
||||
hasWechat: !!account.wxOpenId,
|
||||
wxNickname: account.wxNickname ?? undefined,
|
||||
wxAvatarUrl: account.wxAvatarUrl ?? undefined,
|
||||
managedWarehouseId: primary.managedWarehouseId?.toString() ?? null,
|
||||
hasWarehouseAccess,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +509,7 @@ export class TradeService {
|
||||
|
||||
async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
|
||||
const where = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
@@ -520,7 +521,14 @@ export class TradeService {
|
||||
}),
|
||||
this.prisma.order.count({ where }),
|
||||
]);
|
||||
return { list: serializeBigInt(list.map(mapOrderCompat)), total, page, pageSize };
|
||||
return {
|
||||
list: serializeBigInt(list.map(mapOrderCompat)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
hasWarehouseAccess,
|
||||
message: hasWarehouseAccess ? undefined : '未配置仓库管理权限,购酒订单由总部履约',
|
||||
};
|
||||
}
|
||||
|
||||
async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) {
|
||||
|
||||
Reference in New Issue
Block a user