| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155 |
- import { useState, useEffect } from 'react';
- import { useQuery } from '@tanstack/react-query';
- import { useForm } from 'react-hook-form';
- import { zodResolver } from '@hookform/resolvers/zod';
- import { format } from 'date-fns';
- import { toast } from 'sonner';
- import { Search, Edit, Eye, Package, Truck, Check, Printer, Play } from 'lucide-react';
- // 获取认证token的工具函数
- const getAuthToken = (): string | null => {
- if (typeof window !== 'undefined') {
- return localStorage.getItem('token');
- }
- return null;
- };
- // 创建带认证头的fetch选项
- const createAuthFetchOptions = (options: RequestInit = {}): RequestInit => {
- const token = getAuthToken();
- const headers = new Headers(options.headers);
- if (token) {
- headers.set('Authorization', `Bearer ${token}`);
- }
- // 只有POST/PUT/PATCH请求才需要设置Content-Type
- const method = options.method?.toUpperCase() || 'GET';
- if (['POST', 'PUT', 'PATCH'].includes(method) && !headers.has('Content-Type')) {
- headers.set('Content-Type', 'application/json');
- }
- return {
- ...options,
- headers
- };
- };
-
- // 使用共享UI组件包的具体路径导入
- import { Button } from '@d8d/shared-ui-components/components/ui/button';
- import { Input } from '@d8d/shared-ui-components/components/ui/input';
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
- import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
- import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea';
- import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
- import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
- } from '@d8d/shared-ui-components/components/ui/command';
- import { Popover, PopoverContent, PopoverTrigger } from '@d8d/shared-ui-components/components/ui/popover';
- // 简单分页组件
- const DataTablePagination = ({
- currentPage,
- pageSize,
- totalCount,
- onPageChange
- }: {
- currentPage: number;
- pageSize: number;
- totalCount: number;
- onPageChange: (page: number, limit: number) => void;
- }) => {
- const totalPages = Math.ceil(totalCount / pageSize);
- return (
- <div className="flex items-center justify-between px-2 py-4">
- <div className="text-sm text-muted-foreground">
- 共 {totalCount} 条记录
- </div>
- <div className="flex items-center space-x-2">
- <Button
- variant="outline"
- size="sm"
- onClick={() => onPageChange(Math.max(1, currentPage - 1), pageSize)}
- disabled={currentPage <= 1}
- >
- 上一页
- </Button>
- <div className="text-sm">
- 第 {currentPage} 页,共 {totalPages} 页
- </div>
- <Button
- variant="outline"
- size="sm"
- onClick={() => onPageChange(Math.min(totalPages, currentPage + 1), pageSize)}
- disabled={currentPage >= totalPages}
- >
- 下一页
- </Button>
- </div>
- </div>
- );
- };
- import { adminOrderClient, orderClientManager } from '../api';
- import type { InferResponseType } from 'hono/client';
- import { UpdateOrderDto } from '@d8d/orders-module-mt/schemas';
- // 飞鹅打印相关类型定义
- interface SubmitPrintTaskRequest {
- printerSn: string;
- content: string;
- printType: string;
- orderId?: number;
- delaySeconds?: number;
- }
- interface SubmitPrintTaskResponse {
- taskId: string;
- status: string;
- scheduledAt?: string;
- }
- // 触发支付成功事件响应类型
- interface TriggerPaymentSuccessResponse {
- success: boolean;
- message: string;
- orderId: number;
- tenantId: number;
- }
- interface ApiResponse<T> {
- success: boolean;
- data?: T;
- message?: string;
- error?: string;
- }
- // 类型定义
- type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>['data'][0] & {
- user?: {
- id: number;
- username: string;
- phone: string | null;
- openid?: string | null; // 添加openid字段
- hasSubscribedDeliveryNotice?: boolean; // 添加订阅状态字段
- } | null;
- tenantId?: number; // 添加tenantId字段
- };
- type UpdateRequest = any;
- // 发货请求类型 - 使用UpdateOrderDto的类型
- type DeliveryRequest = {
- deliveryType: number;
- deliveryCompany?: string | null;
- deliveryNo?: string | null;
- deliveryRemark?: string | null;
- deliveryTime?: string | null;
- };
- type DeliveryResponse = any;
- // 微信服务消息通知配置类型 - 参考useShareAppMessage的设计模式
- interface WechatServiceMessageConfig {
- openid: string;
- templateId: string;
- page?: string;
- data: Record<string, { value: string }>;
- miniprogramState?: 'developer' | 'trial' | 'formal';
- tenantId?: number; // 添加tenantId参数
- }
- // 微信服务消息通知结果类型
- interface WechatServiceMessageResult {
- success: boolean;
- message: string;
- data?: any;
- error?: any;
- }
- // 微信服务消息通知函数 - 参考useShareAppMessage的简洁设计模式
- const sendWechatServiceMessage = async (config: WechatServiceMessageConfig): Promise<WechatServiceMessageResult> => {
- try {
- console.debug('准备发送微信服务消息:', config);
- // 调用后端微信API
- const response = await fetch('/api/v1/auth/send-template-message', createAuthFetchOptions({
- method: 'POST',
- body: JSON.stringify({
- openid: config.openid,
- templateId: config.templateId,
- data: config.data,
- page: config.page || 'pages/index/index',
- miniprogramState: config.miniprogramState || 'formal',
- tenantId: config.tenantId
- })
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('微信服务消息API调用失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- return {
- success: false,
- message: `微信服务消息发送失败: ${response.status}`,
- error: errorText
- };
- }
- const result = await response.json();
-
- console.debug('微信服务消息发送成功:', result);
- return {
- success: true,
- message: '微信服务消息发送成功',
- data: result
- };
- } catch (error) {
- console.error('发送微信服务消息时出错:', error);
- return {
- success: false,
- message: '微信服务消息发送失败',
- error
- };
- }
- };
- // 格式化微信日期时间
- const formatWechatDate = (isoDateString: string | null | undefined): string => {
- try {
- // 如果日期字符串为空,使用当前时间
- const date = isoDateString ? new Date(isoDateString) : new Date();
- // 微信date类型格式:YYYY年MM月DD日 HH:mm
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- return `${year}年${month}月${day}日 ${hours}:${minutes}`;
- } catch (error) {
- console.error('格式化微信日期失败:', error, isoDateString);
- // 返回当前时间的格式化版本作为备用
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, '0');
- const day = String(now.getDate()).padStart(2, '0');
- const hours = String(now.getHours()).padStart(2, '0');
- const minutes = String(now.getMinutes()).padStart(2, '0');
- return `${year}年${month}月${day}日 ${hours}:${minutes}`;
- }
- };
- // 快递公司列表
- const getWechatDeliveryCompanies = async (tenantId?: number): Promise<{ success: boolean; message: string; data?: any; error?: any }> => {
- try {
- console.debug('准备获取微信小店快递公司列表:', { tenantId });
- // 调用后端获取快递公司列表API
- const response = await fetch('/api/v1/auth/get-delivery-companies', createAuthFetchOptions({
- method: 'POST',
- body: JSON.stringify({
- tenantId
- })
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('获取快递公司列表API调用失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- let errorMessage = `获取快递公司列表失败: ${response.status}`;
- try {
- const errorData = JSON.parse(errorText);
- if (errorData.message) {
- errorMessage = errorData.message;
- }
- if (errorData.error?.message) {
- errorMessage = errorData.error.message;
- }
- } catch (e) {
- // 如果无法解析为JSON,使用原始文本
- if (errorText) {
- errorMessage = errorText.substring(0, 200);
- }
- }
- return {
- success: false,
- message: errorMessage,
- error: errorText
- };
- }
- const result = await response.json();
- console.debug('获取快递公司列表成功:', result);
- return {
- success: true,
- message: '获取快递公司列表成功',
- data: result
- };
- } catch (error) {
- console.error('调用获取快递公司列表API时出错:', error);
- return {
- success: false,
- message: '获取快递公司列表失败',
- error
- };
- }
- };
-
- // 生成商品描述信息,截取110个字
- const generateItemDesc = (order: OrderResponse): string => {
- if (!order.orderGoods || order.orderGoods.length === 0) {
- return '商品信息';
- }
- // 构建商品描述:商品1名称×数量, 商品2名称×数量, ...
- const itemDescriptions = order.orderGoods.map(item => {
- return `${item.goodsName}×${item.num}`;
- });
- let itemDesc = itemDescriptions.join(', ');
- // 截取110个字(中文字符)
- if (itemDesc.length > 110) {
- itemDesc = itemDesc.substring(0, 110) + '...';
- }
- console.debug('生成的商品描述:', {
- originalLength: itemDescriptions.join(', ').length,
- truncatedLength: itemDesc.length,
- itemDesc
- });
- return itemDesc;
- };
- // 调用微信小程序发货信息录入API
- const uploadShippingInfoToWechat = async (
- order: OrderResponse,
- deliveryData: DeliveryRequest,
- tenantId?: number
- ): Promise<{ success: boolean; message: string; data?: any; error?: any }> => {
- try {
- // 生成商品描述
- const itemDesc = generateItemDesc(order);
- console.debug('准备调用微信小程序发货信息录入API:', {
- orderId: order.id,
- orderNo: order.orderNo,
- deliveryType: deliveryData.deliveryType,
- deliveryCompany: deliveryData.deliveryCompany,
- deliveryNo: deliveryData.deliveryNo,
- itemDesc,
- tenantId: tenantId || order.tenantId
- });
- // 根据发货类型准备参数
- let expressInfo = undefined;
- let localDeliveryInfo = undefined;
- switch (deliveryData.deliveryType) {
- case 1: // 物流快递
- if (deliveryData.deliveryCompany && deliveryData.deliveryNo) {
- // deliveryCompany现在存储的是delivery_id
- expressInfo = {
- deliveryId: deliveryData.deliveryCompany, // 直接使用ID
- waybillId: deliveryData.deliveryNo
- };
- console.debug('使用快递公司ID:', {
- deliveryId: deliveryData.deliveryCompany,
- deliveryNo: deliveryData.deliveryNo
- });
- }
- break;
- case 2: // 同城配送
- // 同城配送不需要物流信息
- // 如果需要配送员信息,可以在这里设置 localDeliveryInfo
- // localDeliveryInfo = {
- // deliveryName: '配送员',
- // deliveryPhone: '13800138000'
- // };
- break;
- case 3: // 虚拟发货
- case 4: // 用户自提
- // 无需物流,不需要额外信息
- break;
- }
- // 调用后端发货信息录入API
- const response = await fetch('/api/v1/auth/upload-shipping-info', createAuthFetchOptions({
- method: 'POST',
- body: JSON.stringify({
- orderId: order.orderNo, // 使用订单号作为微信小程序订单ID
- deliveryType: deliveryData.deliveryType, // 直接使用前端的deliveryType值
- expressInfo,
- localDeliveryInfo,
- isAllDelivered: true,
- itemDesc, // 商品描述,最多110个字
- tenantId: tenantId || order.tenantId
- })
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('发货信息录入API调用失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- return {
- success: false,
- message: `发货信息录入失败: ${response.status}`,
- error: errorText
- };
- }
- const result = await response.json();
- console.debug('发货信息录入成功:', result);
- return {
- success: true,
- message: '发货信息录入成功',
- data: result
- };
- } catch (error) {
- console.error('调用发货信息录入API时出错:', error);
- return {
- success: false,
- message: '发货信息录入失败',
- error
- };
- }
- };
-
- // 状态映射
- const orderStatusMap = {
- 0: { label: '未发货', color: 'warning' },
- 1: { label: '已发货', color: 'info' },
- 2: { label: '收货成功', color: 'success' },
- 3: { label: '已退货', color: 'destructive' },
- } as const;
- const payStatusMap = {
- 0: { label: '未支付', color: 'warning' },
- 1: { label: '支付中', color: 'info' },
- 2: { label: '支付成功', color: 'success' },
- 3: { label: '已退款', color: 'secondary' },
- 4: { label: '支付失败', color: 'destructive' },
- 5: { label: '订单关闭', color: 'destructive' },
- } as const;
- const orderTypeMap = {
- 1: { label: '实物订单', color: 'default' },
- 2: { label: '虚拟订单', color: 'secondary' },
- } as const;
- const deliveryTypeMap = {
- 0: { label: '未发货', color: 'warning' },
- 1: { label: '物流快递', color: 'info' },
- 2: { label: '同城配送', color: 'info' },
- 3: { label: '虚拟发货', color: 'info' },
- 4: { label: '用户自提', color: 'info' },
- } as const;
- export const OrderManagement = () => {
- const [searchParams, setSearchParams] = useState({
- page: 1,
- limit: 10,
- search: '',
- status: 'all',
- payStatus: 'all',
- });
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [editingOrder, setEditingOrder] = useState<OrderResponse | null>(null);
- const [detailModalOpen, setDetailModalOpen] = useState(false);
- const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
- const [deliveryModalOpen, setDeliveryModalOpen] = useState(false);
- const [deliveringOrder, setDeliveringOrder] = useState<OrderResponse | null>(null);
- const [deliveryCompanies, setDeliveryCompanies] = useState<Array<{ delivery_id: string; delivery_name: string }>>([]);
- const [loadingCompanies, setLoadingCompanies] = useState(false);
- const [printingOrder, setPrintingOrder] = useState<OrderResponse | null>(null);
- const [isPrinting, setIsPrinting] = useState(false);
- const [triggeringOrder, setTriggeringOrder] = useState<OrderResponse | null>(null);
- const [isTriggering, setIsTriggering] = useState(false);
- // 用于防止重复提交的请求ID缓存
- const [recentPrintRequests, setRecentPrintRequests] = useState<Map<string, number>>(new Map());
- // 表单实例
- const updateForm = useForm<UpdateRequest>({
- resolver: zodResolver(UpdateOrderDto),
- defaultValues: {},
- });
- // 发货表单实例
- const deliveryForm = useForm<DeliveryRequest>({
- defaultValues: {
- deliveryType: 1,
- deliveryCompany: '__select__',
- deliveryNo: '',
- deliveryRemark: '',
- },
- });
-
- // 数据查询 - 60秒自动刷新
- const { data, isLoading, refetch } = useQuery({
- queryKey: ['orders', searchParams],
- queryFn: async () => {
- const filters: any = {};
- if (searchParams.status !== 'all') {
- filters.state = parseInt(searchParams.status);
- }
- if (searchParams.payStatus !== 'all') {
- filters.payState = parseInt(searchParams.payStatus);
- }
- const res = await orderClientManager.getAdminOrderClient().index.$get({
- query: {
- page: searchParams.page,
- pageSize: searchParams.limit,
- keyword: searchParams.search,
- ...(Object.keys(filters).length > 0 && { filters: JSON.stringify(filters) }),
- }
- });
- if (res.status !== 200) throw new Error('获取订单列表失败');
- return await res.json();
- },
- refetchInterval: 60000, // 60秒自动刷新
- refetchIntervalInBackground: false, // 只在页面可见时刷新
- staleTime: 30000, // 30秒后数据视为过期
- });
- // 定期清理过期的请求记录(每5分钟清理一次)
- useEffect(() => {
- const cleanupInterval = setInterval(() => {
- const now = Date.now();
- setRecentPrintRequests(prev => {
- const newMap = new Map();
- for (const [fingerprint, timestamp] of prev) {
- // 保留最近2分钟内的记录
- if (now - timestamp < 120000) {
- newMap.set(fingerprint, timestamp);
- }
- }
- if (newMap.size !== prev.size) {
- console.debug(`清理了 ${prev.size - newMap.size} 个过期的打印请求记录`);
- }
- return newMap;
- });
- }, 300000); // 每5分钟清理一次
- return () => clearInterval(cleanupInterval);
- }, []);
- // 处理搜索
- const handleSearch = () => {
- setSearchParams(prev => ({ ...prev, page: 1 }));
- };
- // 检查交易管理状态
- const handleCheckTradeManaged = async () => {
- try {
- console.debug('开始检查交易管理状态');
- // 这里需要获取当前租户ID,可以从订单数据中获取第一个订单的tenantId
- // 或者使用默认值,这里先使用默认值
- const tenantId = data?.data?.[0]?.tenantId || undefined;
- const response = await fetch('/api/v1/auth/get-is-trade-managed', createAuthFetchOptions({
- method: 'POST',
- body: JSON.stringify({
- tenantId
- })
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('检查交易管理状态失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- let errorMessage = `检查交易管理状态失败: ${response.status}`;
- try {
- const errorData = JSON.parse(errorText);
- if (errorData.message) {
- errorMessage = errorData.message;
- }
- } catch (e) {
- // 如果无法解析为JSON,使用原始文本
- if (errorText) {
- errorMessage = errorText.substring(0, 200);
- }
- }
- toast.error(errorMessage);
- return;
- }
- const result = await response.json();
- console.debug('交易管理状态检查结果:', result);
- if (result.success) {
- const isTradeManaged = result.data?.is_trade_managed;
- const statusText = isTradeManaged ? '已开启' : '未开启';
- toast.success(`交易管理状态: ${statusText}`);
- } else {
- toast.error(result.message || '检查交易管理状态失败');
- }
- } catch (error) {
- console.error('检查交易管理状态时出错:', error);
- toast.error('检查交易管理状态失败,请重试');
- }
- };
- // 处理编辑订单
- const handleEditOrder = (order: OrderResponse) => {
- setEditingOrder(order);
- updateForm.reset({
- state: order.state,
- payState: order.payState,
- remark: order.remark || '',
- });
- setIsModalOpen(true);
- };
- // 处理查看详情
- const handleViewDetails = (order: OrderResponse) => {
- setSelectedOrder(order);
- setDetailModalOpen(true);
- };
- // 处理发货
- const handleDeliveryOrder = async (order: OrderResponse) => {
- setDeliveringOrder(order);
- deliveryForm.reset({
- deliveryType: 1,
- deliveryCompany: '__select__',
- deliveryNo: '',
- deliveryRemark: '',
- });
- // 获取快递公司列表
- setLoadingCompanies(true);
- try {
- const result = await getWechatDeliveryCompanies(order.tenantId);
- const resdata =result.data;
- console.log("result:",result);
- if (resdata.success && resdata.data?.company_list) {
- const companyList = resdata.data.company_list;
- setDeliveryCompanies(companyList);
- // 如果有快递公司列表,自动选中第一个
- if (companyList.length > 0) {
- // 延迟设置,确保组件已经渲染
- setTimeout(() => {
- deliveryForm.setValue('deliveryCompany', companyList[0].delivery_id);
- }, 0);
- }
- } else {
- console.warn('获取快递公司列表失败或为空:', resdata);
- setDeliveryCompanies([]);
- }
- } catch (error) {
- console.error('获取快递公司列表时出错:', error);
- setDeliveryCompanies([]);
- toast.warning('获取快递公司列表失败,请手动输入快递公司名称');
- } finally {
- setLoadingCompanies(false);
- }
- setDeliveryModalOpen(true);
- };
- const handleDeliverySubmit = async (data: DeliveryRequest) => {
- if (!deliveringOrder || !deliveringOrder.id) {
- console.error('发货失败: deliveringOrder或id为空', { deliveringOrder });
- return;
- }
- // 处理快递公司选择值
- if (data.deliveryCompany === '__select__' || data.deliveryCompany === '__manual__') {
- data.deliveryCompany = null;
- }
- // 验证:如果选择物流快递方式,必须填写快递公司
- if (data.deliveryType === 1 && !data.deliveryCompany) {
- toast.error('请选择或输入快递公司');
- return;
- }
- try {
- // console.debug('发货请求数据:', {
- // orderId: deliveringOrder.id,
- // orderNo: deliveringOrder.orderNo,
- // data,
- // orderState: deliveringOrder.state,
- // payState: deliveringOrder.payState
- // });
- // 使用adminOrderClient的$put接口来更新订单发货信息
- // 因为adminDeliveryRoutes没有被挂载到服务器,而adminOrderRoutes已经被正确挂载
- data.deliveryTime = new Date().toISOString();
- // 构建更新数据
- const updateData: any = {
- state: 1, // 已发货状态
- deliveryType: data.deliveryType,
- deliveryTime: data.deliveryTime,
- deliveryRemark: data.deliveryRemark || null,
- };
- // 只有物流快递(deliveryType === 1)才保存物流公司信息和快递单号
- if (data.deliveryType === 1) {
- updateData.deliveryCompany = data.deliveryCompany || null;
- updateData.deliveryNo = data.deliveryNo || null;
- } else {
- // 同城配送、虚拟发货、用户自提等不保存物流信息
- updateData.deliveryCompany = null;
- updateData.deliveryNo = null;
- }
- // console.debug('更新订单数据:', updateData);
- // 调用adminOrderClient的$put接口
- const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
- param: { id: deliveringOrder.id },
- json: updateData,
- });
- // console.debug('发货响应详情:', {
- // status: res.status,
- // statusText: res.statusText,
- // headers: Object.fromEntries(res.headers.entries()),
- // url: res.url
- // });
-
- if (res.status === 200) {
- let result: DeliveryResponse;
- try {
- const responseText = await res.text();
- if (responseText.trim()) {
- try {
- result = JSON.parse(responseText) as DeliveryResponse;
- } catch (parseError) {
- console.error('成功响应JSON解析失败:', parseError);
- result = { success: true, message: '发货成功' } as DeliveryResponse;
- }
- } else {
- result = { success: true, message: '发货成功' } as DeliveryResponse;
- }
- } catch (error) {
- console.error('处理成功响应失败:', error);
- result = { success: true, message: '发货成功' } as DeliveryResponse;
- }
- console.debug('发货成功:', result);
- toast.success(result.message || '发货成功');
- setDeliveryModalOpen(false);
- refetch();
- // 发货成功后根据支付类型决定是否调用微信小程序发货信息录入API
- // 信用支付(payType === 3)不调用,正常支付(payType !== 3)调用
- if (deliveringOrder?.payType !== 3) {
- try {
- const uploadResult = await uploadShippingInfoToWechat(deliveringOrder, data, deliveringOrder?.tenantId);
- if (uploadResult.success) {
- console.debug("微信小程序发货信息录入成功:", uploadResult);
- // 可以在这里添加额外的成功处理,比如记录日志
- } else {
- // 微信小程序发货信息录入失败,记录警告但不影响主流程
- console.warn("微信小程序发货信息录入失败,但系统发货成功:", uploadResult);
- // 可以在这里添加额外的处理,比如记录到日志系统或发送告警
- }
- } catch (uploadError) {
- console.error("调用微信小程序发货信息录入API时发生异常:", uploadError);
- // 不阻止发货成功,只记录错误
- }
- } else if(deliveringOrder?.payType == 3) {
- // 发送微信服务消息通知 - 使用新的配置化设计
- try {
- const notificationResult = await sendDeliverySuccessNotification(deliveringOrder, data);
- // 根据通知结果记录不同的日志
- if (notificationResult.success) {
- console.debug('微信发货通知发送成功:', notificationResult);
- } else if (notificationResult.data?.skipped) {
- // 用户未订阅,这是正常情况,记录为debug级别
- console.debug('用户未订阅发货通知,跳过发送:', {
- userId: deliveringOrder.user?.id,
- username: deliveringOrder.user?.username,
- reason: notificationResult.data.reason
- });
- } else {
- // 其他原因导致的失败,记录为warn级别
- console.warn('微信发货通知发送失败,但发货成功:', notificationResult);
- // 可以在这里添加额外的处理,比如记录到日志系统
- }
- } catch (notificationError) {
- console.error('发送微信发货通知时发生异常:', notificationError);
- // 不阻止发货成功,只记录错误
- }
- }
- } else {
- // 先尝试获取响应文本,避免JSON解析错误
- let errorText = '';
- let errorData: any = null;
- try {
- errorText = await res.text();
- console.debug('发货失败响应文本:', errorText);
- // 尝试解析为JSON
- if (errorText.trim()) {
- try {
- errorData = JSON.parse(errorText);
- } catch (parseError) {
- console.warn('响应不是有效的JSON:', parseError);
- errorData = { message: errorText.substring(0, 100) + '...' };
- }
- }
- } catch (textError) {
- console.error('获取响应文本失败:', textError);
- errorData = { message: `请求失败,状态码: ${res.status}` };
- }
- console.error('发货失败响应:', {
- status: res.status,
- statusText: res.statusText,
- errorData,
- errorText: errorText.substring(0, 200),
- orderId: deliveringOrder.id
- });
- // 显示错误消息
- const errorMessage = errorData?.message ||
- errorData?.error?.message ||
- res.statusText ||
- `发货失败 (${res.status})`;
- toast.error(errorMessage);
- }
- } catch (error) {
- console.error('发货请求异常:', error);
- toast.error('发货失败,请重试');
- }
- };
- // 发货成功微信通知函数 - 使用新的配置化设计
- const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryData: DeliveryRequest): Promise<WechatServiceMessageResult> => {
- // 检查是否有用户信息和openid
- if (!order.user || !order.user.id) {
- console.warn('订单没有用户信息,无法发送微信通知');
- return { success: false, message: '订单没有用户信息,无法发送微信通知' };
- }
- // 检查用户是否有openid(微信小程序用户)
- if (!order.user.openid) {
- console.warn('用户没有绑定微信小程序,无法发送微信通知', {
- userId: order.user.id,
- username: order.user.username
- });
- return { success: false, message: '用户没有绑定微信小程序,无法发送微信通知' };
- }
- // 检查用户是否已订阅发货通知
- if (order.user.hasSubscribedDeliveryNotice !== true) {
- console.debug('用户未订阅发货通知,跳过发送微信通知', {
- userId: order.user.id,
- username: order.user.username,
- hasSubscribedDeliveryNotice: order.user.hasSubscribedDeliveryNotice
- });
- return {
- success: false,
- message: '用户未订阅发货通知,跳过发送微信通知',
- data: { skipped: true, reason: 'user_not_subscribed' }
- };
- }
- console.debug('用户已订阅发货通知,准备发送微信通知', {
- userId: order.user.id,
- username: order.user.username,
- openid: order.user.openid?.substring(0, 10) + '...' // 部分隐藏openid
- });
- // 构建微信服务消息配置 - 参考useShareAppMessage的配置对象模式
- const config: WechatServiceMessageConfig = {
- openid: order.user.openid,
- templateId: 'T00N0Wq3ECjksXSvPWUBgOUukl1TCE7PhxqeDnFPfso', // 发货成功通知模板ID
- page: 'pages/order/detail/index', // 点击跳转到订单详情页
- data: {
- // 根据实际微信模板字段配置
- character_string7: {
- value: `${order.orderNo}`
- },
- date6: {
- value: formatWechatDate(deliveryData.deliveryTime)
- },
- amount9: {
- value: `¥${order.payAmount.toFixed(2)}`
- },
- phrase12: {
- value: deliveryTypeMap[deliveryData.deliveryType as keyof typeof deliveryTypeMap]?.label || '未知'
- },
- thing4: {
- value: (deliveryData.deliveryRemark || '请收到货/提货后及时确认收货,2天后将自动确认收货,如有异常请及时进行交易投诉。').substring(0, 20)
- }
- },
- miniprogramState: 'formal',
- tenantId: order.tenantId // 从订单数据中获取tenantId
- };
- // 调用微信服务消息函数
- return await sendWechatServiceMessage(config);
- };
- // 获取默认打印机
- const getDefaultPrinter = async (tenantId?: number): Promise<{ printerSn: string; printerKey: string } | null> => {
- try {
- console.debug('获取默认打印机,租户ID:', tenantId);
- const response = await fetch('/api/v1/feie/printers?isDefault=true&pageSize=1', createAuthFetchOptions({
- method: 'GET'
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('获取默认打印机失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- return null;
- }
- const result = await response.json();
- console.debug('获取默认打印机结果:', result);
- if (result.success && result.data?.data?.length > 0) {
- const printer = result.data.data[0];
- return {
- printerSn: printer.printerSn,
- printerKey: printer.printerKey
- };
- }
- return null;
- } catch (error) {
- console.error('获取默认打印机时出错:', error);
- return null;
- }
- };
- // 获取打印模板
- const getPrintTemplate = async (tenantId?: number): Promise<string | null> => {
- try {
- console.debug('获取打印模板,租户ID:', tenantId);
- const response = await fetch('/api/v1/feie/config', createAuthFetchOptions({
- method: 'GET'
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('获取打印模板失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- return null;
- }
- const result = await response.json();
- console.debug('获取打印模板结果:', result);
- if (result.success && result.data?.data) {
- const configs = result.data.data;
- const receiptTemplate = configs.find((config: any) => config.configKey === 'feie.receipt_template');
- return receiptTemplate?.configValue || null;
- }
- return null;
- } catch (error) {
- console.error('获取打印模板时出错:', error);
- return null;
- }
- };
- // 触发支付成功事件(测试延迟打印)
- const handleTriggerPaymentSuccess = async (order: OrderResponse) => {
- setTriggeringOrder(order);
- setIsTriggering(true);
- try {
- console.debug('触发支付成功事件,订单ID:', order.id, '租户ID:', order.tenantId);
- // 尝试不同的API路径
- const apiPaths = [
- '/api/v1/payments/payment/trigger-success', // 如果路由注册在 /api/v1/payments
- '/api/v1/payment/trigger-success' // 如果路由注册在 /api/v1
- ];
- let response: Response | null = null;
- let lastError: Error | null = null;
- let successfulPath: string | null = null;
- for (const apiPath of apiPaths) {
- try {
- console.debug('尝试API路径:', apiPath);
- response = await fetch(apiPath, createAuthFetchOptions({
- method: 'POST',
- body: JSON.stringify({
- orderId: order.id
- })
- }));
- // 如果响应不是404,跳出循环
- if (response.status !== 404) {
- successfulPath = apiPath;
- console.debug(`找到有效API路径: ${apiPath}, 状态码: ${response.status}`);
- break;
- }
- console.debug(`路径 ${apiPath} 返回404,尝试下一个路径`);
- } catch (error) {
- lastError = error as Error;
- console.debug(`路径 ${apiPath} 请求失败:`, error);
- }
- }
- if (!response) {
- throw new Error('所有API路径尝试失败');
- }
- if (!response.ok) {
- const errorText = await response.text();
- console.error('触发支付成功事件失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- let errorMessage = `触发失败: ${response.status}`;
- try {
- const errorData = JSON.parse(errorText);
- if (errorData.message) {
- errorMessage = errorData.message;
- }
- } catch (e) {
- // 如果无法解析为JSON,使用原始文本
- if (errorText) {
- errorMessage = errorText.substring(0, 200);
- }
- }
- throw new Error(errorMessage);
- }
- const result: TriggerPaymentSuccessResponse = await response.json();
- if (!result.success) {
- throw new Error(result.message || '触发支付成功事件失败');
- }
- console.debug('触发支付成功事件成功:', {
- path: successfulPath,
- result
- });
- toast.success(`支付成功事件已触发: ${result.message} (路径: ${successfulPath})`);
- } catch (error: any) {
- console.error('触发支付成功事件失败:', error);
- toast.error(`触发失败: ${error.message || '未知错误'}`);
- } finally {
- setIsTriggering(false);
- setTriggeringOrder(null);
- }
- };
- // 处理打印订单
- const handlePrintOrder = async (order: OrderResponse) => {
- // 防止重复点击:如果已经在打印中,直接返回
- if (isPrinting) {
- console.warn('打印任务正在进行中,请勿重复点击');
- toast.warning('打印任务正在进行中,请稍后再试');
- return;
- }
- // 生成请求指纹:订单ID + 时间戳(分钟级),防止短时间内重复提交相同订单
- const requestFingerprint = `${order.id}_${Math.floor(Date.now() / 60000)}`; // 每分钟一个唯一标识
- // 检查是否在最近1分钟内提交过相同的打印请求
- const recentRequestTime = recentPrintRequests.get(requestFingerprint);
- if (recentRequestTime && Date.now() - recentRequestTime < 60000) {
- console.warn('检测到重复打印请求,短时间内请勿重复提交', {
- orderId: order.id,
- fingerprint: requestFingerprint,
- lastRequestTime: new Date(recentRequestTime).toISOString()
- });
- toast.warning('请勿重复提交打印请求,请等待1分钟后再试');
- return;
- }
- // 记录当前请求
- setRecentPrintRequests(prev => {
- const newMap = new Map(prev);
- newMap.set(requestFingerprint, Date.now());
- return newMap;
- });
- setPrintingOrder(order);
- setIsPrinting(true);
- try {
- // 获取默认打印机
- const defaultPrinter = await getDefaultPrinter(order.tenantId);
- if (!defaultPrinter) {
- throw new Error('未找到默认打印机,请先设置默认打印机');
- }
- // 获取打印模板
- const template = await getPrintTemplate(order.tenantId);
- // 构建打印内容
- const printContent = generatePrintContent(order, template);
- // 构建打印请求
- const printRequest: SubmitPrintTaskRequest = {
- printerSn: defaultPrinter.printerSn,
- content: printContent,
- printType: 'RECEIPT', // 收据类型
- orderId: order.id,
- delaySeconds: 0 // 立即打印
- };
- console.debug('提交打印任务:', {
- orderId: order.id,
- orderNo: order.orderNo,
- printerSn: defaultPrinter.printerSn,
- printRequest
- });
- // 使用fetch API提交打印任务
- const response = await fetch('/api/v1/feie/tasks', createAuthFetchOptions({
- method: 'POST',
- body: JSON.stringify(printRequest)
- }));
- if (!response.ok) {
- const errorText = await response.text();
- console.error('打印任务提交失败:', {
- status: response.status,
- statusText: response.statusText,
- error: errorText
- });
- let errorMessage = `打印失败: ${response.status}`;
- try {
- const errorData = JSON.parse(errorText);
- if (errorData.message) {
- errorMessage = errorData.message;
- }
- } catch (e) {
- // 如果无法解析为JSON,使用原始文本
- if (errorText) {
- errorMessage = errorText.substring(0, 200);
- }
- }
- throw new Error(errorMessage);
- }
- const result: ApiResponse<SubmitPrintTaskResponse> = await response.json();
- if (!result.success) {
- throw new Error(result.message || '打印任务提交失败');
- }
- console.debug('打印任务提交成功:', result.data);
- toast.success(`打印任务已提交,任务ID: ${result.data?.taskId}`);
- } catch (error: any) {
- console.error('打印订单失败:', error);
- toast.error(`打印失败: ${error.message || '未知错误'}`);
- } finally {
- setIsPrinting(false);
- setPrintingOrder(null);
- }
- };
- // 生成打印内容
- const generatePrintContent = (order: OrderResponse, template?: string | null): string => {
- // 如果没有模板或模板为空,使用默认模板
- if (!template) {
- template = `
- <CB>订单收据</CB>
- <BR>
- 订单号: {orderNo}
- 下单时间: {orderTime}
- <BR>
- <B>收货信息</B>
- 收货人: {receiverName}
- 联系电话: {receiverPhone}
- 收货地址: {address}
- <BR>
- <B>商品信息</B>
- {goodsList}
- <BR>
- <B>费用明细</B>
- 商品总额: {totalAmount}
- 运费: {freightAmount}
- 实付金额: {payAmount}
- <BR>
- <B>订单状态</B>
- 订单状态: {orderStatus}
- 支付状态: {payStatus}
- <BR>
- <B>订单备注</B>
- {remark}
- <BR>
- <C>感谢您的惠顾!</C>
- <BR>
- <QR>{orderNo}</QR>
- `;
- }
- // 准备模板变量
- const variables = {
- orderNo: order.orderNo,
- orderTime: format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm:ss'),
- receiverName: order.recevierName || '-',
- receiverPhone: order.receiverMobile || '-',
- phone: order.receiverMobile || '-', // 添加phone变量,兼容两种模板格式
- address: order.address || '-',
- goodsList: order.orderGoods?.map(item =>
- `${item.goodsName} ${item.price} × ${item.num} = ${(item.price * item.num).toFixed(2)}`
- ).join('\n') || '暂无商品信息',
- totalAmount: `${order.amount.toFixed(2)}`,
- freightAmount: `${order.freightAmount.toFixed(2)}`,
- payAmount: `${order.payAmount.toFixed(2)}`,
- orderStatus: orderStatusMap[order.state as keyof typeof orderStatusMap]?.label || '未知',
- payStatus: payStatusMap[order.payState as keyof typeof payStatusMap]?.label || '未知',
- remark: order.remark || '无备注'
- };
- // 替换模板变量 - 使用更健壮的替换方法
- let content = template;
- // 方法1: 标准替换
- for (const [key, value] of Object.entries(variables)) {
- // 处理多种格式的占位符:{key}、{ key }、{{key}}等
- const patterns = [
- `{${key}}`, // 标准格式
- `{ ${key} }`, // 有空格
- `{{${key}}}`, // 双大括号
- `{{ ${key} }}`, // 双大括号有空格
- `{${key} }`, // 右空格
- `{ ${key}}`, // 左空格
- ];
- for (const pattern of patterns) {
- if (content.includes(pattern)) {
- content = content.split(pattern).join(value);
- }
- }
- }
- // 方法2: 清理未替换的变量(特别是remark)
- // 使用正则表达式匹配各种格式的{remark}
- const remarkPatterns = [
- /\{remark\}/g,
- /\{\s*remark\s*\}/g,
- /\{\{remark\}\}/g,
- /\{\{\s*remark\s*\}\}/g,
- ];
- for (const pattern of remarkPatterns) {
- if (pattern.test(content)) {
- const safeRemark = variables.remark || '无备注';
- content = content.replace(pattern, safeRemark);
- }
- }
- return content.trim();
- };
- // 处理更新订单
- const handleUpdateSubmit = async (data: UpdateRequest) => {
- if (!editingOrder || !editingOrder.id) return;
- try {
- const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
- param: { id: editingOrder.id },
- json: data,
- });
- if (res.status === 200) {
- toast.success('订单更新成功');
- setIsModalOpen(false);
- refetch();
- } else {
- const error = await res.json();
- toast.error(error.message || '更新失败');
- }
- } catch (error) {
- console.error('更新订单失败:', error);
- toast.error('更新失败,请重试');
- }
- };
- // 格式化金额
- const formatAmount = (amount: number) => {
- return `¥${amount.toFixed(2)}`;
- };
- // 获取状态颜色
- const getStatusBadge = (status: number, type: 'order' | 'pay' | 'delivery') => {
- const map = type === 'order' ? orderStatusMap : type === 'pay' ? payStatusMap : deliveryTypeMap;
- const config = map[status as keyof typeof map] || { label: '未知', color: 'default' };
- return <Badge variant={config.color as any}>{config.label}</Badge>;
- };
- // 骨架屏 - 只覆盖表格区域,搜索区域保持可用
- if (isLoading) {
- return (
- <div className="space-y-4">
- {/* 页面标题 */}
- <div className="flex justify-between items-center">
- <div>
- <h1 className="text-2xl font-bold">订单管理</h1>
- <p className="text-muted-foreground">管理所有订单信息</p>
- </div>
- </div>
- {/* 搜索区域 - 保持可用 */}
- <Card>
- <CardHeader>
- <CardTitle>订单列表</CardTitle>
- <CardDescription>查看和管理所有订单</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="flex gap-4 mb-4">
- <div className="relative flex-1 max-w-sm">
- <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="搜索订单号、手机号、收货人姓名..."
- value={searchParams.search}
- onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
- className="pl-8"
- data-testid="order-search-input"
- />
- </div>
- <Select
- value={searchParams.status}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-status-select">
- <SelectValue placeholder="订单状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未发货</SelectItem>
- <SelectItem value="1">已发货</SelectItem>
- <SelectItem value="2">收货成功</SelectItem>
- <SelectItem value="3">已退货</SelectItem>
- </SelectContent>
- </Select>
- <Select
- value={searchParams.payStatus}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-pay-status-select">
- <SelectValue placeholder="支付状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未支付</SelectItem>
- <SelectItem value="1">支付中</SelectItem>
- <SelectItem value="2">支付成功</SelectItem>
- <SelectItem value="3">已退款</SelectItem>
- <SelectItem value="4">支付失败</SelectItem>
- <SelectItem value="5">订单关闭</SelectItem>
- </SelectContent>
- </Select>
- <Button onClick={handleSearch} data-testid="order-search-button">
- <Search className="h-4 w-4 mr-2" />
- 搜索
- </Button>
- </div>
- {/* 表格骨架屏 */}
- <div className="rounded-md border">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>订单号</TableHead>
- <TableHead>用户信息</TableHead>
- <TableHead>收货人</TableHead>
- <TableHead>金额</TableHead>
- <TableHead>订单状态</TableHead>
- <TableHead>支付状态</TableHead>
- <TableHead>创建时间</TableHead>
- <TableHead className="text-right">操作</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {[...Array(5)].map((_, i) => (
- <TableRow key={i}>
- <TableCell>
- <Skeleton className="h-4 w-32" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-24" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-20" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-16" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-6 w-16" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-6 w-16" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-24" />
- </TableCell>
- <TableCell className="text-right">
- <div className="flex justify-end gap-2">
- <Skeleton className="h-8 w-8" />
- <Skeleton className="h-8 w-8" />
- <Skeleton className="h-8 w-8" />
- </div>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- {/* 分页骨架屏 */}
- <div className="flex items-center justify-between px-2 py-4">
- <Skeleton className="h-4 w-32" />
- <div className="flex items-center space-x-2">
- <Skeleton className="h-8 w-16" />
- <Skeleton className="h-4 w-24" />
- <Skeleton className="h-8 w-16" />
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- );
- }
- return (
- <div className="space-y-4">
- {/* 页面标题 */}
- <div className="flex justify-between items-center">
- <div>
- <h1 className="text-2xl font-bold">订单管理</h1>
- <p className="text-muted-foreground">管理所有订单信息</p>
- </div>
- {/* <Button
- variant="outline"
- onClick={() => handleCheckTradeManaged()}
- data-testid="check-trade-managed-button"
- >
- 检查交易管理状态
- </Button> */}
- </div>
- {/* 搜索区域 */}
- <Card>
- <CardHeader>
- <CardTitle>订单列表</CardTitle>
- <CardDescription>查看和管理所有订单</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="flex gap-4 mb-4">
- <div className="relative flex-1 max-w-sm">
- <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="搜索订单号、手机号、收货人姓名..."
- value={searchParams.search}
- onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
- className="pl-8"
- data-testid="order-search-input"
- />
- </div>
- <Select
- value={searchParams.status}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-status-select">
- <SelectValue placeholder="订单状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未发货</SelectItem>
- <SelectItem value="1">已发货</SelectItem>
- <SelectItem value="2">收货成功</SelectItem>
- <SelectItem value="3">已退货</SelectItem>
- </SelectContent>
- </Select>
- <Select
- value={searchParams.payStatus}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-pay-status-select">
- <SelectValue placeholder="支付状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未支付</SelectItem>
- <SelectItem value="1">支付中</SelectItem>
- <SelectItem value="2">支付成功</SelectItem>
- <SelectItem value="3">已退款</SelectItem>
- <SelectItem value="4">支付失败</SelectItem>
- <SelectItem value="5">订单关闭</SelectItem>
- </SelectContent>
- </Select>
- <Button onClick={handleSearch} data-testid="order-search-button">
- <Search className="h-4 w-4 mr-2" />
- 搜索
- </Button>
- </div>
- {/* 数据表格 */}
- <div className="rounded-md border">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>订单号</TableHead>
- <TableHead>用户信息</TableHead>
- <TableHead>收货人</TableHead>
- <TableHead>金额</TableHead>
- <TableHead>订单状态</TableHead>
- <TableHead>支付状态</TableHead>
- <TableHead>创建时间</TableHead>
- <TableHead className="text-right">操作</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {data?.data.map((order) => (
- <TableRow key={order.id}>
- <TableCell>
- <div>
- <p className="font-medium">{order.orderNo}</p>
- <p className="text-sm text-muted-foreground">
- {orderTypeMap[order.orderType as keyof typeof orderTypeMap]?.label}
- </p>
- </div>
- </TableCell>
- <TableCell>
- <div>
- <p>{order.user?.username || '-'}</p>
- <p className="text-sm text-muted-foreground">{order.userPhone}</p>
- </div>
- </TableCell>
- <TableCell>
- <div>
- <p>{order.recevierName || '-'}</p>
- <p className="text-sm text-muted-foreground">{order.receiverMobile}</p>
- </div>
- </TableCell>
- <TableCell>
- <div>
- <p className="font-medium">{formatAmount(order.payAmount)}</p>
- <p className="text-sm text-muted-foreground">{formatAmount(order.amount)}</p>
- </div>
- </TableCell>
- <TableCell>{getStatusBadge(order.state, 'order')}</TableCell>
- <TableCell>{getStatusBadge(order.payState, 'pay')}</TableCell>
- <TableCell>
- {format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm')}
- </TableCell>
- <TableCell className="text-right">
- <div className="flex justify-end gap-2">
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handlePrintOrder(order)}
- disabled={isPrinting}
- data-testid="order-print-button"
- >
- {isPrinting && printingOrder?.id === order.id ? (
- <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
- ) : (
- <Printer className="h-4 w-4" />
- )}
- </Button>
- {/* <Button
- variant="ghost"
- size="icon"
- onClick={() => handleTriggerPaymentSuccess(order)}
- disabled={isTriggering && triggeringOrder?.id === order.id}
- data-testid="order-trigger-payment-button"
- title="触发支付成功事件(测试延迟打印)"
- >
- {isTriggering && triggeringOrder?.id === order.id ? (
- <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
- ) : (
- <Play className="h-4 w-4" />
- )}
- </Button> */}
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handleViewDetails(order)}
- data-testid="order-view-button"
- >
- <Eye className="h-4 w-4" />
- </Button>
- {order.state === 0 && order.payState === 2 && (
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handleDeliveryOrder(order)}
- data-testid="order-delivery-button"
- >
- <Truck className="h-4 w-4" />
- </Button>
- )}
-
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handleEditOrder(order)}
- data-testid="order-edit-button"
- >
- <Edit className="h-4 w-4" />
- </Button>
- </div>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- {data?.data.length === 0 && !isLoading && (
- <div className="text-center py-8">
- <p className="text-muted-foreground">暂无订单数据</p>
- </div>
- )}
- <DataTablePagination
- currentPage={searchParams.page}
- pageSize={searchParams.limit}
- totalCount={data?.pagination.total || 0}
- onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
- />
- </CardContent>
- </Card>
- {/* 编辑订单模态框 */}
- <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
- <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>编辑订单</DialogTitle>
- <DialogDescription>更新订单状态和备注信息</DialogDescription>
- </DialogHeader>
- <Form {...updateForm}>
- <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
- <FormField
- control={updateForm.control}
- name="state"
- render={({ field }) => (
- <FormItem>
- <FormLabel>订单状态</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
- <FormControl>
- <SelectTrigger data-testid="edit-order-status-select">
- <SelectValue placeholder="选择订单状态" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="0">未发货</SelectItem>
- <SelectItem value="1">已发货</SelectItem>
- <SelectItem value="2">收货成功</SelectItem>
- <SelectItem value="3">已退货</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={updateForm.control}
- name="payState"
- render={({ field }) => (
- <FormItem>
- <FormLabel>支付状态</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
- <FormControl>
- <SelectTrigger data-testid="edit-pay-status-select">
- <SelectValue placeholder="选择支付状态" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="0">未支付</SelectItem>
- <SelectItem value="1">支付中</SelectItem>
- <SelectItem value="2">支付成功</SelectItem>
- <SelectItem value="3">已退款</SelectItem>
- <SelectItem value="4">支付失败</SelectItem>
- <SelectItem value="5">订单关闭</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={updateForm.control}
- name="remark"
- render={({ field }) => (
- <FormItem>
- <FormLabel>客户备注</FormLabel>
- <FormControl>
- <Textarea
- placeholder="输入客户备注信息..."
- className="resize-none"
- data-testid="edit-remark-textarea"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter>
- <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
- 取消
- </Button>
- <Button type="submit" data-testid="order-save-button">保存</Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- {/* 订单详情模态框 */}
- <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
- <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>订单详情</DialogTitle>
- <DialogDescription>查看订单的详细信息</DialogDescription>
- </DialogHeader>
- {selectedOrder && (
- <div className="space-y-4">
- <div className="grid grid-cols-2 gap-4">
- <div>
- <h4 className="font-medium mb-2">订单信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单号:</span>
- <span>{selectedOrder.orderNo}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单类型:</span>
- <span>{orderTypeMap[selectedOrder.orderType as keyof typeof orderTypeMap]?.label}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单金额:</span>
- <span>{formatAmount(selectedOrder.amount)}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">实付金额:</span>
- <span>{formatAmount(selectedOrder.payAmount)}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">运费:</span>
- <span>{formatAmount(selectedOrder.freightAmount)}</span>
- </div>
- </div>
- </div>
- <div>
- <h4 className="font-medium mb-2">状态信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单状态:</span>
- <span>{getStatusBadge(selectedOrder.state, 'order')}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">支付状态:</span>
- <span>{getStatusBadge(selectedOrder.payState, 'pay')}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">支付方式:</span>
- <span>{selectedOrder.payType === 1 ? '积分' : selectedOrder.payType === 2 ? '礼券' : '未选择'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">创建时间:</span>
- <span>{format(new Date(selectedOrder.createdAt), 'yyyy-MM-dd HH:mm')}</span>
- </div>
- </div>
- </div>
- </div>
- <div className="grid grid-cols-2 gap-4">
- <div>
- <h4 className="font-medium mb-2">用户信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">用户名:</span>
- <span>{selectedOrder.user?.username || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">手机号:</span>
- <span>{selectedOrder.userPhone || '-'}</span>
- </div>
- </div>
- </div>
- <div>
- <h4 className="font-medium mb-2">收货信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">收货人:</span>
- <span>{selectedOrder.recevierName || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">手机号:</span>
- <span>{selectedOrder.receiverMobile || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">地址:</span>
- <span>{selectedOrder.address || '-'}</span>
- </div>
- </div>
- </div>
- </div>
- {/* 订单商品信息 */}
- <div>
- <h4 className="font-medium mb-3 flex items-center gap-2">
- <Package className="h-4 w-4" />
- 订单商品
- </h4>
- <div className="border rounded-md overflow-hidden">
- <div className="bg-muted px-4 py-2 border-b">
- <div className="grid grid-cols-12 gap-4 text-sm font-medium">
- <div className="col-span-5">商品信息</div>
- <div className="col-span-2 text-center">单价</div>
- <div className="col-span-2 text-center">数量</div>
- <div className="col-span-3 text-right">小计</div>
- </div>
- </div>
- <div className="divide-y">
- {selectedOrder.orderGoods?.map((item, index) => (
- <div key={item.id || index} className="px-4 py-3">
- <div className="grid grid-cols-12 gap-4 items-center">
- <div className="col-span-5 flex items-center gap-3">
- {item.imageFile && (
- <img
- src={item.imageFile.fullUrl}
- alt={item.goodsName}
- className="w-12 h-12 rounded-md object-cover"
- />
- )}
- <div>
- <p className="font-medium text-sm">{item.goodsName}</p>
- </div>
- </div>
- <div className="col-span-2 text-center text-sm">
- {formatAmount(item.price)}
- </div>
- <div className="col-span-2 text-center text-sm">
- {item.num}
- </div>
- <div className="col-span-3 text-right text-sm font-medium">
- {formatAmount(item.price * item.num)}
- </div>
- </div>
- </div>
- ))}
- </div>
- {selectedOrder.orderGoods?.length === 0 && (
- <div className="text-center py-8 text-muted-foreground">
- 暂无商品信息
- </div>
- )}
- </div>
- </div>
- {selectedOrder.remark && (
- <div>
- <h4 className="font-medium mb-2">客户备注</h4>
- <p className="text-sm bg-muted p-3 rounded-md">{selectedOrder.remark}</p>
- </div>
- )}
- </div>
- )}
- </DialogContent>
- </Dialog>
- {/* 发货模态框 */}
- <Dialog open={deliveryModalOpen} onOpenChange={setDeliveryModalOpen}>
- <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>订单发货</DialogTitle>
- <DialogDescription>选择发货方式并填写发货信息</DialogDescription>
- </DialogHeader>
- {deliveringOrder && (
- <div className="space-y-4">
- <div className="bg-muted p-3 rounded-md">
- <h4 className="font-medium mb-2">订单信息</h4>
- <div className="text-sm space-y-1">
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单号:</span>
- <span>{deliveringOrder.orderNo}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">收货人:</span>
- <span>{deliveringOrder.recevierName || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">手机号:</span>
- <span>{deliveringOrder.receiverMobile || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">地址:</span>
- <span>{deliveringOrder.address || '-'}</span>
- </div>
- </div>
- </div>
- <Form {...deliveryForm}>
- <form onSubmit={deliveryForm.handleSubmit(handleDeliverySubmit)} className="space-y-4">
- <FormField
- control={deliveryForm.control}
- name="deliveryType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>发货方式</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
- <FormControl>
- <SelectTrigger data-testid="delivery-type-select">
- <SelectValue placeholder="选择发货方式" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="1">物流快递</SelectItem>
- <SelectItem value="2">同城配送</SelectItem>
- <SelectItem value="3">虚拟发货</SelectItem>
- <SelectItem value="4">用户自提</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- {deliveryForm.watch('deliveryType') === 1 && (
- <>
- <FormField
- control={deliveryForm.control}
- name="deliveryCompany"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>快递公司</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- className="w-full justify-between"
- disabled={loadingCompanies}
- data-testid="delivery-company-select"
- >
- {field.value === '__manual__'
- ? "手动输入"
- : field.value && field.value !== '__select__'
- ? deliveryCompanies.find(
- (company) => company.delivery_id === field.value
- )?.delivery_name || field.value // 如果是手动输入的名称,显示名称
- : loadingCompanies
- ? "加载中..."
- : "选择快递公司"}
- <Search className="ml-2 h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0">
- <Command>
- <CommandInput
- placeholder="搜索快递公司..."
- className="h-9"
- />
- <CommandList>
- <CommandEmpty>未找到匹配的快递公司</CommandEmpty>
- <CommandGroup>
- {deliveryCompanies.map((company) => (
- <CommandItem
- key={company.delivery_id}
- value={company.delivery_name} // 搜索时使用名称
- onSelect={() => {
- field.onChange(company.delivery_id); // 存储ID
- }}
- >
- {company.delivery_name}
- {company.delivery_id === field.value && (
- <Check className="ml-auto h-4 w-4" />
- )}
- </CommandItem>
- ))}
- </CommandGroup>
- {deliveryCompanies.length === 0 && !loadingCompanies && (
- <>
- <CommandSeparator />
- <CommandGroup>
- <CommandItem
- value="手动输入"
- onSelect={() => {
- // 切换到手动输入模式,使用特殊值标识
- field.onChange('__manual__');
- }}
- className="text-muted-foreground"
- >
- 手动输入快递公司名称
- </CommandItem>
- </CommandGroup>
- </>
- )}
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- {/* 手动输入框 */}
- {field.value === '__manual__' && (
- <div className="mt-2">
- <div className="flex gap-2 mb-2">
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={() => field.onChange('__select__')}
- >
- ← 返回选择列表
- </Button>
- </div>
- <FormControl>
- <Input
- placeholder="请输入快递公司名称"
- data-testid="delivery-company-manual-input"
- onChange={(e) => {
- // 手动输入时,我们需要存储公司名称
- // 但为了区分,我们可以存储为对象或特殊格式
- // 这里简单处理:如果输入不为空,存储为字符串
- field.onChange(e.target.value);
- }}
- />
- </FormControl>
- </div>
- )}
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={deliveryForm.control}
- name="deliveryNo"
- render={({ field }) => (
- <FormItem>
- <FormLabel>快递单号</FormLabel>
- <FormControl>
- <Input
- placeholder="输入快递单号"
- data-testid="delivery-no-input"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </>
- )}
- <FormField
- control={deliveryForm.control}
- name="deliveryRemark"
- render={({ field }) => (
- <FormItem>
- <FormLabel>发货备注</FormLabel>
- <FormControl>
- <Textarea
- placeholder="输入发货备注信息..."
- className="resize-none"
- data-testid="delivery-remark-textarea"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter>
- <Button type="button" variant="outline" onClick={() => setDeliveryModalOpen(false)}>
- 取消
- </Button>
- <Button type="submit" data-testid="delivery-submit-button">确认发货</Button>
- </DialogFooter>
- </form>
- </Form>
- </div>
- )}
- </DialogContent>
- </Dialog>
- </div>
- );
- };
|