OrderManagement.tsx 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155
  1. import { useState, useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { useForm } from 'react-hook-form';
  4. import { zodResolver } from '@hookform/resolvers/zod';
  5. import { format } from 'date-fns';
  6. import { toast } from 'sonner';
  7. import { Search, Edit, Eye, Package, Truck, Check, Printer, Play } from 'lucide-react';
  8. // 获取认证token的工具函数
  9. const getAuthToken = (): string | null => {
  10. if (typeof window !== 'undefined') {
  11. return localStorage.getItem('token');
  12. }
  13. return null;
  14. };
  15. // 创建带认证头的fetch选项
  16. const createAuthFetchOptions = (options: RequestInit = {}): RequestInit => {
  17. const token = getAuthToken();
  18. const headers = new Headers(options.headers);
  19. if (token) {
  20. headers.set('Authorization', `Bearer ${token}`);
  21. }
  22. // 只有POST/PUT/PATCH请求才需要设置Content-Type
  23. const method = options.method?.toUpperCase() || 'GET';
  24. if (['POST', 'PUT', 'PATCH'].includes(method) && !headers.has('Content-Type')) {
  25. headers.set('Content-Type', 'application/json');
  26. }
  27. return {
  28. ...options,
  29. headers
  30. };
  31. };
  32. // 使用共享UI组件包的具体路径导入
  33. import { Button } from '@d8d/shared-ui-components/components/ui/button';
  34. import { Input } from '@d8d/shared-ui-components/components/ui/input';
  35. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
  36. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
  37. import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
  38. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
  39. import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
  40. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
  41. import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea';
  42. import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
  43. import {
  44. Command,
  45. CommandEmpty,
  46. CommandGroup,
  47. CommandInput,
  48. CommandItem,
  49. CommandList,
  50. CommandSeparator,
  51. } from '@d8d/shared-ui-components/components/ui/command';
  52. import { Popover, PopoverContent, PopoverTrigger } from '@d8d/shared-ui-components/components/ui/popover';
  53. // 简单分页组件
  54. const DataTablePagination = ({
  55. currentPage,
  56. pageSize,
  57. totalCount,
  58. onPageChange
  59. }: {
  60. currentPage: number;
  61. pageSize: number;
  62. totalCount: number;
  63. onPageChange: (page: number, limit: number) => void;
  64. }) => {
  65. const totalPages = Math.ceil(totalCount / pageSize);
  66. return (
  67. <div className="flex items-center justify-between px-2 py-4">
  68. <div className="text-sm text-muted-foreground">
  69. 共 {totalCount} 条记录
  70. </div>
  71. <div className="flex items-center space-x-2">
  72. <Button
  73. variant="outline"
  74. size="sm"
  75. onClick={() => onPageChange(Math.max(1, currentPage - 1), pageSize)}
  76. disabled={currentPage <= 1}
  77. >
  78. 上一页
  79. </Button>
  80. <div className="text-sm">
  81. 第 {currentPage} 页,共 {totalPages} 页
  82. </div>
  83. <Button
  84. variant="outline"
  85. size="sm"
  86. onClick={() => onPageChange(Math.min(totalPages, currentPage + 1), pageSize)}
  87. disabled={currentPage >= totalPages}
  88. >
  89. 下一页
  90. </Button>
  91. </div>
  92. </div>
  93. );
  94. };
  95. import { adminOrderClient, orderClientManager } from '../api';
  96. import type { InferResponseType } from 'hono/client';
  97. import { UpdateOrderDto } from '@d8d/orders-module-mt/schemas';
  98. // 飞鹅打印相关类型定义
  99. interface SubmitPrintTaskRequest {
  100. printerSn: string;
  101. content: string;
  102. printType: string;
  103. orderId?: number;
  104. delaySeconds?: number;
  105. }
  106. interface SubmitPrintTaskResponse {
  107. taskId: string;
  108. status: string;
  109. scheduledAt?: string;
  110. }
  111. // 触发支付成功事件响应类型
  112. interface TriggerPaymentSuccessResponse {
  113. success: boolean;
  114. message: string;
  115. orderId: number;
  116. tenantId: number;
  117. }
  118. interface ApiResponse<T> {
  119. success: boolean;
  120. data?: T;
  121. message?: string;
  122. error?: string;
  123. }
  124. // 类型定义
  125. type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>['data'][0] & {
  126. user?: {
  127. id: number;
  128. username: string;
  129. phone: string | null;
  130. openid?: string | null; // 添加openid字段
  131. hasSubscribedDeliveryNotice?: boolean; // 添加订阅状态字段
  132. } | null;
  133. tenantId?: number; // 添加tenantId字段
  134. };
  135. type UpdateRequest = any;
  136. // 发货请求类型 - 使用UpdateOrderDto的类型
  137. type DeliveryRequest = {
  138. deliveryType: number;
  139. deliveryCompany?: string | null;
  140. deliveryNo?: string | null;
  141. deliveryRemark?: string | null;
  142. deliveryTime?: string | null;
  143. };
  144. type DeliveryResponse = any;
  145. // 微信服务消息通知配置类型 - 参考useShareAppMessage的设计模式
  146. interface WechatServiceMessageConfig {
  147. openid: string;
  148. templateId: string;
  149. page?: string;
  150. data: Record<string, { value: string }>;
  151. miniprogramState?: 'developer' | 'trial' | 'formal';
  152. tenantId?: number; // 添加tenantId参数
  153. }
  154. // 微信服务消息通知结果类型
  155. interface WechatServiceMessageResult {
  156. success: boolean;
  157. message: string;
  158. data?: any;
  159. error?: any;
  160. }
  161. // 微信服务消息通知函数 - 参考useShareAppMessage的简洁设计模式
  162. const sendWechatServiceMessage = async (config: WechatServiceMessageConfig): Promise<WechatServiceMessageResult> => {
  163. try {
  164. console.debug('准备发送微信服务消息:', config);
  165. // 调用后端微信API
  166. const response = await fetch('/api/v1/auth/send-template-message', createAuthFetchOptions({
  167. method: 'POST',
  168. body: JSON.stringify({
  169. openid: config.openid,
  170. templateId: config.templateId,
  171. data: config.data,
  172. page: config.page || 'pages/index/index',
  173. miniprogramState: config.miniprogramState || 'formal',
  174. tenantId: config.tenantId
  175. })
  176. }));
  177. if (!response.ok) {
  178. const errorText = await response.text();
  179. console.error('微信服务消息API调用失败:', {
  180. status: response.status,
  181. statusText: response.statusText,
  182. error: errorText
  183. });
  184. return {
  185. success: false,
  186. message: `微信服务消息发送失败: ${response.status}`,
  187. error: errorText
  188. };
  189. }
  190. const result = await response.json();
  191. console.debug('微信服务消息发送成功:', result);
  192. return {
  193. success: true,
  194. message: '微信服务消息发送成功',
  195. data: result
  196. };
  197. } catch (error) {
  198. console.error('发送微信服务消息时出错:', error);
  199. return {
  200. success: false,
  201. message: '微信服务消息发送失败',
  202. error
  203. };
  204. }
  205. };
  206. // 格式化微信日期时间
  207. const formatWechatDate = (isoDateString: string | null | undefined): string => {
  208. try {
  209. // 如果日期字符串为空,使用当前时间
  210. const date = isoDateString ? new Date(isoDateString) : new Date();
  211. // 微信date类型格式:YYYY年MM月DD日 HH:mm
  212. const year = date.getFullYear();
  213. const month = String(date.getMonth() + 1).padStart(2, '0');
  214. const day = String(date.getDate()).padStart(2, '0');
  215. const hours = String(date.getHours()).padStart(2, '0');
  216. const minutes = String(date.getMinutes()).padStart(2, '0');
  217. return `${year}年${month}月${day}日 ${hours}:${minutes}`;
  218. } catch (error) {
  219. console.error('格式化微信日期失败:', error, isoDateString);
  220. // 返回当前时间的格式化版本作为备用
  221. const now = new Date();
  222. const year = now.getFullYear();
  223. const month = String(now.getMonth() + 1).padStart(2, '0');
  224. const day = String(now.getDate()).padStart(2, '0');
  225. const hours = String(now.getHours()).padStart(2, '0');
  226. const minutes = String(now.getMinutes()).padStart(2, '0');
  227. return `${year}年${month}月${day}日 ${hours}:${minutes}`;
  228. }
  229. };
  230. // 快递公司列表
  231. const getWechatDeliveryCompanies = async (tenantId?: number): Promise<{ success: boolean; message: string; data?: any; error?: any }> => {
  232. try {
  233. console.debug('准备获取微信小店快递公司列表:', { tenantId });
  234. // 调用后端获取快递公司列表API
  235. const response = await fetch('/api/v1/auth/get-delivery-companies', createAuthFetchOptions({
  236. method: 'POST',
  237. body: JSON.stringify({
  238. tenantId
  239. })
  240. }));
  241. if (!response.ok) {
  242. const errorText = await response.text();
  243. console.error('获取快递公司列表API调用失败:', {
  244. status: response.status,
  245. statusText: response.statusText,
  246. error: errorText
  247. });
  248. let errorMessage = `获取快递公司列表失败: ${response.status}`;
  249. try {
  250. const errorData = JSON.parse(errorText);
  251. if (errorData.message) {
  252. errorMessage = errorData.message;
  253. }
  254. if (errorData.error?.message) {
  255. errorMessage = errorData.error.message;
  256. }
  257. } catch (e) {
  258. // 如果无法解析为JSON,使用原始文本
  259. if (errorText) {
  260. errorMessage = errorText.substring(0, 200);
  261. }
  262. }
  263. return {
  264. success: false,
  265. message: errorMessage,
  266. error: errorText
  267. };
  268. }
  269. const result = await response.json();
  270. console.debug('获取快递公司列表成功:', result);
  271. return {
  272. success: true,
  273. message: '获取快递公司列表成功',
  274. data: result
  275. };
  276. } catch (error) {
  277. console.error('调用获取快递公司列表API时出错:', error);
  278. return {
  279. success: false,
  280. message: '获取快递公司列表失败',
  281. error
  282. };
  283. }
  284. };
  285. // 生成商品描述信息,截取110个字
  286. const generateItemDesc = (order: OrderResponse): string => {
  287. if (!order.orderGoods || order.orderGoods.length === 0) {
  288. return '商品信息';
  289. }
  290. // 构建商品描述:商品1名称×数量, 商品2名称×数量, ...
  291. const itemDescriptions = order.orderGoods.map(item => {
  292. return `${item.goodsName}×${item.num}`;
  293. });
  294. let itemDesc = itemDescriptions.join(', ');
  295. // 截取110个字(中文字符)
  296. if (itemDesc.length > 110) {
  297. itemDesc = itemDesc.substring(0, 110) + '...';
  298. }
  299. console.debug('生成的商品描述:', {
  300. originalLength: itemDescriptions.join(', ').length,
  301. truncatedLength: itemDesc.length,
  302. itemDesc
  303. });
  304. return itemDesc;
  305. };
  306. // 调用微信小程序发货信息录入API
  307. const uploadShippingInfoToWechat = async (
  308. order: OrderResponse,
  309. deliveryData: DeliveryRequest,
  310. tenantId?: number
  311. ): Promise<{ success: boolean; message: string; data?: any; error?: any }> => {
  312. try {
  313. // 生成商品描述
  314. const itemDesc = generateItemDesc(order);
  315. console.debug('准备调用微信小程序发货信息录入API:', {
  316. orderId: order.id,
  317. orderNo: order.orderNo,
  318. deliveryType: deliveryData.deliveryType,
  319. deliveryCompany: deliveryData.deliveryCompany,
  320. deliveryNo: deliveryData.deliveryNo,
  321. itemDesc,
  322. tenantId: tenantId || order.tenantId
  323. });
  324. // 根据发货类型准备参数
  325. let expressInfo = undefined;
  326. let localDeliveryInfo = undefined;
  327. switch (deliveryData.deliveryType) {
  328. case 1: // 物流快递
  329. if (deliveryData.deliveryCompany && deliveryData.deliveryNo) {
  330. // deliveryCompany现在存储的是delivery_id
  331. expressInfo = {
  332. deliveryId: deliveryData.deliveryCompany, // 直接使用ID
  333. waybillId: deliveryData.deliveryNo
  334. };
  335. console.debug('使用快递公司ID:', {
  336. deliveryId: deliveryData.deliveryCompany,
  337. deliveryNo: deliveryData.deliveryNo
  338. });
  339. }
  340. break;
  341. case 2: // 同城配送
  342. // 同城配送不需要物流信息
  343. // 如果需要配送员信息,可以在这里设置 localDeliveryInfo
  344. // localDeliveryInfo = {
  345. // deliveryName: '配送员',
  346. // deliveryPhone: '13800138000'
  347. // };
  348. break;
  349. case 3: // 虚拟发货
  350. case 4: // 用户自提
  351. // 无需物流,不需要额外信息
  352. break;
  353. }
  354. // 调用后端发货信息录入API
  355. const response = await fetch('/api/v1/auth/upload-shipping-info', createAuthFetchOptions({
  356. method: 'POST',
  357. body: JSON.stringify({
  358. orderId: order.orderNo, // 使用订单号作为微信小程序订单ID
  359. deliveryType: deliveryData.deliveryType, // 直接使用前端的deliveryType值
  360. expressInfo,
  361. localDeliveryInfo,
  362. isAllDelivered: true,
  363. itemDesc, // 商品描述,最多110个字
  364. tenantId: tenantId || order.tenantId
  365. })
  366. }));
  367. if (!response.ok) {
  368. const errorText = await response.text();
  369. console.error('发货信息录入API调用失败:', {
  370. status: response.status,
  371. statusText: response.statusText,
  372. error: errorText
  373. });
  374. return {
  375. success: false,
  376. message: `发货信息录入失败: ${response.status}`,
  377. error: errorText
  378. };
  379. }
  380. const result = await response.json();
  381. console.debug('发货信息录入成功:', result);
  382. return {
  383. success: true,
  384. message: '发货信息录入成功',
  385. data: result
  386. };
  387. } catch (error) {
  388. console.error('调用发货信息录入API时出错:', error);
  389. return {
  390. success: false,
  391. message: '发货信息录入失败',
  392. error
  393. };
  394. }
  395. };
  396. // 状态映射
  397. const orderStatusMap = {
  398. 0: { label: '未发货', color: 'warning' },
  399. 1: { label: '已发货', color: 'info' },
  400. 2: { label: '收货成功', color: 'success' },
  401. 3: { label: '已退货', color: 'destructive' },
  402. } as const;
  403. const payStatusMap = {
  404. 0: { label: '未支付', color: 'warning' },
  405. 1: { label: '支付中', color: 'info' },
  406. 2: { label: '支付成功', color: 'success' },
  407. 3: { label: '已退款', color: 'secondary' },
  408. 4: { label: '支付失败', color: 'destructive' },
  409. 5: { label: '订单关闭', color: 'destructive' },
  410. } as const;
  411. const orderTypeMap = {
  412. 1: { label: '实物订单', color: 'default' },
  413. 2: { label: '虚拟订单', color: 'secondary' },
  414. } as const;
  415. const deliveryTypeMap = {
  416. 0: { label: '未发货', color: 'warning' },
  417. 1: { label: '物流快递', color: 'info' },
  418. 2: { label: '同城配送', color: 'info' },
  419. 3: { label: '虚拟发货', color: 'info' },
  420. 4: { label: '用户自提', color: 'info' },
  421. } as const;
  422. export const OrderManagement = () => {
  423. const [searchParams, setSearchParams] = useState({
  424. page: 1,
  425. limit: 10,
  426. search: '',
  427. status: 'all',
  428. payStatus: 'all',
  429. });
  430. const [isModalOpen, setIsModalOpen] = useState(false);
  431. const [editingOrder, setEditingOrder] = useState<OrderResponse | null>(null);
  432. const [detailModalOpen, setDetailModalOpen] = useState(false);
  433. const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
  434. const [deliveryModalOpen, setDeliveryModalOpen] = useState(false);
  435. const [deliveringOrder, setDeliveringOrder] = useState<OrderResponse | null>(null);
  436. const [deliveryCompanies, setDeliveryCompanies] = useState<Array<{ delivery_id: string; delivery_name: string }>>([]);
  437. const [loadingCompanies, setLoadingCompanies] = useState(false);
  438. const [printingOrder, setPrintingOrder] = useState<OrderResponse | null>(null);
  439. const [isPrinting, setIsPrinting] = useState(false);
  440. const [triggeringOrder, setTriggeringOrder] = useState<OrderResponse | null>(null);
  441. const [isTriggering, setIsTriggering] = useState(false);
  442. // 用于防止重复提交的请求ID缓存
  443. const [recentPrintRequests, setRecentPrintRequests] = useState<Map<string, number>>(new Map());
  444. // 表单实例
  445. const updateForm = useForm<UpdateRequest>({
  446. resolver: zodResolver(UpdateOrderDto),
  447. defaultValues: {},
  448. });
  449. // 发货表单实例
  450. const deliveryForm = useForm<DeliveryRequest>({
  451. defaultValues: {
  452. deliveryType: 1,
  453. deliveryCompany: '__select__',
  454. deliveryNo: '',
  455. deliveryRemark: '',
  456. },
  457. });
  458. // 数据查询 - 60秒自动刷新
  459. const { data, isLoading, refetch } = useQuery({
  460. queryKey: ['orders', searchParams],
  461. queryFn: async () => {
  462. const filters: any = {};
  463. if (searchParams.status !== 'all') {
  464. filters.state = parseInt(searchParams.status);
  465. }
  466. if (searchParams.payStatus !== 'all') {
  467. filters.payState = parseInt(searchParams.payStatus);
  468. }
  469. const res = await orderClientManager.getAdminOrderClient().index.$get({
  470. query: {
  471. page: searchParams.page,
  472. pageSize: searchParams.limit,
  473. keyword: searchParams.search,
  474. ...(Object.keys(filters).length > 0 && { filters: JSON.stringify(filters) }),
  475. }
  476. });
  477. if (res.status !== 200) throw new Error('获取订单列表失败');
  478. return await res.json();
  479. },
  480. refetchInterval: 60000, // 60秒自动刷新
  481. refetchIntervalInBackground: false, // 只在页面可见时刷新
  482. staleTime: 30000, // 30秒后数据视为过期
  483. });
  484. // 定期清理过期的请求记录(每5分钟清理一次)
  485. useEffect(() => {
  486. const cleanupInterval = setInterval(() => {
  487. const now = Date.now();
  488. setRecentPrintRequests(prev => {
  489. const newMap = new Map();
  490. for (const [fingerprint, timestamp] of prev) {
  491. // 保留最近2分钟内的记录
  492. if (now - timestamp < 120000) {
  493. newMap.set(fingerprint, timestamp);
  494. }
  495. }
  496. if (newMap.size !== prev.size) {
  497. console.debug(`清理了 ${prev.size - newMap.size} 个过期的打印请求记录`);
  498. }
  499. return newMap;
  500. });
  501. }, 300000); // 每5分钟清理一次
  502. return () => clearInterval(cleanupInterval);
  503. }, []);
  504. // 处理搜索
  505. const handleSearch = () => {
  506. setSearchParams(prev => ({ ...prev, page: 1 }));
  507. };
  508. // 检查交易管理状态
  509. const handleCheckTradeManaged = async () => {
  510. try {
  511. console.debug('开始检查交易管理状态');
  512. // 这里需要获取当前租户ID,可以从订单数据中获取第一个订单的tenantId
  513. // 或者使用默认值,这里先使用默认值
  514. const tenantId = data?.data?.[0]?.tenantId || undefined;
  515. const response = await fetch('/api/v1/auth/get-is-trade-managed', createAuthFetchOptions({
  516. method: 'POST',
  517. body: JSON.stringify({
  518. tenantId
  519. })
  520. }));
  521. if (!response.ok) {
  522. const errorText = await response.text();
  523. console.error('检查交易管理状态失败:', {
  524. status: response.status,
  525. statusText: response.statusText,
  526. error: errorText
  527. });
  528. let errorMessage = `检查交易管理状态失败: ${response.status}`;
  529. try {
  530. const errorData = JSON.parse(errorText);
  531. if (errorData.message) {
  532. errorMessage = errorData.message;
  533. }
  534. } catch (e) {
  535. // 如果无法解析为JSON,使用原始文本
  536. if (errorText) {
  537. errorMessage = errorText.substring(0, 200);
  538. }
  539. }
  540. toast.error(errorMessage);
  541. return;
  542. }
  543. const result = await response.json();
  544. console.debug('交易管理状态检查结果:', result);
  545. if (result.success) {
  546. const isTradeManaged = result.data?.is_trade_managed;
  547. const statusText = isTradeManaged ? '已开启' : '未开启';
  548. toast.success(`交易管理状态: ${statusText}`);
  549. } else {
  550. toast.error(result.message || '检查交易管理状态失败');
  551. }
  552. } catch (error) {
  553. console.error('检查交易管理状态时出错:', error);
  554. toast.error('检查交易管理状态失败,请重试');
  555. }
  556. };
  557. // 处理编辑订单
  558. const handleEditOrder = (order: OrderResponse) => {
  559. setEditingOrder(order);
  560. updateForm.reset({
  561. state: order.state,
  562. payState: order.payState,
  563. remark: order.remark || '',
  564. });
  565. setIsModalOpen(true);
  566. };
  567. // 处理查看详情
  568. const handleViewDetails = (order: OrderResponse) => {
  569. setSelectedOrder(order);
  570. setDetailModalOpen(true);
  571. };
  572. // 处理发货
  573. const handleDeliveryOrder = async (order: OrderResponse) => {
  574. setDeliveringOrder(order);
  575. deliveryForm.reset({
  576. deliveryType: 1,
  577. deliveryCompany: '__select__',
  578. deliveryNo: '',
  579. deliveryRemark: '',
  580. });
  581. // 获取快递公司列表
  582. setLoadingCompanies(true);
  583. try {
  584. const result = await getWechatDeliveryCompanies(order.tenantId);
  585. const resdata =result.data;
  586. console.log("result:",result);
  587. if (resdata.success && resdata.data?.company_list) {
  588. const companyList = resdata.data.company_list;
  589. setDeliveryCompanies(companyList);
  590. // 如果有快递公司列表,自动选中第一个
  591. if (companyList.length > 0) {
  592. // 延迟设置,确保组件已经渲染
  593. setTimeout(() => {
  594. deliveryForm.setValue('deliveryCompany', companyList[0].delivery_id);
  595. }, 0);
  596. }
  597. } else {
  598. console.warn('获取快递公司列表失败或为空:', resdata);
  599. setDeliveryCompanies([]);
  600. }
  601. } catch (error) {
  602. console.error('获取快递公司列表时出错:', error);
  603. setDeliveryCompanies([]);
  604. toast.warning('获取快递公司列表失败,请手动输入快递公司名称');
  605. } finally {
  606. setLoadingCompanies(false);
  607. }
  608. setDeliveryModalOpen(true);
  609. };
  610. const handleDeliverySubmit = async (data: DeliveryRequest) => {
  611. if (!deliveringOrder || !deliveringOrder.id) {
  612. console.error('发货失败: deliveringOrder或id为空', { deliveringOrder });
  613. return;
  614. }
  615. // 处理快递公司选择值
  616. if (data.deliveryCompany === '__select__' || data.deliveryCompany === '__manual__') {
  617. data.deliveryCompany = null;
  618. }
  619. // 验证:如果选择物流快递方式,必须填写快递公司
  620. if (data.deliveryType === 1 && !data.deliveryCompany) {
  621. toast.error('请选择或输入快递公司');
  622. return;
  623. }
  624. try {
  625. // console.debug('发货请求数据:', {
  626. // orderId: deliveringOrder.id,
  627. // orderNo: deliveringOrder.orderNo,
  628. // data,
  629. // orderState: deliveringOrder.state,
  630. // payState: deliveringOrder.payState
  631. // });
  632. // 使用adminOrderClient的$put接口来更新订单发货信息
  633. // 因为adminDeliveryRoutes没有被挂载到服务器,而adminOrderRoutes已经被正确挂载
  634. data.deliveryTime = new Date().toISOString();
  635. // 构建更新数据
  636. const updateData: any = {
  637. state: 1, // 已发货状态
  638. deliveryType: data.deliveryType,
  639. deliveryTime: data.deliveryTime,
  640. deliveryRemark: data.deliveryRemark || null,
  641. };
  642. // 只有物流快递(deliveryType === 1)才保存物流公司信息和快递单号
  643. if (data.deliveryType === 1) {
  644. updateData.deliveryCompany = data.deliveryCompany || null;
  645. updateData.deliveryNo = data.deliveryNo || null;
  646. } else {
  647. // 同城配送、虚拟发货、用户自提等不保存物流信息
  648. updateData.deliveryCompany = null;
  649. updateData.deliveryNo = null;
  650. }
  651. // console.debug('更新订单数据:', updateData);
  652. // 调用adminOrderClient的$put接口
  653. const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
  654. param: { id: deliveringOrder.id },
  655. json: updateData,
  656. });
  657. // console.debug('发货响应详情:', {
  658. // status: res.status,
  659. // statusText: res.statusText,
  660. // headers: Object.fromEntries(res.headers.entries()),
  661. // url: res.url
  662. // });
  663. if (res.status === 200) {
  664. let result: DeliveryResponse;
  665. try {
  666. const responseText = await res.text();
  667. if (responseText.trim()) {
  668. try {
  669. result = JSON.parse(responseText) as DeliveryResponse;
  670. } catch (parseError) {
  671. console.error('成功响应JSON解析失败:', parseError);
  672. result = { success: true, message: '发货成功' } as DeliveryResponse;
  673. }
  674. } else {
  675. result = { success: true, message: '发货成功' } as DeliveryResponse;
  676. }
  677. } catch (error) {
  678. console.error('处理成功响应失败:', error);
  679. result = { success: true, message: '发货成功' } as DeliveryResponse;
  680. }
  681. console.debug('发货成功:', result);
  682. toast.success(result.message || '发货成功');
  683. setDeliveryModalOpen(false);
  684. refetch();
  685. // 发货成功后根据支付类型决定是否调用微信小程序发货信息录入API
  686. // 信用支付(payType === 3)不调用,正常支付(payType !== 3)调用
  687. if (deliveringOrder?.payType !== 3) {
  688. try {
  689. const uploadResult = await uploadShippingInfoToWechat(deliveringOrder, data, deliveringOrder?.tenantId);
  690. if (uploadResult.success) {
  691. console.debug("微信小程序发货信息录入成功:", uploadResult);
  692. // 可以在这里添加额外的成功处理,比如记录日志
  693. } else {
  694. // 微信小程序发货信息录入失败,记录警告但不影响主流程
  695. console.warn("微信小程序发货信息录入失败,但系统发货成功:", uploadResult);
  696. // 可以在这里添加额外的处理,比如记录到日志系统或发送告警
  697. }
  698. } catch (uploadError) {
  699. console.error("调用微信小程序发货信息录入API时发生异常:", uploadError);
  700. // 不阻止发货成功,只记录错误
  701. }
  702. } else if(deliveringOrder?.payType == 3) {
  703. // 发送微信服务消息通知 - 使用新的配置化设计
  704. try {
  705. const notificationResult = await sendDeliverySuccessNotification(deliveringOrder, data);
  706. // 根据通知结果记录不同的日志
  707. if (notificationResult.success) {
  708. console.debug('微信发货通知发送成功:', notificationResult);
  709. } else if (notificationResult.data?.skipped) {
  710. // 用户未订阅,这是正常情况,记录为debug级别
  711. console.debug('用户未订阅发货通知,跳过发送:', {
  712. userId: deliveringOrder.user?.id,
  713. username: deliveringOrder.user?.username,
  714. reason: notificationResult.data.reason
  715. });
  716. } else {
  717. // 其他原因导致的失败,记录为warn级别
  718. console.warn('微信发货通知发送失败,但发货成功:', notificationResult);
  719. // 可以在这里添加额外的处理,比如记录到日志系统
  720. }
  721. } catch (notificationError) {
  722. console.error('发送微信发货通知时发生异常:', notificationError);
  723. // 不阻止发货成功,只记录错误
  724. }
  725. }
  726. } else {
  727. // 先尝试获取响应文本,避免JSON解析错误
  728. let errorText = '';
  729. let errorData: any = null;
  730. try {
  731. errorText = await res.text();
  732. console.debug('发货失败响应文本:', errorText);
  733. // 尝试解析为JSON
  734. if (errorText.trim()) {
  735. try {
  736. errorData = JSON.parse(errorText);
  737. } catch (parseError) {
  738. console.warn('响应不是有效的JSON:', parseError);
  739. errorData = { message: errorText.substring(0, 100) + '...' };
  740. }
  741. }
  742. } catch (textError) {
  743. console.error('获取响应文本失败:', textError);
  744. errorData = { message: `请求失败,状态码: ${res.status}` };
  745. }
  746. console.error('发货失败响应:', {
  747. status: res.status,
  748. statusText: res.statusText,
  749. errorData,
  750. errorText: errorText.substring(0, 200),
  751. orderId: deliveringOrder.id
  752. });
  753. // 显示错误消息
  754. const errorMessage = errorData?.message ||
  755. errorData?.error?.message ||
  756. res.statusText ||
  757. `发货失败 (${res.status})`;
  758. toast.error(errorMessage);
  759. }
  760. } catch (error) {
  761. console.error('发货请求异常:', error);
  762. toast.error('发货失败,请重试');
  763. }
  764. };
  765. // 发货成功微信通知函数 - 使用新的配置化设计
  766. const sendDeliverySuccessNotification = async (order: OrderResponse, deliveryData: DeliveryRequest): Promise<WechatServiceMessageResult> => {
  767. // 检查是否有用户信息和openid
  768. if (!order.user || !order.user.id) {
  769. console.warn('订单没有用户信息,无法发送微信通知');
  770. return { success: false, message: '订单没有用户信息,无法发送微信通知' };
  771. }
  772. // 检查用户是否有openid(微信小程序用户)
  773. if (!order.user.openid) {
  774. console.warn('用户没有绑定微信小程序,无法发送微信通知', {
  775. userId: order.user.id,
  776. username: order.user.username
  777. });
  778. return { success: false, message: '用户没有绑定微信小程序,无法发送微信通知' };
  779. }
  780. // 检查用户是否已订阅发货通知
  781. if (order.user.hasSubscribedDeliveryNotice !== true) {
  782. console.debug('用户未订阅发货通知,跳过发送微信通知', {
  783. userId: order.user.id,
  784. username: order.user.username,
  785. hasSubscribedDeliveryNotice: order.user.hasSubscribedDeliveryNotice
  786. });
  787. return {
  788. success: false,
  789. message: '用户未订阅发货通知,跳过发送微信通知',
  790. data: { skipped: true, reason: 'user_not_subscribed' }
  791. };
  792. }
  793. console.debug('用户已订阅发货通知,准备发送微信通知', {
  794. userId: order.user.id,
  795. username: order.user.username,
  796. openid: order.user.openid?.substring(0, 10) + '...' // 部分隐藏openid
  797. });
  798. // 构建微信服务消息配置 - 参考useShareAppMessage的配置对象模式
  799. const config: WechatServiceMessageConfig = {
  800. openid: order.user.openid,
  801. templateId: 'T00N0Wq3ECjksXSvPWUBgOUukl1TCE7PhxqeDnFPfso', // 发货成功通知模板ID
  802. page: 'pages/order/detail/index', // 点击跳转到订单详情页
  803. data: {
  804. // 根据实际微信模板字段配置
  805. character_string7: {
  806. value: `${order.orderNo}`
  807. },
  808. date6: {
  809. value: formatWechatDate(deliveryData.deliveryTime)
  810. },
  811. amount9: {
  812. value: `¥${order.payAmount.toFixed(2)}`
  813. },
  814. phrase12: {
  815. value: deliveryTypeMap[deliveryData.deliveryType as keyof typeof deliveryTypeMap]?.label || '未知'
  816. },
  817. thing4: {
  818. value: (deliveryData.deliveryRemark || '请收到货/提货后及时确认收货,2天后将自动确认收货,如有异常请及时进行交易投诉。').substring(0, 20)
  819. }
  820. },
  821. miniprogramState: 'formal',
  822. tenantId: order.tenantId // 从订单数据中获取tenantId
  823. };
  824. // 调用微信服务消息函数
  825. return await sendWechatServiceMessage(config);
  826. };
  827. // 获取默认打印机
  828. const getDefaultPrinter = async (tenantId?: number): Promise<{ printerSn: string; printerKey: string } | null> => {
  829. try {
  830. console.debug('获取默认打印机,租户ID:', tenantId);
  831. const response = await fetch('/api/v1/feie/printers?isDefault=true&pageSize=1', createAuthFetchOptions({
  832. method: 'GET'
  833. }));
  834. if (!response.ok) {
  835. const errorText = await response.text();
  836. console.error('获取默认打印机失败:', {
  837. status: response.status,
  838. statusText: response.statusText,
  839. error: errorText
  840. });
  841. return null;
  842. }
  843. const result = await response.json();
  844. console.debug('获取默认打印机结果:', result);
  845. if (result.success && result.data?.data?.length > 0) {
  846. const printer = result.data.data[0];
  847. return {
  848. printerSn: printer.printerSn,
  849. printerKey: printer.printerKey
  850. };
  851. }
  852. return null;
  853. } catch (error) {
  854. console.error('获取默认打印机时出错:', error);
  855. return null;
  856. }
  857. };
  858. // 获取打印模板
  859. const getPrintTemplate = async (tenantId?: number): Promise<string | null> => {
  860. try {
  861. console.debug('获取打印模板,租户ID:', tenantId);
  862. const response = await fetch('/api/v1/feie/config', createAuthFetchOptions({
  863. method: 'GET'
  864. }));
  865. if (!response.ok) {
  866. const errorText = await response.text();
  867. console.error('获取打印模板失败:', {
  868. status: response.status,
  869. statusText: response.statusText,
  870. error: errorText
  871. });
  872. return null;
  873. }
  874. const result = await response.json();
  875. console.debug('获取打印模板结果:', result);
  876. if (result.success && result.data?.data) {
  877. const configs = result.data.data;
  878. const receiptTemplate = configs.find((config: any) => config.configKey === 'feie.receipt_template');
  879. return receiptTemplate?.configValue || null;
  880. }
  881. return null;
  882. } catch (error) {
  883. console.error('获取打印模板时出错:', error);
  884. return null;
  885. }
  886. };
  887. // 触发支付成功事件(测试延迟打印)
  888. const handleTriggerPaymentSuccess = async (order: OrderResponse) => {
  889. setTriggeringOrder(order);
  890. setIsTriggering(true);
  891. try {
  892. console.debug('触发支付成功事件,订单ID:', order.id, '租户ID:', order.tenantId);
  893. // 尝试不同的API路径
  894. const apiPaths = [
  895. '/api/v1/payments/payment/trigger-success', // 如果路由注册在 /api/v1/payments
  896. '/api/v1/payment/trigger-success' // 如果路由注册在 /api/v1
  897. ];
  898. let response: Response | null = null;
  899. let lastError: Error | null = null;
  900. let successfulPath: string | null = null;
  901. for (const apiPath of apiPaths) {
  902. try {
  903. console.debug('尝试API路径:', apiPath);
  904. response = await fetch(apiPath, createAuthFetchOptions({
  905. method: 'POST',
  906. body: JSON.stringify({
  907. orderId: order.id
  908. })
  909. }));
  910. // 如果响应不是404,跳出循环
  911. if (response.status !== 404) {
  912. successfulPath = apiPath;
  913. console.debug(`找到有效API路径: ${apiPath}, 状态码: ${response.status}`);
  914. break;
  915. }
  916. console.debug(`路径 ${apiPath} 返回404,尝试下一个路径`);
  917. } catch (error) {
  918. lastError = error as Error;
  919. console.debug(`路径 ${apiPath} 请求失败:`, error);
  920. }
  921. }
  922. if (!response) {
  923. throw new Error('所有API路径尝试失败');
  924. }
  925. if (!response.ok) {
  926. const errorText = await response.text();
  927. console.error('触发支付成功事件失败:', {
  928. status: response.status,
  929. statusText: response.statusText,
  930. error: errorText
  931. });
  932. let errorMessage = `触发失败: ${response.status}`;
  933. try {
  934. const errorData = JSON.parse(errorText);
  935. if (errorData.message) {
  936. errorMessage = errorData.message;
  937. }
  938. } catch (e) {
  939. // 如果无法解析为JSON,使用原始文本
  940. if (errorText) {
  941. errorMessage = errorText.substring(0, 200);
  942. }
  943. }
  944. throw new Error(errorMessage);
  945. }
  946. const result: TriggerPaymentSuccessResponse = await response.json();
  947. if (!result.success) {
  948. throw new Error(result.message || '触发支付成功事件失败');
  949. }
  950. console.debug('触发支付成功事件成功:', {
  951. path: successfulPath,
  952. result
  953. });
  954. toast.success(`支付成功事件已触发: ${result.message} (路径: ${successfulPath})`);
  955. } catch (error: any) {
  956. console.error('触发支付成功事件失败:', error);
  957. toast.error(`触发失败: ${error.message || '未知错误'}`);
  958. } finally {
  959. setIsTriggering(false);
  960. setTriggeringOrder(null);
  961. }
  962. };
  963. // 处理打印订单
  964. const handlePrintOrder = async (order: OrderResponse) => {
  965. // 防止重复点击:如果已经在打印中,直接返回
  966. if (isPrinting) {
  967. console.warn('打印任务正在进行中,请勿重复点击');
  968. toast.warning('打印任务正在进行中,请稍后再试');
  969. return;
  970. }
  971. // 生成请求指纹:订单ID + 时间戳(分钟级),防止短时间内重复提交相同订单
  972. const requestFingerprint = `${order.id}_${Math.floor(Date.now() / 60000)}`; // 每分钟一个唯一标识
  973. // 检查是否在最近1分钟内提交过相同的打印请求
  974. const recentRequestTime = recentPrintRequests.get(requestFingerprint);
  975. if (recentRequestTime && Date.now() - recentRequestTime < 60000) {
  976. console.warn('检测到重复打印请求,短时间内请勿重复提交', {
  977. orderId: order.id,
  978. fingerprint: requestFingerprint,
  979. lastRequestTime: new Date(recentRequestTime).toISOString()
  980. });
  981. toast.warning('请勿重复提交打印请求,请等待1分钟后再试');
  982. return;
  983. }
  984. // 记录当前请求
  985. setRecentPrintRequests(prev => {
  986. const newMap = new Map(prev);
  987. newMap.set(requestFingerprint, Date.now());
  988. return newMap;
  989. });
  990. setPrintingOrder(order);
  991. setIsPrinting(true);
  992. try {
  993. // 获取默认打印机
  994. const defaultPrinter = await getDefaultPrinter(order.tenantId);
  995. if (!defaultPrinter) {
  996. throw new Error('未找到默认打印机,请先设置默认打印机');
  997. }
  998. // 获取打印模板
  999. const template = await getPrintTemplate(order.tenantId);
  1000. // 构建打印内容
  1001. const printContent = generatePrintContent(order, template);
  1002. // 构建打印请求
  1003. const printRequest: SubmitPrintTaskRequest = {
  1004. printerSn: defaultPrinter.printerSn,
  1005. content: printContent,
  1006. printType: 'RECEIPT', // 收据类型
  1007. orderId: order.id,
  1008. delaySeconds: 0 // 立即打印
  1009. };
  1010. console.debug('提交打印任务:', {
  1011. orderId: order.id,
  1012. orderNo: order.orderNo,
  1013. printerSn: defaultPrinter.printerSn,
  1014. printRequest
  1015. });
  1016. // 使用fetch API提交打印任务
  1017. const response = await fetch('/api/v1/feie/tasks', createAuthFetchOptions({
  1018. method: 'POST',
  1019. body: JSON.stringify(printRequest)
  1020. }));
  1021. if (!response.ok) {
  1022. const errorText = await response.text();
  1023. console.error('打印任务提交失败:', {
  1024. status: response.status,
  1025. statusText: response.statusText,
  1026. error: errorText
  1027. });
  1028. let errorMessage = `打印失败: ${response.status}`;
  1029. try {
  1030. const errorData = JSON.parse(errorText);
  1031. if (errorData.message) {
  1032. errorMessage = errorData.message;
  1033. }
  1034. } catch (e) {
  1035. // 如果无法解析为JSON,使用原始文本
  1036. if (errorText) {
  1037. errorMessage = errorText.substring(0, 200);
  1038. }
  1039. }
  1040. throw new Error(errorMessage);
  1041. }
  1042. const result: ApiResponse<SubmitPrintTaskResponse> = await response.json();
  1043. if (!result.success) {
  1044. throw new Error(result.message || '打印任务提交失败');
  1045. }
  1046. console.debug('打印任务提交成功:', result.data);
  1047. toast.success(`打印任务已提交,任务ID: ${result.data?.taskId}`);
  1048. } catch (error: any) {
  1049. console.error('打印订单失败:', error);
  1050. toast.error(`打印失败: ${error.message || '未知错误'}`);
  1051. } finally {
  1052. setIsPrinting(false);
  1053. setPrintingOrder(null);
  1054. }
  1055. };
  1056. // 生成打印内容
  1057. const generatePrintContent = (order: OrderResponse, template?: string | null): string => {
  1058. // 如果没有模板或模板为空,使用默认模板
  1059. if (!template) {
  1060. template = `
  1061. <CB>订单收据</CB>
  1062. <BR>
  1063. 订单号: {orderNo}
  1064. 下单时间: {orderTime}
  1065. <BR>
  1066. <B>收货信息</B>
  1067. 收货人: {receiverName}
  1068. 联系电话: {receiverPhone}
  1069. 收货地址: {address}
  1070. <BR>
  1071. <B>商品信息</B>
  1072. {goodsList}
  1073. <BR>
  1074. <B>费用明细</B>
  1075. 商品总额: {totalAmount}
  1076. 运费: {freightAmount}
  1077. 实付金额: {payAmount}
  1078. <BR>
  1079. <B>订单状态</B>
  1080. 订单状态: {orderStatus}
  1081. 支付状态: {payStatus}
  1082. <BR>
  1083. <B>订单备注</B>
  1084. {remark}
  1085. <BR>
  1086. <C>感谢您的惠顾!</C>
  1087. <BR>
  1088. <QR>{orderNo}</QR>
  1089. `;
  1090. }
  1091. // 准备模板变量
  1092. const variables = {
  1093. orderNo: order.orderNo,
  1094. orderTime: format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm:ss'),
  1095. receiverName: order.recevierName || '-',
  1096. receiverPhone: order.receiverMobile || '-',
  1097. phone: order.receiverMobile || '-', // 添加phone变量,兼容两种模板格式
  1098. address: order.address || '-',
  1099. goodsList: order.orderGoods?.map(item =>
  1100. `${item.goodsName} ${item.price} × ${item.num} = ${(item.price * item.num).toFixed(2)}`
  1101. ).join('\n') || '暂无商品信息',
  1102. totalAmount: `${order.amount.toFixed(2)}`,
  1103. freightAmount: `${order.freightAmount.toFixed(2)}`,
  1104. payAmount: `${order.payAmount.toFixed(2)}`,
  1105. orderStatus: orderStatusMap[order.state as keyof typeof orderStatusMap]?.label || '未知',
  1106. payStatus: payStatusMap[order.payState as keyof typeof payStatusMap]?.label || '未知',
  1107. remark: order.remark || '无备注'
  1108. };
  1109. // 替换模板变量 - 使用更健壮的替换方法
  1110. let content = template;
  1111. // 方法1: 标准替换
  1112. for (const [key, value] of Object.entries(variables)) {
  1113. // 处理多种格式的占位符:{key}、{ key }、{{key}}等
  1114. const patterns = [
  1115. `{${key}}`, // 标准格式
  1116. `{ ${key} }`, // 有空格
  1117. `{{${key}}}`, // 双大括号
  1118. `{{ ${key} }}`, // 双大括号有空格
  1119. `{${key} }`, // 右空格
  1120. `{ ${key}}`, // 左空格
  1121. ];
  1122. for (const pattern of patterns) {
  1123. if (content.includes(pattern)) {
  1124. content = content.split(pattern).join(value);
  1125. }
  1126. }
  1127. }
  1128. // 方法2: 清理未替换的变量(特别是remark)
  1129. // 使用正则表达式匹配各种格式的{remark}
  1130. const remarkPatterns = [
  1131. /\{remark\}/g,
  1132. /\{\s*remark\s*\}/g,
  1133. /\{\{remark\}\}/g,
  1134. /\{\{\s*remark\s*\}\}/g,
  1135. ];
  1136. for (const pattern of remarkPatterns) {
  1137. if (pattern.test(content)) {
  1138. const safeRemark = variables.remark || '无备注';
  1139. content = content.replace(pattern, safeRemark);
  1140. }
  1141. }
  1142. return content.trim();
  1143. };
  1144. // 处理更新订单
  1145. const handleUpdateSubmit = async (data: UpdateRequest) => {
  1146. if (!editingOrder || !editingOrder.id) return;
  1147. try {
  1148. const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
  1149. param: { id: editingOrder.id },
  1150. json: data,
  1151. });
  1152. if (res.status === 200) {
  1153. toast.success('订单更新成功');
  1154. setIsModalOpen(false);
  1155. refetch();
  1156. } else {
  1157. const error = await res.json();
  1158. toast.error(error.message || '更新失败');
  1159. }
  1160. } catch (error) {
  1161. console.error('更新订单失败:', error);
  1162. toast.error('更新失败,请重试');
  1163. }
  1164. };
  1165. // 格式化金额
  1166. const formatAmount = (amount: number) => {
  1167. return `¥${amount.toFixed(2)}`;
  1168. };
  1169. // 获取状态颜色
  1170. const getStatusBadge = (status: number, type: 'order' | 'pay' | 'delivery') => {
  1171. const map = type === 'order' ? orderStatusMap : type === 'pay' ? payStatusMap : deliveryTypeMap;
  1172. const config = map[status as keyof typeof map] || { label: '未知', color: 'default' };
  1173. return <Badge variant={config.color as any}>{config.label}</Badge>;
  1174. };
  1175. // 骨架屏 - 只覆盖表格区域,搜索区域保持可用
  1176. if (isLoading) {
  1177. return (
  1178. <div className="space-y-4">
  1179. {/* 页面标题 */}
  1180. <div className="flex justify-between items-center">
  1181. <div>
  1182. <h1 className="text-2xl font-bold">订单管理</h1>
  1183. <p className="text-muted-foreground">管理所有订单信息</p>
  1184. </div>
  1185. </div>
  1186. {/* 搜索区域 - 保持可用 */}
  1187. <Card>
  1188. <CardHeader>
  1189. <CardTitle>订单列表</CardTitle>
  1190. <CardDescription>查看和管理所有订单</CardDescription>
  1191. </CardHeader>
  1192. <CardContent>
  1193. <div className="flex gap-4 mb-4">
  1194. <div className="relative flex-1 max-w-sm">
  1195. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  1196. <Input
  1197. placeholder="搜索订单号、手机号、收货人姓名..."
  1198. value={searchParams.search}
  1199. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  1200. className="pl-8"
  1201. data-testid="order-search-input"
  1202. />
  1203. </div>
  1204. <Select
  1205. value={searchParams.status}
  1206. onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
  1207. >
  1208. <SelectTrigger className="w-32" data-testid="order-status-select">
  1209. <SelectValue placeholder="订单状态" />
  1210. </SelectTrigger>
  1211. <SelectContent>
  1212. <SelectItem value="all">全部</SelectItem>
  1213. <SelectItem value="0">未发货</SelectItem>
  1214. <SelectItem value="1">已发货</SelectItem>
  1215. <SelectItem value="2">收货成功</SelectItem>
  1216. <SelectItem value="3">已退货</SelectItem>
  1217. </SelectContent>
  1218. </Select>
  1219. <Select
  1220. value={searchParams.payStatus}
  1221. onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
  1222. >
  1223. <SelectTrigger className="w-32" data-testid="order-pay-status-select">
  1224. <SelectValue placeholder="支付状态" />
  1225. </SelectTrigger>
  1226. <SelectContent>
  1227. <SelectItem value="all">全部</SelectItem>
  1228. <SelectItem value="0">未支付</SelectItem>
  1229. <SelectItem value="1">支付中</SelectItem>
  1230. <SelectItem value="2">支付成功</SelectItem>
  1231. <SelectItem value="3">已退款</SelectItem>
  1232. <SelectItem value="4">支付失败</SelectItem>
  1233. <SelectItem value="5">订单关闭</SelectItem>
  1234. </SelectContent>
  1235. </Select>
  1236. <Button onClick={handleSearch} data-testid="order-search-button">
  1237. <Search className="h-4 w-4 mr-2" />
  1238. 搜索
  1239. </Button>
  1240. </div>
  1241. {/* 表格骨架屏 */}
  1242. <div className="rounded-md border">
  1243. <Table>
  1244. <TableHeader>
  1245. <TableRow>
  1246. <TableHead>订单号</TableHead>
  1247. <TableHead>用户信息</TableHead>
  1248. <TableHead>收货人</TableHead>
  1249. <TableHead>金额</TableHead>
  1250. <TableHead>订单状态</TableHead>
  1251. <TableHead>支付状态</TableHead>
  1252. <TableHead>创建时间</TableHead>
  1253. <TableHead className="text-right">操作</TableHead>
  1254. </TableRow>
  1255. </TableHeader>
  1256. <TableBody>
  1257. {[...Array(5)].map((_, i) => (
  1258. <TableRow key={i}>
  1259. <TableCell>
  1260. <Skeleton className="h-4 w-32" />
  1261. </TableCell>
  1262. <TableCell>
  1263. <Skeleton className="h-4 w-24" />
  1264. </TableCell>
  1265. <TableCell>
  1266. <Skeleton className="h-4 w-20" />
  1267. </TableCell>
  1268. <TableCell>
  1269. <Skeleton className="h-4 w-16" />
  1270. </TableCell>
  1271. <TableCell>
  1272. <Skeleton className="h-6 w-16" />
  1273. </TableCell>
  1274. <TableCell>
  1275. <Skeleton className="h-6 w-16" />
  1276. </TableCell>
  1277. <TableCell>
  1278. <Skeleton className="h-4 w-24" />
  1279. </TableCell>
  1280. <TableCell className="text-right">
  1281. <div className="flex justify-end gap-2">
  1282. <Skeleton className="h-8 w-8" />
  1283. <Skeleton className="h-8 w-8" />
  1284. <Skeleton className="h-8 w-8" />
  1285. </div>
  1286. </TableCell>
  1287. </TableRow>
  1288. ))}
  1289. </TableBody>
  1290. </Table>
  1291. </div>
  1292. {/* 分页骨架屏 */}
  1293. <div className="flex items-center justify-between px-2 py-4">
  1294. <Skeleton className="h-4 w-32" />
  1295. <div className="flex items-center space-x-2">
  1296. <Skeleton className="h-8 w-16" />
  1297. <Skeleton className="h-4 w-24" />
  1298. <Skeleton className="h-8 w-16" />
  1299. </div>
  1300. </div>
  1301. </CardContent>
  1302. </Card>
  1303. </div>
  1304. );
  1305. }
  1306. return (
  1307. <div className="space-y-4">
  1308. {/* 页面标题 */}
  1309. <div className="flex justify-between items-center">
  1310. <div>
  1311. <h1 className="text-2xl font-bold">订单管理</h1>
  1312. <p className="text-muted-foreground">管理所有订单信息</p>
  1313. </div>
  1314. {/* <Button
  1315. variant="outline"
  1316. onClick={() => handleCheckTradeManaged()}
  1317. data-testid="check-trade-managed-button"
  1318. >
  1319. 检查交易管理状态
  1320. </Button> */}
  1321. </div>
  1322. {/* 搜索区域 */}
  1323. <Card>
  1324. <CardHeader>
  1325. <CardTitle>订单列表</CardTitle>
  1326. <CardDescription>查看和管理所有订单</CardDescription>
  1327. </CardHeader>
  1328. <CardContent>
  1329. <div className="flex gap-4 mb-4">
  1330. <div className="relative flex-1 max-w-sm">
  1331. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  1332. <Input
  1333. placeholder="搜索订单号、手机号、收货人姓名..."
  1334. value={searchParams.search}
  1335. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  1336. className="pl-8"
  1337. data-testid="order-search-input"
  1338. />
  1339. </div>
  1340. <Select
  1341. value={searchParams.status}
  1342. onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
  1343. >
  1344. <SelectTrigger className="w-32" data-testid="order-status-select">
  1345. <SelectValue placeholder="订单状态" />
  1346. </SelectTrigger>
  1347. <SelectContent>
  1348. <SelectItem value="all">全部</SelectItem>
  1349. <SelectItem value="0">未发货</SelectItem>
  1350. <SelectItem value="1">已发货</SelectItem>
  1351. <SelectItem value="2">收货成功</SelectItem>
  1352. <SelectItem value="3">已退货</SelectItem>
  1353. </SelectContent>
  1354. </Select>
  1355. <Select
  1356. value={searchParams.payStatus}
  1357. onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
  1358. >
  1359. <SelectTrigger className="w-32" data-testid="order-pay-status-select">
  1360. <SelectValue placeholder="支付状态" />
  1361. </SelectTrigger>
  1362. <SelectContent>
  1363. <SelectItem value="all">全部</SelectItem>
  1364. <SelectItem value="0">未支付</SelectItem>
  1365. <SelectItem value="1">支付中</SelectItem>
  1366. <SelectItem value="2">支付成功</SelectItem>
  1367. <SelectItem value="3">已退款</SelectItem>
  1368. <SelectItem value="4">支付失败</SelectItem>
  1369. <SelectItem value="5">订单关闭</SelectItem>
  1370. </SelectContent>
  1371. </Select>
  1372. <Button onClick={handleSearch} data-testid="order-search-button">
  1373. <Search className="h-4 w-4 mr-2" />
  1374. 搜索
  1375. </Button>
  1376. </div>
  1377. {/* 数据表格 */}
  1378. <div className="rounded-md border">
  1379. <Table>
  1380. <TableHeader>
  1381. <TableRow>
  1382. <TableHead>订单号</TableHead>
  1383. <TableHead>用户信息</TableHead>
  1384. <TableHead>收货人</TableHead>
  1385. <TableHead>金额</TableHead>
  1386. <TableHead>订单状态</TableHead>
  1387. <TableHead>支付状态</TableHead>
  1388. <TableHead>创建时间</TableHead>
  1389. <TableHead className="text-right">操作</TableHead>
  1390. </TableRow>
  1391. </TableHeader>
  1392. <TableBody>
  1393. {data?.data.map((order) => (
  1394. <TableRow key={order.id}>
  1395. <TableCell>
  1396. <div>
  1397. <p className="font-medium">{order.orderNo}</p>
  1398. <p className="text-sm text-muted-foreground">
  1399. {orderTypeMap[order.orderType as keyof typeof orderTypeMap]?.label}
  1400. </p>
  1401. </div>
  1402. </TableCell>
  1403. <TableCell>
  1404. <div>
  1405. <p>{order.user?.username || '-'}</p>
  1406. <p className="text-sm text-muted-foreground">{order.userPhone}</p>
  1407. </div>
  1408. </TableCell>
  1409. <TableCell>
  1410. <div>
  1411. <p>{order.recevierName || '-'}</p>
  1412. <p className="text-sm text-muted-foreground">{order.receiverMobile}</p>
  1413. </div>
  1414. </TableCell>
  1415. <TableCell>
  1416. <div>
  1417. <p className="font-medium">{formatAmount(order.payAmount)}</p>
  1418. <p className="text-sm text-muted-foreground">{formatAmount(order.amount)}</p>
  1419. </div>
  1420. </TableCell>
  1421. <TableCell>{getStatusBadge(order.state, 'order')}</TableCell>
  1422. <TableCell>{getStatusBadge(order.payState, 'pay')}</TableCell>
  1423. <TableCell>
  1424. {format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm')}
  1425. </TableCell>
  1426. <TableCell className="text-right">
  1427. <div className="flex justify-end gap-2">
  1428. <Button
  1429. variant="ghost"
  1430. size="icon"
  1431. onClick={() => handlePrintOrder(order)}
  1432. disabled={isPrinting}
  1433. data-testid="order-print-button"
  1434. >
  1435. {isPrinting && printingOrder?.id === order.id ? (
  1436. <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
  1437. ) : (
  1438. <Printer className="h-4 w-4" />
  1439. )}
  1440. </Button>
  1441. {/* <Button
  1442. variant="ghost"
  1443. size="icon"
  1444. onClick={() => handleTriggerPaymentSuccess(order)}
  1445. disabled={isTriggering && triggeringOrder?.id === order.id}
  1446. data-testid="order-trigger-payment-button"
  1447. title="触发支付成功事件(测试延迟打印)"
  1448. >
  1449. {isTriggering && triggeringOrder?.id === order.id ? (
  1450. <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
  1451. ) : (
  1452. <Play className="h-4 w-4" />
  1453. )}
  1454. </Button> */}
  1455. <Button
  1456. variant="ghost"
  1457. size="icon"
  1458. onClick={() => handleViewDetails(order)}
  1459. data-testid="order-view-button"
  1460. >
  1461. <Eye className="h-4 w-4" />
  1462. </Button>
  1463. {order.state === 0 && order.payState === 2 && (
  1464. <Button
  1465. variant="ghost"
  1466. size="icon"
  1467. onClick={() => handleDeliveryOrder(order)}
  1468. data-testid="order-delivery-button"
  1469. >
  1470. <Truck className="h-4 w-4" />
  1471. </Button>
  1472. )}
  1473. <Button
  1474. variant="ghost"
  1475. size="icon"
  1476. onClick={() => handleEditOrder(order)}
  1477. data-testid="order-edit-button"
  1478. >
  1479. <Edit className="h-4 w-4" />
  1480. </Button>
  1481. </div>
  1482. </TableCell>
  1483. </TableRow>
  1484. ))}
  1485. </TableBody>
  1486. </Table>
  1487. </div>
  1488. {data?.data.length === 0 && !isLoading && (
  1489. <div className="text-center py-8">
  1490. <p className="text-muted-foreground">暂无订单数据</p>
  1491. </div>
  1492. )}
  1493. <DataTablePagination
  1494. currentPage={searchParams.page}
  1495. pageSize={searchParams.limit}
  1496. totalCount={data?.pagination.total || 0}
  1497. onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
  1498. />
  1499. </CardContent>
  1500. </Card>
  1501. {/* 编辑订单模态框 */}
  1502. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  1503. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  1504. <DialogHeader>
  1505. <DialogTitle>编辑订单</DialogTitle>
  1506. <DialogDescription>更新订单状态和备注信息</DialogDescription>
  1507. </DialogHeader>
  1508. <Form {...updateForm}>
  1509. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  1510. <FormField
  1511. control={updateForm.control}
  1512. name="state"
  1513. render={({ field }) => (
  1514. <FormItem>
  1515. <FormLabel>订单状态</FormLabel>
  1516. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  1517. <FormControl>
  1518. <SelectTrigger data-testid="edit-order-status-select">
  1519. <SelectValue placeholder="选择订单状态" />
  1520. </SelectTrigger>
  1521. </FormControl>
  1522. <SelectContent>
  1523. <SelectItem value="0">未发货</SelectItem>
  1524. <SelectItem value="1">已发货</SelectItem>
  1525. <SelectItem value="2">收货成功</SelectItem>
  1526. <SelectItem value="3">已退货</SelectItem>
  1527. </SelectContent>
  1528. </Select>
  1529. <FormMessage />
  1530. </FormItem>
  1531. )}
  1532. />
  1533. <FormField
  1534. control={updateForm.control}
  1535. name="payState"
  1536. render={({ field }) => (
  1537. <FormItem>
  1538. <FormLabel>支付状态</FormLabel>
  1539. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  1540. <FormControl>
  1541. <SelectTrigger data-testid="edit-pay-status-select">
  1542. <SelectValue placeholder="选择支付状态" />
  1543. </SelectTrigger>
  1544. </FormControl>
  1545. <SelectContent>
  1546. <SelectItem value="0">未支付</SelectItem>
  1547. <SelectItem value="1">支付中</SelectItem>
  1548. <SelectItem value="2">支付成功</SelectItem>
  1549. <SelectItem value="3">已退款</SelectItem>
  1550. <SelectItem value="4">支付失败</SelectItem>
  1551. <SelectItem value="5">订单关闭</SelectItem>
  1552. </SelectContent>
  1553. </Select>
  1554. <FormMessage />
  1555. </FormItem>
  1556. )}
  1557. />
  1558. <FormField
  1559. control={updateForm.control}
  1560. name="remark"
  1561. render={({ field }) => (
  1562. <FormItem>
  1563. <FormLabel>客户备注</FormLabel>
  1564. <FormControl>
  1565. <Textarea
  1566. placeholder="输入客户备注信息..."
  1567. className="resize-none"
  1568. data-testid="edit-remark-textarea"
  1569. {...field}
  1570. />
  1571. </FormControl>
  1572. <FormMessage />
  1573. </FormItem>
  1574. )}
  1575. />
  1576. <DialogFooter>
  1577. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  1578. 取消
  1579. </Button>
  1580. <Button type="submit" data-testid="order-save-button">保存</Button>
  1581. </DialogFooter>
  1582. </form>
  1583. </Form>
  1584. </DialogContent>
  1585. </Dialog>
  1586. {/* 订单详情模态框 */}
  1587. <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
  1588. <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
  1589. <DialogHeader>
  1590. <DialogTitle>订单详情</DialogTitle>
  1591. <DialogDescription>查看订单的详细信息</DialogDescription>
  1592. </DialogHeader>
  1593. {selectedOrder && (
  1594. <div className="space-y-4">
  1595. <div className="grid grid-cols-2 gap-4">
  1596. <div>
  1597. <h4 className="font-medium mb-2">订单信息</h4>
  1598. <div className="space-y-2 text-sm">
  1599. <div className="flex justify-between">
  1600. <span className="text-muted-foreground">订单号:</span>
  1601. <span>{selectedOrder.orderNo}</span>
  1602. </div>
  1603. <div className="flex justify-between">
  1604. <span className="text-muted-foreground">订单类型:</span>
  1605. <span>{orderTypeMap[selectedOrder.orderType as keyof typeof orderTypeMap]?.label}</span>
  1606. </div>
  1607. <div className="flex justify-between">
  1608. <span className="text-muted-foreground">订单金额:</span>
  1609. <span>{formatAmount(selectedOrder.amount)}</span>
  1610. </div>
  1611. <div className="flex justify-between">
  1612. <span className="text-muted-foreground">实付金额:</span>
  1613. <span>{formatAmount(selectedOrder.payAmount)}</span>
  1614. </div>
  1615. <div className="flex justify-between">
  1616. <span className="text-muted-foreground">运费:</span>
  1617. <span>{formatAmount(selectedOrder.freightAmount)}</span>
  1618. </div>
  1619. </div>
  1620. </div>
  1621. <div>
  1622. <h4 className="font-medium mb-2">状态信息</h4>
  1623. <div className="space-y-2 text-sm">
  1624. <div className="flex justify-between">
  1625. <span className="text-muted-foreground">订单状态:</span>
  1626. <span>{getStatusBadge(selectedOrder.state, 'order')}</span>
  1627. </div>
  1628. <div className="flex justify-between">
  1629. <span className="text-muted-foreground">支付状态:</span>
  1630. <span>{getStatusBadge(selectedOrder.payState, 'pay')}</span>
  1631. </div>
  1632. <div className="flex justify-between">
  1633. <span className="text-muted-foreground">支付方式:</span>
  1634. <span>{selectedOrder.payType === 1 ? '积分' : selectedOrder.payType === 2 ? '礼券' : '未选择'}</span>
  1635. </div>
  1636. <div className="flex justify-between">
  1637. <span className="text-muted-foreground">创建时间:</span>
  1638. <span>{format(new Date(selectedOrder.createdAt), 'yyyy-MM-dd HH:mm')}</span>
  1639. </div>
  1640. </div>
  1641. </div>
  1642. </div>
  1643. <div className="grid grid-cols-2 gap-4">
  1644. <div>
  1645. <h4 className="font-medium mb-2">用户信息</h4>
  1646. <div className="space-y-2 text-sm">
  1647. <div className="flex justify-between">
  1648. <span className="text-muted-foreground">用户名:</span>
  1649. <span>{selectedOrder.user?.username || '-'}</span>
  1650. </div>
  1651. <div className="flex justify-between">
  1652. <span className="text-muted-foreground">手机号:</span>
  1653. <span>{selectedOrder.userPhone || '-'}</span>
  1654. </div>
  1655. </div>
  1656. </div>
  1657. <div>
  1658. <h4 className="font-medium mb-2">收货信息</h4>
  1659. <div className="space-y-2 text-sm">
  1660. <div className="flex justify-between">
  1661. <span className="text-muted-foreground">收货人:</span>
  1662. <span>{selectedOrder.recevierName || '-'}</span>
  1663. </div>
  1664. <div className="flex justify-between">
  1665. <span className="text-muted-foreground">手机号:</span>
  1666. <span>{selectedOrder.receiverMobile || '-'}</span>
  1667. </div>
  1668. <div className="flex justify-between">
  1669. <span className="text-muted-foreground">地址:</span>
  1670. <span>{selectedOrder.address || '-'}</span>
  1671. </div>
  1672. </div>
  1673. </div>
  1674. </div>
  1675. {/* 订单商品信息 */}
  1676. <div>
  1677. <h4 className="font-medium mb-3 flex items-center gap-2">
  1678. <Package className="h-4 w-4" />
  1679. 订单商品
  1680. </h4>
  1681. <div className="border rounded-md overflow-hidden">
  1682. <div className="bg-muted px-4 py-2 border-b">
  1683. <div className="grid grid-cols-12 gap-4 text-sm font-medium">
  1684. <div className="col-span-5">商品信息</div>
  1685. <div className="col-span-2 text-center">单价</div>
  1686. <div className="col-span-2 text-center">数量</div>
  1687. <div className="col-span-3 text-right">小计</div>
  1688. </div>
  1689. </div>
  1690. <div className="divide-y">
  1691. {selectedOrder.orderGoods?.map((item, index) => (
  1692. <div key={item.id || index} className="px-4 py-3">
  1693. <div className="grid grid-cols-12 gap-4 items-center">
  1694. <div className="col-span-5 flex items-center gap-3">
  1695. {item.imageFile && (
  1696. <img
  1697. src={item.imageFile.fullUrl}
  1698. alt={item.goodsName}
  1699. className="w-12 h-12 rounded-md object-cover"
  1700. />
  1701. )}
  1702. <div>
  1703. <p className="font-medium text-sm">{item.goodsName}</p>
  1704. </div>
  1705. </div>
  1706. <div className="col-span-2 text-center text-sm">
  1707. {formatAmount(item.price)}
  1708. </div>
  1709. <div className="col-span-2 text-center text-sm">
  1710. {item.num}
  1711. </div>
  1712. <div className="col-span-3 text-right text-sm font-medium">
  1713. {formatAmount(item.price * item.num)}
  1714. </div>
  1715. </div>
  1716. </div>
  1717. ))}
  1718. </div>
  1719. {selectedOrder.orderGoods?.length === 0 && (
  1720. <div className="text-center py-8 text-muted-foreground">
  1721. 暂无商品信息
  1722. </div>
  1723. )}
  1724. </div>
  1725. </div>
  1726. {selectedOrder.remark && (
  1727. <div>
  1728. <h4 className="font-medium mb-2">客户备注</h4>
  1729. <p className="text-sm bg-muted p-3 rounded-md">{selectedOrder.remark}</p>
  1730. </div>
  1731. )}
  1732. </div>
  1733. )}
  1734. </DialogContent>
  1735. </Dialog>
  1736. {/* 发货模态框 */}
  1737. <Dialog open={deliveryModalOpen} onOpenChange={setDeliveryModalOpen}>
  1738. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  1739. <DialogHeader>
  1740. <DialogTitle>订单发货</DialogTitle>
  1741. <DialogDescription>选择发货方式并填写发货信息</DialogDescription>
  1742. </DialogHeader>
  1743. {deliveringOrder && (
  1744. <div className="space-y-4">
  1745. <div className="bg-muted p-3 rounded-md">
  1746. <h4 className="font-medium mb-2">订单信息</h4>
  1747. <div className="text-sm space-y-1">
  1748. <div className="flex justify-between">
  1749. <span className="text-muted-foreground">订单号:</span>
  1750. <span>{deliveringOrder.orderNo}</span>
  1751. </div>
  1752. <div className="flex justify-between">
  1753. <span className="text-muted-foreground">收货人:</span>
  1754. <span>{deliveringOrder.recevierName || '-'}</span>
  1755. </div>
  1756. <div className="flex justify-between">
  1757. <span className="text-muted-foreground">手机号:</span>
  1758. <span>{deliveringOrder.receiverMobile || '-'}</span>
  1759. </div>
  1760. <div className="flex justify-between">
  1761. <span className="text-muted-foreground">地址:</span>
  1762. <span>{deliveringOrder.address || '-'}</span>
  1763. </div>
  1764. </div>
  1765. </div>
  1766. <Form {...deliveryForm}>
  1767. <form onSubmit={deliveryForm.handleSubmit(handleDeliverySubmit)} className="space-y-4">
  1768. <FormField
  1769. control={deliveryForm.control}
  1770. name="deliveryType"
  1771. render={({ field }) => (
  1772. <FormItem>
  1773. <FormLabel>发货方式</FormLabel>
  1774. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  1775. <FormControl>
  1776. <SelectTrigger data-testid="delivery-type-select">
  1777. <SelectValue placeholder="选择发货方式" />
  1778. </SelectTrigger>
  1779. </FormControl>
  1780. <SelectContent>
  1781. <SelectItem value="1">物流快递</SelectItem>
  1782. <SelectItem value="2">同城配送</SelectItem>
  1783. <SelectItem value="3">虚拟发货</SelectItem>
  1784. <SelectItem value="4">用户自提</SelectItem>
  1785. </SelectContent>
  1786. </Select>
  1787. <FormMessage />
  1788. </FormItem>
  1789. )}
  1790. />
  1791. {deliveryForm.watch('deliveryType') === 1 && (
  1792. <>
  1793. <FormField
  1794. control={deliveryForm.control}
  1795. name="deliveryCompany"
  1796. render={({ field }) => (
  1797. <FormItem className="flex flex-col">
  1798. <FormLabel>快递公司</FormLabel>
  1799. <Popover>
  1800. <PopoverTrigger asChild>
  1801. <FormControl>
  1802. <Button
  1803. variant="outline"
  1804. role="combobox"
  1805. className="w-full justify-between"
  1806. disabled={loadingCompanies}
  1807. data-testid="delivery-company-select"
  1808. >
  1809. {field.value === '__manual__'
  1810. ? "手动输入"
  1811. : field.value && field.value !== '__select__'
  1812. ? deliveryCompanies.find(
  1813. (company) => company.delivery_id === field.value
  1814. )?.delivery_name || field.value // 如果是手动输入的名称,显示名称
  1815. : loadingCompanies
  1816. ? "加载中..."
  1817. : "选择快递公司"}
  1818. <Search className="ml-2 h-4 w-4 shrink-0 opacity-50" />
  1819. </Button>
  1820. </FormControl>
  1821. </PopoverTrigger>
  1822. <PopoverContent className="w-full p-0">
  1823. <Command>
  1824. <CommandInput
  1825. placeholder="搜索快递公司..."
  1826. className="h-9"
  1827. />
  1828. <CommandList>
  1829. <CommandEmpty>未找到匹配的快递公司</CommandEmpty>
  1830. <CommandGroup>
  1831. {deliveryCompanies.map((company) => (
  1832. <CommandItem
  1833. key={company.delivery_id}
  1834. value={company.delivery_name} // 搜索时使用名称
  1835. onSelect={() => {
  1836. field.onChange(company.delivery_id); // 存储ID
  1837. }}
  1838. >
  1839. {company.delivery_name}
  1840. {company.delivery_id === field.value && (
  1841. <Check className="ml-auto h-4 w-4" />
  1842. )}
  1843. </CommandItem>
  1844. ))}
  1845. </CommandGroup>
  1846. {deliveryCompanies.length === 0 && !loadingCompanies && (
  1847. <>
  1848. <CommandSeparator />
  1849. <CommandGroup>
  1850. <CommandItem
  1851. value="手动输入"
  1852. onSelect={() => {
  1853. // 切换到手动输入模式,使用特殊值标识
  1854. field.onChange('__manual__');
  1855. }}
  1856. className="text-muted-foreground"
  1857. >
  1858. 手动输入快递公司名称
  1859. </CommandItem>
  1860. </CommandGroup>
  1861. </>
  1862. )}
  1863. </CommandList>
  1864. </Command>
  1865. </PopoverContent>
  1866. </Popover>
  1867. {/* 手动输入框 */}
  1868. {field.value === '__manual__' && (
  1869. <div className="mt-2">
  1870. <div className="flex gap-2 mb-2">
  1871. <Button
  1872. type="button"
  1873. variant="outline"
  1874. size="sm"
  1875. onClick={() => field.onChange('__select__')}
  1876. >
  1877. ← 返回选择列表
  1878. </Button>
  1879. </div>
  1880. <FormControl>
  1881. <Input
  1882. placeholder="请输入快递公司名称"
  1883. data-testid="delivery-company-manual-input"
  1884. onChange={(e) => {
  1885. // 手动输入时,我们需要存储公司名称
  1886. // 但为了区分,我们可以存储为对象或特殊格式
  1887. // 这里简单处理:如果输入不为空,存储为字符串
  1888. field.onChange(e.target.value);
  1889. }}
  1890. />
  1891. </FormControl>
  1892. </div>
  1893. )}
  1894. <FormMessage />
  1895. </FormItem>
  1896. )}
  1897. />
  1898. <FormField
  1899. control={deliveryForm.control}
  1900. name="deliveryNo"
  1901. render={({ field }) => (
  1902. <FormItem>
  1903. <FormLabel>快递单号</FormLabel>
  1904. <FormControl>
  1905. <Input
  1906. placeholder="输入快递单号"
  1907. data-testid="delivery-no-input"
  1908. {...field}
  1909. />
  1910. </FormControl>
  1911. <FormMessage />
  1912. </FormItem>
  1913. )}
  1914. />
  1915. </>
  1916. )}
  1917. <FormField
  1918. control={deliveryForm.control}
  1919. name="deliveryRemark"
  1920. render={({ field }) => (
  1921. <FormItem>
  1922. <FormLabel>发货备注</FormLabel>
  1923. <FormControl>
  1924. <Textarea
  1925. placeholder="输入发货备注信息..."
  1926. className="resize-none"
  1927. data-testid="delivery-remark-textarea"
  1928. {...field}
  1929. />
  1930. </FormControl>
  1931. <FormMessage />
  1932. </FormItem>
  1933. )}
  1934. />
  1935. <DialogFooter>
  1936. <Button type="button" variant="outline" onClick={() => setDeliveryModalOpen(false)}>
  1937. 取消
  1938. </Button>
  1939. <Button type="submit" data-testid="delivery-submit-button">确认发货</Button>
  1940. </DialogFooter>
  1941. </form>
  1942. </Form>
  1943. </div>
  1944. )}
  1945. </DialogContent>
  1946. </Dialog>
  1947. </div>
  1948. );
  1949. };