PrintConfigManagement.tsx 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203
  1. import React, { useState, useEffect } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { toast } from 'sonner';
  4. import { zodResolver } from '@hookform/resolvers/zod';
  5. import { useForm } from 'react-hook-form';
  6. import { z } from 'zod';
  7. import {
  8. Settings,
  9. RefreshCw,
  10. AlertCircle,
  11. CheckCircle,
  12. XCircle,
  13. Clock,
  14. Repeat,
  15. Timer,
  16. FileText
  17. } from 'lucide-react';
  18. import {
  19. FeieConfig,
  20. ConfigKey,
  21. ConfigType,
  22. UpdateConfigRequest
  23. } from '../types/feiePrinter';
  24. import { createFeiePrinterClient } from '../api/feiePrinterClient';
  25. import {
  26. Button,
  27. Card,
  28. CardContent,
  29. CardDescription,
  30. CardHeader,
  31. CardTitle,
  32. Table,
  33. TableBody,
  34. TableCell,
  35. TableHead,
  36. TableHeader,
  37. TableRow,
  38. Badge,
  39. Dialog,
  40. DialogContent,
  41. DialogDescription,
  42. DialogFooter,
  43. DialogHeader,
  44. DialogTitle,
  45. Form,
  46. FormControl,
  47. FormField,
  48. FormItem,
  49. FormLabel,
  50. FormMessage,
  51. Input,
  52. Select,
  53. SelectContent,
  54. SelectItem,
  55. SelectTrigger,
  56. SelectValue,
  57. Switch,
  58. Textarea
  59. } from '@d8d/shared-ui-components';
  60. // 配置分组
  61. enum ConfigGroup {
  62. BASIC = 'basic',
  63. PRINT_POLICY = 'print_policy',
  64. TEMPLATE = 'template'
  65. }
  66. // 配置项定义
  67. interface ConfigItemDefinition {
  68. key: ConfigKey;
  69. label: string;
  70. description: string;
  71. type: ConfigType;
  72. defaultValue: string;
  73. group: ConfigGroup;
  74. validation?: z.ZodType<any>;
  75. component?: 'input' | 'textarea' | 'switch' | 'select' | 'number';
  76. options?: { value: string; label: string }[];
  77. min?: number;
  78. max?: number;
  79. step?: number;
  80. }
  81. // 配置项定义映射
  82. const configDefinitions: ConfigItemDefinition[] = [
  83. {
  84. key: ConfigKey.ENABLED,
  85. label: '启用飞鹅打印',
  86. description: '是否启用飞鹅打印功能',
  87. type: ConfigType.BOOLEAN,
  88. defaultValue: 'true',
  89. group: ConfigGroup.BASIC,
  90. component: 'switch'
  91. },
  92. {
  93. key: ConfigKey.DEFAULT_PRINTER_SN,
  94. label: '默认打印机序列号',
  95. description: '默认使用的打印机序列号',
  96. type: ConfigType.STRING,
  97. defaultValue: '',
  98. group: ConfigGroup.BASIC,
  99. component: 'input'
  100. },
  101. {
  102. key: ConfigKey.AUTO_PRINT_ON_PAYMENT,
  103. label: '支付成功时自动打印',
  104. description: '订单支付成功后是否自动打印小票',
  105. type: ConfigType.BOOLEAN,
  106. defaultValue: 'true',
  107. group: ConfigGroup.PRINT_POLICY,
  108. component: 'switch'
  109. },
  110. {
  111. key: ConfigKey.AUTO_PRINT_ON_SHIPPING,
  112. label: '发货时自动打印',
  113. description: '订单发货时是否自动打印发货单',
  114. type: ConfigType.BOOLEAN,
  115. defaultValue: 'true',
  116. group: ConfigGroup.PRINT_POLICY,
  117. component: 'switch'
  118. },
  119. {
  120. key: ConfigKey.ANTI_REFUND_DELAY,
  121. label: '防退款延迟时间(秒)',
  122. description: '支付成功后等待确认无退款的时间,默认120秒(2分钟)',
  123. type: ConfigType.NUMBER,
  124. defaultValue: '120',
  125. group: ConfigGroup.PRINT_POLICY,
  126. component: 'number',
  127. min: 0,
  128. max: 600,
  129. step: 10
  130. },
  131. {
  132. key: ConfigKey.RETRY_MAX_COUNT,
  133. label: '最大重试次数',
  134. description: '打印失败时的最大重试次数',
  135. type: ConfigType.NUMBER,
  136. defaultValue: '3',
  137. group: ConfigGroup.PRINT_POLICY,
  138. component: 'number',
  139. min: 0,
  140. max: 10,
  141. step: 1
  142. },
  143. {
  144. key: ConfigKey.RETRY_INTERVAL,
  145. label: '重试间隔(秒)',
  146. description: '打印失败后重试的间隔时间',
  147. type: ConfigType.NUMBER,
  148. defaultValue: '30',
  149. group: ConfigGroup.PRINT_POLICY,
  150. component: 'number',
  151. min: 5,
  152. max: 300,
  153. step: 5
  154. },
  155. {
  156. key: ConfigKey.TASK_TIMEOUT,
  157. label: '任务超时时间(秒)',
  158. description: '打印任务的最大执行时间,超时后自动取消',
  159. type: ConfigType.NUMBER,
  160. defaultValue: '300',
  161. group: ConfigGroup.PRINT_POLICY,
  162. component: 'number',
  163. min: 30,
  164. max: 1800,
  165. step: 30
  166. },
  167. {
  168. key: ConfigKey.RECEIPT_TEMPLATE,
  169. label: '小票模板',
  170. description: '小票打印的模板内容,支持变量替换',
  171. type: ConfigType.STRING,
  172. defaultValue: `订单号: {orderNo}
  173. 时间: {orderTime}
  174. 商品: {goodsList}
  175. 合计: {totalAmount}
  176. 地址: {address}
  177. 联系电话: {phone}`,
  178. group: ConfigGroup.TEMPLATE,
  179. component: 'textarea'
  180. },
  181. {
  182. key: ConfigKey.SHIPPING_TEMPLATE,
  183. label: '发货单模板',
  184. description: '发货单打印的模板内容,支持变量替换',
  185. type: ConfigType.STRING,
  186. defaultValue: `发货单
  187. 订单号: {orderNo}
  188. 发货时间: {shippingTime}
  189. 收货人: {receiver}
  190. 地址: {address}
  191. 联系电话: {phone}
  192. 商品列表:
  193. {goodsList}`,
  194. group: ConfigGroup.TEMPLATE,
  195. component: 'textarea'
  196. }
  197. ];
  198. // 配置类型标签映射
  199. const typeLabelMap: Record<ConfigType, string> = {
  200. [ConfigType.STRING]: '字符串',
  201. [ConfigType.JSON]: 'JSON',
  202. [ConfigType.BOOLEAN]: '布尔值',
  203. [ConfigType.NUMBER]: '数字'
  204. };
  205. // 配置类型颜色映射
  206. const typeColorMap: Record<ConfigType, string> = {
  207. [ConfigType.STRING]: 'bg-blue-100 text-blue-800 border-blue-200',
  208. [ConfigType.JSON]: 'bg-purple-100 text-purple-800 border-purple-200',
  209. [ConfigType.BOOLEAN]: 'bg-green-100 text-green-800 border-green-200',
  210. [ConfigType.NUMBER]: 'bg-orange-100 text-orange-800 border-orange-200'
  211. };
  212. // 更新配置表单验证模式
  213. const updateConfigSchema = z.object({
  214. configValue: z.string().min(1, '配置值不能为空')
  215. });
  216. type UpdateConfigFormValues = z.infer<typeof updateConfigSchema>;
  217. interface PrintConfigManagementProps {
  218. /**
  219. * API基础URL
  220. * @default '/api'
  221. */
  222. baseURL?: string;
  223. /**
  224. * 租户ID
  225. */
  226. tenantId?: number;
  227. /**
  228. * 认证token
  229. */
  230. authToken?: string;
  231. }
  232. /**
  233. * 打印配置管理组件
  234. * 提供打印配置的查询、编辑和管理功能
  235. */
  236. export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
  237. baseURL = '/api',
  238. tenantId,
  239. authToken
  240. }) => {
  241. const [activeGroup, setActiveGroup] = useState<ConfigGroup>(ConfigGroup.BASIC);
  242. const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
  243. const [selectedConfig, setSelectedConfig] = useState<FeieConfig | null>(null);
  244. const [configMap, setConfigMap] = useState<Record<string, FeieConfig>>({});
  245. const [tableEdits, setTableEdits] = useState<Record<string, string>>({});
  246. const [groupEdits, setGroupEdits] = useState<Record<string, string>>({});
  247. const queryClient = useQueryClient();
  248. const feieClient = createFeiePrinterClient(baseURL);
  249. // 设置认证和租户信息
  250. useEffect(() => {
  251. if (authToken) {
  252. feieClient.setAuthToken(authToken);
  253. }
  254. if (tenantId) {
  255. feieClient.setTenantId(tenantId);
  256. }
  257. }, [authToken, tenantId, feieClient]);
  258. // 查询配置列表
  259. const {
  260. data: configList,
  261. isLoading,
  262. isError,
  263. refetch
  264. } = useQuery({
  265. queryKey: ['printConfigs', tenantId],
  266. queryFn: () => feieClient.getPrintConfigs(),
  267. enabled: !!tenantId
  268. });
  269. // 当配置数据加载成功后,更新配置映射
  270. useEffect(() => {
  271. if (configList?.data) {
  272. const map: Record<string, FeieConfig> = {};
  273. configList.data.forEach((config: FeieConfig) => {
  274. map[config.configKey] = config;
  275. });
  276. setConfigMap(map);
  277. }
  278. }, [configList]);
  279. // 当切换分组时,清空分组编辑记录
  280. useEffect(() => {
  281. setGroupEdits({});
  282. }, [activeGroup]);
  283. // 更新配置表单
  284. const updateForm = useForm<UpdateConfigFormValues>({
  285. resolver: zodResolver(updateConfigSchema),
  286. defaultValues: {
  287. configValue: ''
  288. }
  289. });
  290. // 更新配置Mutation(用于编辑对话框)
  291. const updateConfigMutation = useMutation({
  292. mutationFn: ({ configKey, data }: { configKey: string; data: UpdateConfigRequest }) =>
  293. feieClient.updatePrintConfig(configKey, data),
  294. onSuccess: () => {
  295. toast.success('配置更新成功');
  296. setIsEditDialogOpen(false);
  297. setSelectedConfig(null);
  298. queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
  299. },
  300. onError: (error: Error) => {
  301. toast.error(`更新配置失败: ${error.message}`);
  302. }
  303. });
  304. // 即时保存配置Mutation(用于直接编辑)
  305. const instantSaveConfigMutation = useMutation({
  306. mutationFn: ({ configKey, configValue }: { configKey: string; configValue: string }) =>
  307. feieClient.updatePrintConfig(configKey, { configValue }),
  308. onSuccess: () => {
  309. queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
  310. },
  311. onError: (error: Error) => {
  312. toast.error(`保存配置失败: ${error.message}`);
  313. }
  314. });
  315. // 重置配置Mutation
  316. const resetConfigMutation = useMutation({
  317. mutationFn: async (configKey: string) => {
  318. const definition = configDefinitions.find(def => def.key === configKey);
  319. if (!definition) throw new Error('配置定义不存在');
  320. return feieClient.updatePrintConfig(configKey, {
  321. configValue: definition.defaultValue
  322. });
  323. },
  324. onSuccess: () => {
  325. toast.success('配置重置成功');
  326. queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
  327. },
  328. onError: (error: Error) => {
  329. toast.error(`重置配置失败: ${error.message}`);
  330. }
  331. });
  332. // 处理更新配置
  333. const handleUpdateConfig = (data: UpdateConfigFormValues) => {
  334. if (!selectedConfig) return;
  335. updateConfigMutation.mutate({
  336. configKey: selectedConfig.configKey,
  337. data
  338. });
  339. };
  340. // 打开编辑对话框
  341. const openEditDialog = (config: FeieConfig) => {
  342. setSelectedConfig(config);
  343. updateForm.reset({
  344. configValue: config.configValue
  345. });
  346. setIsEditDialogOpen(true);
  347. };
  348. // 批量保存配置Mutation
  349. const batchSaveConfigMutation = useMutation({
  350. mutationFn: async (edits: Record<string, string>) => {
  351. const promises = Object.entries(edits).map(([configKey, configValue]) =>
  352. feieClient.updatePrintConfig(configKey, { configValue })
  353. );
  354. return Promise.all(promises);
  355. },
  356. onSuccess: () => {
  357. toast.success('批量保存成功');
  358. // 清空编辑记录
  359. setTableEdits({});
  360. setGroupEdits({});
  361. queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
  362. },
  363. onError: (error: Error) => {
  364. toast.error(`批量保存失败: ${error.message}`);
  365. }
  366. });
  367. // 处理表格批量保存
  368. const handleTableBatchSave = () => {
  369. const editsToSave = Object.entries(tableEdits).filter(([key, value]) => {
  370. const currentValue = getConfigValue(key);
  371. return value !== currentValue;
  372. });
  373. if (editsToSave.length === 0) {
  374. toast.info('没有需要保存的修改');
  375. return;
  376. }
  377. const editsObject = Object.fromEntries(editsToSave);
  378. batchSaveConfigMutation.mutate(editsObject);
  379. };
  380. // 处理分组批量保存
  381. const handleGroupBatchSave = () => {
  382. const editsToSave = Object.entries(groupEdits).filter(([key, value]) => {
  383. const currentValue = getConfigValue(key);
  384. return value !== currentValue;
  385. });
  386. if (editsToSave.length === 0) {
  387. toast.info('没有需要保存的修改');
  388. return;
  389. }
  390. const editsObject = Object.fromEntries(editsToSave);
  391. batchSaveConfigMutation.mutate(editsObject);
  392. };
  393. // 处理重置配置
  394. const handleResetConfig = (configKey: string) => {
  395. resetConfigMutation.mutate(configKey);
  396. };
  397. // 获取配置值显示
  398. const getConfigValueDisplay = (config: FeieConfig): string => {
  399. if (config.configType === ConfigType.BOOLEAN) {
  400. return config.configValue === 'true' ? '是' : '否';
  401. }
  402. if (config.configType === ConfigType.JSON) {
  403. try {
  404. const parsed = JSON.parse(config.configValue);
  405. return JSON.stringify(parsed, null, 2);
  406. } catch {
  407. return config.configValue;
  408. }
  409. }
  410. return config.configValue;
  411. };
  412. // 获取配置值
  413. const getConfigValue = (configKey: string): string => {
  414. const config = configMap[configKey];
  415. if (config) return config.configValue;
  416. // 如果配置不存在,返回默认值
  417. const definition = configDefinitions.find(def => def.key === configKey);
  418. return definition?.defaultValue || '';
  419. };
  420. // 渲染配置项组件(批量保存模式)
  421. const renderBatchConfigItem = (definition: ConfigItemDefinition) => {
  422. const config = configMap[definition.key];
  423. const value = getConfigValue(definition.key);
  424. const editedValue = groupEdits[definition.key];
  425. return (
  426. <div key={definition.key} className="space-y-2">
  427. <div className="flex items-center justify-between">
  428. <div>
  429. <h4 className="font-medium">{definition.label}</h4>
  430. <p className="text-sm text-muted-foreground">{definition.description}</p>
  431. </div>
  432. <div className="flex items-center space-x-2">
  433. <Badge variant="outline" className={typeColorMap[definition.type]}>
  434. {typeLabelMap[definition.type]}
  435. </Badge>
  436. <Button
  437. variant="ghost"
  438. size="sm"
  439. onClick={() => handleResetConfig(definition.key)}
  440. disabled={resetConfigMutation.isPending}
  441. title="重置为默认值"
  442. >
  443. <RefreshCw className="h-4 w-4" />
  444. </Button>
  445. </div>
  446. </div>
  447. {definition.component === 'switch' ? (
  448. <div className="flex items-center space-x-2">
  449. <Switch
  450. checked={editedValue !== undefined ? editedValue === 'true' : value === 'true'}
  451. onCheckedChange={(checked) => {
  452. const newValue = checked.toString();
  453. setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
  454. }}
  455. />
  456. <span className="text-sm">
  457. {editedValue !== undefined
  458. ? (editedValue === 'true' ? '已启用' : '已禁用')
  459. : (value === 'true' ? '已启用' : '已禁用')}
  460. </span>
  461. {editedValue !== undefined && editedValue !== value && (
  462. <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
  463. )}
  464. </div>
  465. ) : definition.component === 'textarea' ? (
  466. <div className="relative">
  467. <Textarea
  468. value={editedValue !== undefined ? editedValue : value}
  469. onChange={(e) => {
  470. const newValue = e.target.value;
  471. setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
  472. }}
  473. rows={6}
  474. className="font-mono text-sm"
  475. />
  476. {editedValue !== undefined && editedValue !== value && (
  477. <div className="absolute right-2 top-2">
  478. <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
  479. </div>
  480. )}
  481. </div>
  482. ) : definition.component === 'number' ? (
  483. <div className="flex items-center space-x-2">
  484. <div className="relative">
  485. <Input
  486. type="number"
  487. value={editedValue !== undefined ? editedValue : value}
  488. onChange={(e) => {
  489. const newValue = e.target.value;
  490. setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
  491. }}
  492. min={definition.min}
  493. max={definition.max}
  494. step={definition.step}
  495. className="w-32"
  496. />
  497. {editedValue !== undefined && editedValue !== value && (
  498. <div className="absolute -right-6 top-1/2 transform -translate-y-1/2">
  499. <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
  500. </div>
  501. )}
  502. </div>
  503. {definition.key === ConfigKey.ANTI_REFUND_DELAY && (
  504. <span className="text-sm text-muted-foreground">
  505. ({Math.floor(parseInt(editedValue !== undefined ? editedValue : value) / 60)}分{parseInt(editedValue !== undefined ? editedValue : value) % 60}秒)
  506. </span>
  507. )}
  508. </div>
  509. ) : (
  510. <div className="relative">
  511. <Input
  512. value={editedValue !== undefined ? editedValue : value}
  513. onChange={(e) => {
  514. const newValue = e.target.value;
  515. setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
  516. }}
  517. placeholder={`请输入${definition.label}`}
  518. />
  519. {editedValue !== undefined && editedValue !== value && (
  520. <div className="absolute -right-6 top-1/2 transform -translate-y-1/2">
  521. <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
  522. </div>
  523. )}
  524. </div>
  525. )}
  526. {definition.key === ConfigKey.DEFAULT_PRINTER_SN && value && (
  527. <p className="text-sm text-muted-foreground">
  528. 当前默认打印机: <span className="font-mono">{editedValue !== undefined ? editedValue : value}</span>
  529. </p>
  530. )}
  531. </div>
  532. );
  533. };
  534. // 渲染配置项组件(即时保存模式)
  535. const renderConfigItem = (definition: ConfigItemDefinition) => {
  536. const config = configMap[definition.key];
  537. const value = getConfigValue(definition.key);
  538. return (
  539. <div key={definition.key} className="space-y-2">
  540. <div className="flex items-center justify-between">
  541. <div>
  542. <h4 className="font-medium">{definition.label}</h4>
  543. <p className="text-sm text-muted-foreground">{definition.description}</p>
  544. </div>
  545. <div className="flex items-center space-x-2">
  546. <Badge variant="outline" className={typeColorMap[definition.type]}>
  547. {typeLabelMap[definition.type]}
  548. </Badge>
  549. <Button
  550. variant="ghost"
  551. size="sm"
  552. onClick={() => handleResetConfig(definition.key)}
  553. disabled={resetConfigMutation.isPending}
  554. title="重置为默认值"
  555. >
  556. <RefreshCw className="h-4 w-4" />
  557. </Button>
  558. </div>
  559. </div>
  560. {definition.component === 'switch' ? (
  561. <div className="flex items-center space-x-2">
  562. <Switch
  563. checked={value === 'true'}
  564. onCheckedChange={(checked) => {
  565. const newValue = checked.toString();
  566. // 更新本地状态
  567. const newConfig = { ...config, configValue: newValue };
  568. setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
  569. // 即时保存到服务器
  570. instantSaveConfigMutation.mutate({
  571. configKey: definition.key,
  572. configValue: newValue
  573. });
  574. }}
  575. disabled={instantSaveConfigMutation.isPending}
  576. />
  577. <span className="text-sm">{value === 'true' ? '已启用' : '已禁用'}</span>
  578. {instantSaveConfigMutation.isPending && (
  579. <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
  580. )}
  581. </div>
  582. ) : definition.component === 'textarea' ? (
  583. <div className="relative">
  584. <Textarea
  585. value={value}
  586. onChange={(e) => {
  587. const newValue = e.target.value;
  588. // 更新本地状态
  589. const newConfig = { ...config, configValue: newValue };
  590. setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
  591. }}
  592. onBlur={(e) => {
  593. // 失去焦点时保存
  594. const newValue = e.target.value;
  595. if (newValue !== value) {
  596. instantSaveConfigMutation.mutate({
  597. configKey: definition.key,
  598. configValue: newValue
  599. });
  600. }
  601. }}
  602. rows={6}
  603. className="font-mono text-sm"
  604. disabled={instantSaveConfigMutation.isPending}
  605. />
  606. {instantSaveConfigMutation.isPending && (
  607. <div className="absolute right-2 top-2">
  608. <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
  609. </div>
  610. )}
  611. </div>
  612. ) : definition.component === 'number' ? (
  613. <div className="flex items-center space-x-2">
  614. <div className="relative">
  615. <Input
  616. type="number"
  617. value={value}
  618. onChange={(e) => {
  619. const newValue = e.target.value;
  620. // 更新本地状态
  621. const newConfig = { ...config, configValue: newValue };
  622. setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
  623. }}
  624. onBlur={(e) => {
  625. // 失去焦点时保存
  626. const newValue = e.target.value;
  627. if (newValue !== value) {
  628. instantSaveConfigMutation.mutate({
  629. configKey: definition.key,
  630. configValue: newValue
  631. });
  632. }
  633. }}
  634. min={definition.min}
  635. max={definition.max}
  636. step={definition.step}
  637. className="w-32"
  638. disabled={instantSaveConfigMutation.isPending}
  639. />
  640. {instantSaveConfigMutation.isPending && (
  641. <div className="absolute right-2 top-2">
  642. <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
  643. </div>
  644. )}
  645. </div>
  646. {definition.key === ConfigKey.ANTI_REFUND_DELAY && (
  647. <span className="text-sm text-muted-foreground">
  648. ({Math.floor(parseInt(value) / 60)}分{parseInt(value) % 60}秒)
  649. </span>
  650. )}
  651. </div>
  652. ) : (
  653. <div className="relative">
  654. <Input
  655. value={value}
  656. onChange={(e) => {
  657. const newValue = e.target.value;
  658. // 更新本地状态
  659. const newConfig = { ...config, configValue: newValue };
  660. setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
  661. }}
  662. onBlur={(e) => {
  663. // 失去焦点时保存
  664. const newValue = e.target.value;
  665. if (newValue !== value) {
  666. instantSaveConfigMutation.mutate({
  667. configKey: definition.key,
  668. configValue: newValue
  669. });
  670. }
  671. }}
  672. placeholder={`请输入${definition.label}`}
  673. disabled={instantSaveConfigMutation.isPending}
  674. />
  675. {instantSaveConfigMutation.isPending && (
  676. <div className="absolute right-2 top-2">
  677. <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
  678. </div>
  679. )}
  680. </div>
  681. )}
  682. {definition.key === ConfigKey.DEFAULT_PRINTER_SN && value && (
  683. <p className="text-sm text-muted-foreground">
  684. 当前默认打印机: <span className="font-mono">{value}</span>
  685. </p>
  686. )}
  687. </div>
  688. );
  689. };
  690. return (
  691. <div className="space-y-6">
  692. {/* 标题和操作栏 */}
  693. <div className="flex items-center justify-between">
  694. <div>
  695. <h2 className="text-2xl font-bold tracking-tight">打印配置管理</h2>
  696. <p className="text-muted-foreground">
  697. 管理飞鹅打印的配置项,包括基础配置、打印策略和模板配置
  698. </p>
  699. </div>
  700. <div className="flex items-center space-x-2">
  701. <Button
  702. variant="outline"
  703. onClick={() => refetch()}
  704. disabled={isLoading}
  705. >
  706. <RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
  707. 刷新
  708. </Button>
  709. </div>
  710. </div>
  711. {/* 配置分组按钮 */}
  712. <div className="flex space-x-2 mb-4">
  713. <Button
  714. variant={activeGroup === ConfigGroup.BASIC ? "default" : "outline"}
  715. onClick={() => setActiveGroup(ConfigGroup.BASIC)}
  716. className="flex items-center"
  717. >
  718. <Settings className="mr-2 h-4 w-4" />
  719. 基础配置
  720. </Button>
  721. <Button
  722. variant={activeGroup === ConfigGroup.PRINT_POLICY ? "default" : "outline"}
  723. onClick={() => setActiveGroup(ConfigGroup.PRINT_POLICY)}
  724. className="flex items-center"
  725. >
  726. <Timer className="mr-2 h-4 w-4" />
  727. 打印策略
  728. </Button>
  729. <Button
  730. variant={activeGroup === ConfigGroup.TEMPLATE ? "default" : "outline"}
  731. onClick={() => setActiveGroup(ConfigGroup.TEMPLATE)}
  732. className="flex items-center"
  733. >
  734. <FileText className="mr-2 h-4 w-4" />
  735. 模板配置
  736. </Button>
  737. </div>
  738. {/* 加载状态 */}
  739. {isLoading ? (
  740. <Card className="mt-4">
  741. <CardContent className="pt-6">
  742. <div className="flex items-center justify-center py-8">
  743. <div className="text-center">
  744. <RefreshCw className="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
  745. <p className="mt-2 text-sm text-muted-foreground">加载配置中...</p>
  746. </div>
  747. </div>
  748. </CardContent>
  749. </Card>
  750. ) : isError ? (
  751. <Card className="mt-4">
  752. <CardContent className="pt-6">
  753. <div className="flex items-center justify-center py-8">
  754. <div className="text-center">
  755. <AlertCircle className="h-8 w-8 mx-auto text-red-500" />
  756. <p className="mt-2 text-sm text-red-600">加载配置失败</p>
  757. <Button
  758. variant="outline"
  759. size="sm"
  760. className="mt-4"
  761. onClick={() => refetch()}
  762. >
  763. 重试
  764. </Button>
  765. </div>
  766. </div>
  767. </CardContent>
  768. </Card>
  769. ) : (
  770. <>
  771. {/* 基础配置 */}
  772. {activeGroup === ConfigGroup.BASIC && (
  773. <Card>
  774. <CardHeader>
  775. <CardTitle>基础配置</CardTitle>
  776. <CardDescription>
  777. 配置飞鹅打印的基础功能,包括启用状态和默认打印机
  778. </CardDescription>
  779. </CardHeader>
  780. <CardContent className="space-y-6">
  781. {configDefinitions
  782. .filter(def => def.group === ConfigGroup.BASIC)
  783. .map(renderConfigItem)}
  784. </CardContent>
  785. </Card>
  786. )}
  787. {/* 打印策略 */}
  788. {activeGroup === ConfigGroup.PRINT_POLICY && (
  789. <Card>
  790. <CardHeader>
  791. <div className="flex items-center justify-between">
  792. <div>
  793. <CardTitle>打印策略</CardTitle>
  794. <CardDescription>
  795. 配置打印任务的触发条件、重试策略和超时设置
  796. </CardDescription>
  797. </div>
  798. <div className="flex items-center space-x-2">
  799. <Button
  800. onClick={handleGroupBatchSave}
  801. disabled={isLoading || batchSaveConfigMutation.isPending || Object.keys(groupEdits).length === 0}
  802. size="sm"
  803. >
  804. {batchSaveConfigMutation.isPending ? (
  805. <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
  806. ) : (
  807. <CheckCircle className="mr-2 h-4 w-4" />
  808. )}
  809. 批量保存
  810. {Object.keys(groupEdits).length > 0 && (
  811. <span className="ml-2 h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
  812. )}
  813. </Button>
  814. {Object.keys(groupEdits).length > 0 && (
  815. <Button
  816. onClick={() => setGroupEdits({})}
  817. variant="outline"
  818. size="sm"
  819. disabled={batchSaveConfigMutation.isPending}
  820. >
  821. <XCircle className="mr-2 h-4 w-4" />
  822. 取消编辑
  823. </Button>
  824. )}
  825. </div>
  826. </div>
  827. </CardHeader>
  828. <CardContent className="space-y-6">
  829. {configDefinitions
  830. .filter(def => def.group === ConfigGroup.PRINT_POLICY)
  831. .map(renderBatchConfigItem)}
  832. {/* 策略说明 */}
  833. <div className="rounded-lg border bg-muted/50 p-4">
  834. <h4 className="font-medium mb-2">策略说明</h4>
  835. <ul className="text-sm text-muted-foreground space-y-1">
  836. <li className="flex items-start">
  837. <Clock className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
  838. <span>
  839. <strong>防退款延迟</strong>: 支付成功后等待指定时间确认无退款再打印,避免无效打印
  840. </span>
  841. </li>
  842. <li className="flex items-start">
  843. <Repeat className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
  844. <span>
  845. <strong>重试机制</strong>: 打印失败时自动重试,最多重试指定次数,每次间隔指定时间
  846. </span>
  847. </li>
  848. <li className="flex items-start">
  849. <Timer className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
  850. <span>
  851. <strong>超时取消</strong>: 打印任务执行超过指定时间自动取消,避免任务阻塞
  852. </span>
  853. </li>
  854. </ul>
  855. </div>
  856. </CardContent>
  857. </Card>
  858. )}
  859. {/* 模板配置 */}
  860. {activeGroup === ConfigGroup.TEMPLATE && (
  861. <Card>
  862. <CardHeader>
  863. <div className="flex items-center justify-between">
  864. <div>
  865. <CardTitle>模板配置</CardTitle>
  866. <CardDescription>
  867. 配置小票和发货单的打印模板,支持变量替换
  868. </CardDescription>
  869. </div>
  870. <div className="flex items-center space-x-2">
  871. <Button
  872. onClick={handleGroupBatchSave}
  873. disabled={isLoading || batchSaveConfigMutation.isPending || Object.keys(groupEdits).length === 0}
  874. size="sm"
  875. >
  876. {batchSaveConfigMutation.isPending ? (
  877. <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
  878. ) : (
  879. <CheckCircle className="mr-2 h-4 w-4" />
  880. )}
  881. 批量保存
  882. {Object.keys(groupEdits).length > 0 && (
  883. <span className="ml-2 h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
  884. )}
  885. </Button>
  886. {Object.keys(groupEdits).length > 0 && (
  887. <Button
  888. onClick={() => setGroupEdits({})}
  889. variant="outline"
  890. size="sm"
  891. disabled={batchSaveConfigMutation.isPending}
  892. >
  893. <XCircle className="mr-2 h-4 w-4" />
  894. 取消编辑
  895. </Button>
  896. )}
  897. </div>
  898. </div>
  899. </CardHeader>
  900. <CardContent className="space-y-6">
  901. {configDefinitions
  902. .filter(def => def.group === ConfigGroup.TEMPLATE)
  903. .map(renderBatchConfigItem)}
  904. {/* 模板变量说明 */}
  905. <div className="rounded-lg border bg-muted/50 p-4">
  906. <h4 className="font-medium mb-2">可用变量</h4>
  907. <div className="grid grid-cols-2 gap-4">
  908. <div>
  909. <h5 className="text-sm font-medium mb-1">订单信息</h5>
  910. <ul className="text-sm text-muted-foreground space-y-1">
  911. <li><code>{'{orderNo}'}</code> - 订单号</li>
  912. <li><code>{'{orderTime}'}</code> - 订单时间</li>
  913. <li><code>{'{totalAmount}'}</code> - 订单总金额</li>
  914. <li><code>{'{paymentMethod}'}</code> - 支付方式</li>
  915. </ul>
  916. </div>
  917. <div>
  918. <h5 className="text-sm font-medium mb-1">收货信息</h5>
  919. <ul className="text-sm text-muted-foreground space-y-1">
  920. <li><code>{'{receiver}'}</code> - 收货人姓名</li>
  921. <li><code>{'{phone}'}</code> - 联系电话</li>
  922. <li><code>{'{address}'}</code> - 收货地址</li>
  923. <li><code>{'{shippingTime}'}</code> - 发货时间</li>
  924. </ul>
  925. </div>
  926. </div>
  927. <div className="mt-4">
  928. <h5 className="text-sm font-medium mb-1">商品信息</h5>
  929. <ul className="text-sm text-muted-foreground space-y-1">
  930. <li><code>{'{goodsList}'}</code> - 商品列表(自动格式化)</li>
  931. <li><code>{'{goodsName}'}</code> - 商品名称</li>
  932. <li><code>{'{goodsPrice}'}</code> - 商品价格</li>
  933. <li><code>{'{goodsQuantity}'}</code> - 商品数量</li>
  934. </ul>
  935. </div>
  936. </div>
  937. </CardContent>
  938. </Card>
  939. )}
  940. </>
  941. )}
  942. {/* 配置列表表格视图(备用) */}
  943. <Card>
  944. <CardHeader>
  945. <CardTitle>所有配置项</CardTitle>
  946. <CardDescription>
  947. 以表格形式查看所有配置项的当前值
  948. </CardDescription>
  949. </CardHeader>
  950. <CardContent>
  951. {configList?.data.length === 0 ? (
  952. <div className="flex flex-col items-center justify-center py-12 text-center">
  953. <Settings className="h-12 w-12 text-muted-foreground mb-4" />
  954. <h3 className="text-lg font-medium">暂无配置</h3>
  955. <p className="text-sm text-muted-foreground mt-2">
  956. 还没有配置信息,系统将使用默认配置
  957. </p>
  958. </div>
  959. ) : (
  960. <div className="rounded-md border">
  961. <Table>
  962. <TableHeader>
  963. <TableRow>
  964. <TableHead>配置键</TableHead>
  965. <TableHead>描述</TableHead>
  966. <TableHead>类型</TableHead>
  967. <TableHead>当前值</TableHead>
  968. <TableHead>默认值</TableHead>
  969. <TableHead className="text-right">操作</TableHead>
  970. </TableRow>
  971. </TableHeader>
  972. <TableBody>
  973. {configDefinitions.map((definition) => {
  974. const config = configMap[definition.key];
  975. const value = getConfigValue(definition.key);
  976. const isDefault = !config || config.configValue === definition.defaultValue;
  977. return (
  978. <TableRow key={definition.key}>
  979. <TableCell className="font-mono text-sm">{definition.key}</TableCell>
  980. <TableCell>
  981. <div>
  982. <div className="font-medium">{definition.label}</div>
  983. <div className="text-sm text-muted-foreground">
  984. {definition.description}
  985. </div>
  986. </div>
  987. </TableCell>
  988. <TableCell>
  989. <Badge variant="outline" className={typeColorMap[definition.type]}>
  990. {typeLabelMap[definition.type]}
  991. </Badge>
  992. </TableCell>
  993. <TableCell>
  994. <div className="max-w-xs truncate" title={getConfigValueDisplay(config || {
  995. ...definition,
  996. configValue: value
  997. } as any)}>
  998. {definition.type === ConfigType.BOOLEAN ? (
  999. <div className="flex items-center">
  1000. {value === 'true' ? (
  1001. <CheckCircle className="h-4 w-4 text-green-500 mr-2" />
  1002. ) : (
  1003. <XCircle className="h-4 w-4 text-red-500 mr-2" />
  1004. )}
  1005. <span>{value === 'true' ? '是' : '否'}</span>
  1006. </div>
  1007. ) : (
  1008. <span className="font-mono text-sm">{value}</span>
  1009. )}
  1010. </div>
  1011. </TableCell>
  1012. <TableCell>
  1013. <span className="font-mono text-sm">{definition.defaultValue}</span>
  1014. </TableCell>
  1015. <TableCell className="text-right">
  1016. <div className="flex items-center justify-end space-x-2">
  1017. {!isDefault && (
  1018. <Button
  1019. variant="ghost"
  1020. size="sm"
  1021. onClick={() => handleResetConfig(definition.key)}
  1022. disabled={resetConfigMutation.isPending}
  1023. title="重置为默认值"
  1024. >
  1025. <RefreshCw className="h-4 w-4" />
  1026. </Button>
  1027. )}
  1028. <Button
  1029. variant="ghost"
  1030. size="sm"
  1031. onClick={() => {
  1032. // 如果config不存在,创建一个临时的配置对象
  1033. const configToEdit = config || {
  1034. configKey: definition.key,
  1035. configValue: definition.defaultValue,
  1036. configType: definition.type,
  1037. description: definition.description
  1038. };
  1039. openEditDialog(configToEdit);
  1040. }}
  1041. title="编辑"
  1042. >
  1043. 编辑
  1044. </Button>
  1045. </div>
  1046. </TableCell>
  1047. </TableRow>
  1048. );
  1049. })}
  1050. </TableBody>
  1051. </Table>
  1052. </div>
  1053. )}
  1054. </CardContent>
  1055. </Card>
  1056. {/* 编辑配置对话框 */}
  1057. <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
  1058. <DialogContent className="sm:max-w-[500px]">
  1059. <DialogHeader>
  1060. <DialogTitle>编辑配置</DialogTitle>
  1061. <DialogDescription>
  1062. 修改配置项 "{selectedConfig?.configKey}" 的值
  1063. </DialogDescription>
  1064. </DialogHeader>
  1065. <Form {...updateForm}>
  1066. <form onSubmit={updateForm.handleSubmit(handleUpdateConfig)} className="space-y-4">
  1067. {selectedConfig && (
  1068. <>
  1069. <div className="rounded-lg border p-4">
  1070. <div className="space-y-2">
  1071. <div className="flex items-center justify-between">
  1072. <span className="text-sm font-medium">配置键</span>
  1073. <span className="font-mono text-sm">{selectedConfig.configKey}</span>
  1074. </div>
  1075. <div className="flex items-center justify-between">
  1076. <span className="text-sm font-medium">类型</span>
  1077. <Badge variant="outline" className={typeColorMap[selectedConfig.configType]}>
  1078. {typeLabelMap[selectedConfig.configType]}
  1079. </Badge>
  1080. </div>
  1081. {selectedConfig.description && (
  1082. <div>
  1083. <span className="text-sm font-medium">描述</span>
  1084. <p className="text-sm text-muted-foreground mt-1">
  1085. {selectedConfig.description}
  1086. </p>
  1087. </div>
  1088. )}
  1089. </div>
  1090. </div>
  1091. <FormField
  1092. control={updateForm.control}
  1093. name="configValue"
  1094. render={({ field }) => (
  1095. <FormItem>
  1096. <FormLabel>配置值</FormLabel>
  1097. {selectedConfig.configType === ConfigType.BOOLEAN ? (
  1098. <Select onValueChange={field.onChange} defaultValue={field.value}>
  1099. <FormControl>
  1100. <SelectTrigger>
  1101. <SelectValue placeholder="选择配置值" />
  1102. </SelectTrigger>
  1103. </FormControl>
  1104. <SelectContent>
  1105. <SelectItem value="true">是(启用)</SelectItem>
  1106. <SelectItem value="false">否(禁用)</SelectItem>
  1107. </SelectContent>
  1108. </Select>
  1109. ) : selectedConfig.configType === ConfigType.JSON ? (
  1110. <FormControl>
  1111. <Textarea
  1112. placeholder="请输入JSON格式的配置值"
  1113. className="font-mono text-sm"
  1114. rows={6}
  1115. {...field}
  1116. />
  1117. </FormControl>
  1118. ) : (
  1119. <FormControl>
  1120. <Input placeholder="请输入配置值" {...field} />
  1121. </FormControl>
  1122. )}
  1123. <FormMessage />
  1124. </FormItem>
  1125. )}
  1126. />
  1127. </>
  1128. )}
  1129. <DialogFooter>
  1130. <Button
  1131. type="button"
  1132. variant="outline"
  1133. onClick={() => setIsEditDialogOpen(false)}
  1134. disabled={updateConfigMutation.isPending}
  1135. >
  1136. 取消
  1137. </Button>
  1138. <Button type="submit" disabled={updateConfigMutation.isPending}>
  1139. {updateConfigMutation.isPending ? '更新中...' : '更新配置'}
  1140. </Button>
  1141. </DialogFooter>
  1142. </form>
  1143. </Form>
  1144. </DialogContent>
  1145. </Dialog>
  1146. </div>
  1147. );
  1148. };