| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203 |
- import React, { useState, useEffect } from 'react';
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
- import { toast } from 'sonner';
- import { zodResolver } from '@hookform/resolvers/zod';
- import { useForm } from 'react-hook-form';
- import { z } from 'zod';
- import {
- Settings,
- RefreshCw,
- AlertCircle,
- CheckCircle,
- XCircle,
- Clock,
- Repeat,
- Timer,
- FileText
- } from 'lucide-react';
- import {
- FeieConfig,
- ConfigKey,
- ConfigType,
- UpdateConfigRequest
- } from '../types/feiePrinter';
- import { createFeiePrinterClient } from '../api/feiePrinterClient';
- import {
- Button,
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- Badge,
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- Input,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- Switch,
- Textarea
- } from '@d8d/shared-ui-components';
- // 配置分组
- enum ConfigGroup {
- BASIC = 'basic',
- PRINT_POLICY = 'print_policy',
- TEMPLATE = 'template'
- }
- // 配置项定义
- interface ConfigItemDefinition {
- key: ConfigKey;
- label: string;
- description: string;
- type: ConfigType;
- defaultValue: string;
- group: ConfigGroup;
- validation?: z.ZodType<any>;
- component?: 'input' | 'textarea' | 'switch' | 'select' | 'number';
- options?: { value: string; label: string }[];
- min?: number;
- max?: number;
- step?: number;
- }
- // 配置项定义映射
- const configDefinitions: ConfigItemDefinition[] = [
- {
- key: ConfigKey.ENABLED,
- label: '启用飞鹅打印',
- description: '是否启用飞鹅打印功能',
- type: ConfigType.BOOLEAN,
- defaultValue: 'true',
- group: ConfigGroup.BASIC,
- component: 'switch'
- },
- {
- key: ConfigKey.DEFAULT_PRINTER_SN,
- label: '默认打印机序列号',
- description: '默认使用的打印机序列号',
- type: ConfigType.STRING,
- defaultValue: '',
- group: ConfigGroup.BASIC,
- component: 'input'
- },
- {
- key: ConfigKey.AUTO_PRINT_ON_PAYMENT,
- label: '支付成功时自动打印',
- description: '订单支付成功后是否自动打印小票',
- type: ConfigType.BOOLEAN,
- defaultValue: 'true',
- group: ConfigGroup.PRINT_POLICY,
- component: 'switch'
- },
- {
- key: ConfigKey.AUTO_PRINT_ON_SHIPPING,
- label: '发货时自动打印',
- description: '订单发货时是否自动打印发货单',
- type: ConfigType.BOOLEAN,
- defaultValue: 'true',
- group: ConfigGroup.PRINT_POLICY,
- component: 'switch'
- },
- {
- key: ConfigKey.ANTI_REFUND_DELAY,
- label: '防退款延迟时间(秒)',
- description: '支付成功后等待确认无退款的时间,默认120秒(2分钟)',
- type: ConfigType.NUMBER,
- defaultValue: '120',
- group: ConfigGroup.PRINT_POLICY,
- component: 'number',
- min: 0,
- max: 600,
- step: 10
- },
- {
- key: ConfigKey.RETRY_MAX_COUNT,
- label: '最大重试次数',
- description: '打印失败时的最大重试次数',
- type: ConfigType.NUMBER,
- defaultValue: '3',
- group: ConfigGroup.PRINT_POLICY,
- component: 'number',
- min: 0,
- max: 10,
- step: 1
- },
- {
- key: ConfigKey.RETRY_INTERVAL,
- label: '重试间隔(秒)',
- description: '打印失败后重试的间隔时间',
- type: ConfigType.NUMBER,
- defaultValue: '30',
- group: ConfigGroup.PRINT_POLICY,
- component: 'number',
- min: 5,
- max: 300,
- step: 5
- },
- {
- key: ConfigKey.TASK_TIMEOUT,
- label: '任务超时时间(秒)',
- description: '打印任务的最大执行时间,超时后自动取消',
- type: ConfigType.NUMBER,
- defaultValue: '300',
- group: ConfigGroup.PRINT_POLICY,
- component: 'number',
- min: 30,
- max: 1800,
- step: 30
- },
- {
- key: ConfigKey.RECEIPT_TEMPLATE,
- label: '小票模板',
- description: '小票打印的模板内容,支持变量替换',
- type: ConfigType.STRING,
- defaultValue: `订单号: {orderNo}
- 时间: {orderTime}
- 商品: {goodsList}
- 合计: {totalAmount}
- 地址: {address}
- 联系电话: {phone}`,
- group: ConfigGroup.TEMPLATE,
- component: 'textarea'
- },
- {
- key: ConfigKey.SHIPPING_TEMPLATE,
- label: '发货单模板',
- description: '发货单打印的模板内容,支持变量替换',
- type: ConfigType.STRING,
- defaultValue: `发货单
- 订单号: {orderNo}
- 发货时间: {shippingTime}
- 收货人: {receiver}
- 地址: {address}
- 联系电话: {phone}
- 商品列表:
- {goodsList}`,
- group: ConfigGroup.TEMPLATE,
- component: 'textarea'
- }
- ];
- // 配置类型标签映射
- const typeLabelMap: Record<ConfigType, string> = {
- [ConfigType.STRING]: '字符串',
- [ConfigType.JSON]: 'JSON',
- [ConfigType.BOOLEAN]: '布尔值',
- [ConfigType.NUMBER]: '数字'
- };
- // 配置类型颜色映射
- const typeColorMap: Record<ConfigType, string> = {
- [ConfigType.STRING]: 'bg-blue-100 text-blue-800 border-blue-200',
- [ConfigType.JSON]: 'bg-purple-100 text-purple-800 border-purple-200',
- [ConfigType.BOOLEAN]: 'bg-green-100 text-green-800 border-green-200',
- [ConfigType.NUMBER]: 'bg-orange-100 text-orange-800 border-orange-200'
- };
- // 更新配置表单验证模式
- const updateConfigSchema = z.object({
- configValue: z.string().min(1, '配置值不能为空')
- });
- type UpdateConfigFormValues = z.infer<typeof updateConfigSchema>;
- interface PrintConfigManagementProps {
- /**
- * API基础URL
- * @default '/api'
- */
- baseURL?: string;
- /**
- * 租户ID
- */
- tenantId?: number;
- /**
- * 认证token
- */
- authToken?: string;
- }
- /**
- * 打印配置管理组件
- * 提供打印配置的查询、编辑和管理功能
- */
- export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
- baseURL = '/api',
- tenantId,
- authToken
- }) => {
- const [activeGroup, setActiveGroup] = useState<ConfigGroup>(ConfigGroup.BASIC);
- const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
- const [selectedConfig, setSelectedConfig] = useState<FeieConfig | null>(null);
- const [configMap, setConfigMap] = useState<Record<string, FeieConfig>>({});
- const [tableEdits, setTableEdits] = useState<Record<string, string>>({});
- const [groupEdits, setGroupEdits] = useState<Record<string, string>>({});
- const queryClient = useQueryClient();
- const feieClient = createFeiePrinterClient(baseURL);
- // 设置认证和租户信息
- useEffect(() => {
- if (authToken) {
- feieClient.setAuthToken(authToken);
- }
- if (tenantId) {
- feieClient.setTenantId(tenantId);
- }
- }, [authToken, tenantId, feieClient]);
- // 查询配置列表
- const {
- data: configList,
- isLoading,
- isError,
- refetch
- } = useQuery({
- queryKey: ['printConfigs', tenantId],
- queryFn: () => feieClient.getPrintConfigs(),
- enabled: !!tenantId
- });
- // 当配置数据加载成功后,更新配置映射
- useEffect(() => {
- if (configList?.data) {
- const map: Record<string, FeieConfig> = {};
- configList.data.forEach((config: FeieConfig) => {
- map[config.configKey] = config;
- });
- setConfigMap(map);
- }
- }, [configList]);
- // 当切换分组时,清空分组编辑记录
- useEffect(() => {
- setGroupEdits({});
- }, [activeGroup]);
- // 更新配置表单
- const updateForm = useForm<UpdateConfigFormValues>({
- resolver: zodResolver(updateConfigSchema),
- defaultValues: {
- configValue: ''
- }
- });
- // 更新配置Mutation(用于编辑对话框)
- const updateConfigMutation = useMutation({
- mutationFn: ({ configKey, data }: { configKey: string; data: UpdateConfigRequest }) =>
- feieClient.updatePrintConfig(configKey, data),
- onSuccess: () => {
- toast.success('配置更新成功');
- setIsEditDialogOpen(false);
- setSelectedConfig(null);
- queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
- },
- onError: (error: Error) => {
- toast.error(`更新配置失败: ${error.message}`);
- }
- });
- // 即时保存配置Mutation(用于直接编辑)
- const instantSaveConfigMutation = useMutation({
- mutationFn: ({ configKey, configValue }: { configKey: string; configValue: string }) =>
- feieClient.updatePrintConfig(configKey, { configValue }),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
- },
- onError: (error: Error) => {
- toast.error(`保存配置失败: ${error.message}`);
- }
- });
- // 重置配置Mutation
- const resetConfigMutation = useMutation({
- mutationFn: async (configKey: string) => {
- const definition = configDefinitions.find(def => def.key === configKey);
- if (!definition) throw new Error('配置定义不存在');
- return feieClient.updatePrintConfig(configKey, {
- configValue: definition.defaultValue
- });
- },
- onSuccess: () => {
- toast.success('配置重置成功');
- queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
- },
- onError: (error: Error) => {
- toast.error(`重置配置失败: ${error.message}`);
- }
- });
- // 处理更新配置
- const handleUpdateConfig = (data: UpdateConfigFormValues) => {
- if (!selectedConfig) return;
- updateConfigMutation.mutate({
- configKey: selectedConfig.configKey,
- data
- });
- };
- // 打开编辑对话框
- const openEditDialog = (config: FeieConfig) => {
- setSelectedConfig(config);
- updateForm.reset({
- configValue: config.configValue
- });
- setIsEditDialogOpen(true);
- };
- // 批量保存配置Mutation
- const batchSaveConfigMutation = useMutation({
- mutationFn: async (edits: Record<string, string>) => {
- const promises = Object.entries(edits).map(([configKey, configValue]) =>
- feieClient.updatePrintConfig(configKey, { configValue })
- );
- return Promise.all(promises);
- },
- onSuccess: () => {
- toast.success('批量保存成功');
- // 清空编辑记录
- setTableEdits({});
- setGroupEdits({});
- queryClient.invalidateQueries({ queryKey: ['printConfigs'] });
- },
- onError: (error: Error) => {
- toast.error(`批量保存失败: ${error.message}`);
- }
- });
- // 处理表格批量保存
- const handleTableBatchSave = () => {
- const editsToSave = Object.entries(tableEdits).filter(([key, value]) => {
- const currentValue = getConfigValue(key);
- return value !== currentValue;
- });
- if (editsToSave.length === 0) {
- toast.info('没有需要保存的修改');
- return;
- }
- const editsObject = Object.fromEntries(editsToSave);
- batchSaveConfigMutation.mutate(editsObject);
- };
- // 处理分组批量保存
- const handleGroupBatchSave = () => {
- const editsToSave = Object.entries(groupEdits).filter(([key, value]) => {
- const currentValue = getConfigValue(key);
- return value !== currentValue;
- });
- if (editsToSave.length === 0) {
- toast.info('没有需要保存的修改');
- return;
- }
- const editsObject = Object.fromEntries(editsToSave);
- batchSaveConfigMutation.mutate(editsObject);
- };
- // 处理重置配置
- const handleResetConfig = (configKey: string) => {
- resetConfigMutation.mutate(configKey);
- };
- // 获取配置值显示
- const getConfigValueDisplay = (config: FeieConfig): string => {
- if (config.configType === ConfigType.BOOLEAN) {
- return config.configValue === 'true' ? '是' : '否';
- }
- if (config.configType === ConfigType.JSON) {
- try {
- const parsed = JSON.parse(config.configValue);
- return JSON.stringify(parsed, null, 2);
- } catch {
- return config.configValue;
- }
- }
- return config.configValue;
- };
- // 获取配置值
- const getConfigValue = (configKey: string): string => {
- const config = configMap[configKey];
- if (config) return config.configValue;
- // 如果配置不存在,返回默认值
- const definition = configDefinitions.find(def => def.key === configKey);
- return definition?.defaultValue || '';
- };
- // 渲染配置项组件(批量保存模式)
- const renderBatchConfigItem = (definition: ConfigItemDefinition) => {
- const config = configMap[definition.key];
- const value = getConfigValue(definition.key);
- const editedValue = groupEdits[definition.key];
- return (
- <div key={definition.key} className="space-y-2">
- <div className="flex items-center justify-between">
- <div>
- <h4 className="font-medium">{definition.label}</h4>
- <p className="text-sm text-muted-foreground">{definition.description}</p>
- </div>
- <div className="flex items-center space-x-2">
- <Badge variant="outline" className={typeColorMap[definition.type]}>
- {typeLabelMap[definition.type]}
- </Badge>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleResetConfig(definition.key)}
- disabled={resetConfigMutation.isPending}
- title="重置为默认值"
- >
- <RefreshCw className="h-4 w-4" />
- </Button>
- </div>
- </div>
- {definition.component === 'switch' ? (
- <div className="flex items-center space-x-2">
- <Switch
- checked={editedValue !== undefined ? editedValue === 'true' : value === 'true'}
- onCheckedChange={(checked) => {
- const newValue = checked.toString();
- setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
- }}
- />
- <span className="text-sm">
- {editedValue !== undefined
- ? (editedValue === 'true' ? '已启用' : '已禁用')
- : (value === 'true' ? '已启用' : '已禁用')}
- </span>
- {editedValue !== undefined && editedValue !== value && (
- <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
- )}
- </div>
- ) : definition.component === 'textarea' ? (
- <div className="relative">
- <Textarea
- value={editedValue !== undefined ? editedValue : value}
- onChange={(e) => {
- const newValue = e.target.value;
- setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
- }}
- rows={6}
- className="font-mono text-sm"
- />
- {editedValue !== undefined && editedValue !== value && (
- <div className="absolute right-2 top-2">
- <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
- </div>
- )}
- </div>
- ) : definition.component === 'number' ? (
- <div className="flex items-center space-x-2">
- <div className="relative">
- <Input
- type="number"
- value={editedValue !== undefined ? editedValue : value}
- onChange={(e) => {
- const newValue = e.target.value;
- setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
- }}
- min={definition.min}
- max={definition.max}
- step={definition.step}
- className="w-32"
- />
- {editedValue !== undefined && editedValue !== value && (
- <div className="absolute -right-6 top-1/2 transform -translate-y-1/2">
- <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
- </div>
- )}
- </div>
- {definition.key === ConfigKey.ANTI_REFUND_DELAY && (
- <span className="text-sm text-muted-foreground">
- ({Math.floor(parseInt(editedValue !== undefined ? editedValue : value) / 60)}分{parseInt(editedValue !== undefined ? editedValue : value) % 60}秒)
- </span>
- )}
- </div>
- ) : (
- <div className="relative">
- <Input
- value={editedValue !== undefined ? editedValue : value}
- onChange={(e) => {
- const newValue = e.target.value;
- setGroupEdits(prev => ({ ...prev, [definition.key]: newValue }));
- }}
- placeholder={`请输入${definition.label}`}
- />
- {editedValue !== undefined && editedValue !== value && (
- <div className="absolute -right-6 top-1/2 transform -translate-y-1/2">
- <div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" title="有未保存的修改" />
- </div>
- )}
- </div>
- )}
- {definition.key === ConfigKey.DEFAULT_PRINTER_SN && value && (
- <p className="text-sm text-muted-foreground">
- 当前默认打印机: <span className="font-mono">{editedValue !== undefined ? editedValue : value}</span>
- </p>
- )}
- </div>
- );
- };
- // 渲染配置项组件(即时保存模式)
- const renderConfigItem = (definition: ConfigItemDefinition) => {
- const config = configMap[definition.key];
- const value = getConfigValue(definition.key);
- return (
- <div key={definition.key} className="space-y-2">
- <div className="flex items-center justify-between">
- <div>
- <h4 className="font-medium">{definition.label}</h4>
- <p className="text-sm text-muted-foreground">{definition.description}</p>
- </div>
- <div className="flex items-center space-x-2">
- <Badge variant="outline" className={typeColorMap[definition.type]}>
- {typeLabelMap[definition.type]}
- </Badge>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleResetConfig(definition.key)}
- disabled={resetConfigMutation.isPending}
- title="重置为默认值"
- >
- <RefreshCw className="h-4 w-4" />
- </Button>
- </div>
- </div>
- {definition.component === 'switch' ? (
- <div className="flex items-center space-x-2">
- <Switch
- checked={value === 'true'}
- onCheckedChange={(checked) => {
- const newValue = checked.toString();
- // 更新本地状态
- const newConfig = { ...config, configValue: newValue };
- setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
- // 即时保存到服务器
- instantSaveConfigMutation.mutate({
- configKey: definition.key,
- configValue: newValue
- });
- }}
- disabled={instantSaveConfigMutation.isPending}
- />
- <span className="text-sm">{value === 'true' ? '已启用' : '已禁用'}</span>
- {instantSaveConfigMutation.isPending && (
- <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
- )}
- </div>
- ) : definition.component === 'textarea' ? (
- <div className="relative">
- <Textarea
- value={value}
- onChange={(e) => {
- const newValue = e.target.value;
- // 更新本地状态
- const newConfig = { ...config, configValue: newValue };
- setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
- }}
- onBlur={(e) => {
- // 失去焦点时保存
- const newValue = e.target.value;
- if (newValue !== value) {
- instantSaveConfigMutation.mutate({
- configKey: definition.key,
- configValue: newValue
- });
- }
- }}
- rows={6}
- className="font-mono text-sm"
- disabled={instantSaveConfigMutation.isPending}
- />
- {instantSaveConfigMutation.isPending && (
- <div className="absolute right-2 top-2">
- <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
- </div>
- )}
- </div>
- ) : definition.component === 'number' ? (
- <div className="flex items-center space-x-2">
- <div className="relative">
- <Input
- type="number"
- value={value}
- onChange={(e) => {
- const newValue = e.target.value;
- // 更新本地状态
- const newConfig = { ...config, configValue: newValue };
- setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
- }}
- onBlur={(e) => {
- // 失去焦点时保存
- const newValue = e.target.value;
- if (newValue !== value) {
- instantSaveConfigMutation.mutate({
- configKey: definition.key,
- configValue: newValue
- });
- }
- }}
- min={definition.min}
- max={definition.max}
- step={definition.step}
- className="w-32"
- disabled={instantSaveConfigMutation.isPending}
- />
- {instantSaveConfigMutation.isPending && (
- <div className="absolute right-2 top-2">
- <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
- </div>
- )}
- </div>
- {definition.key === ConfigKey.ANTI_REFUND_DELAY && (
- <span className="text-sm text-muted-foreground">
- ({Math.floor(parseInt(value) / 60)}分{parseInt(value) % 60}秒)
- </span>
- )}
- </div>
- ) : (
- <div className="relative">
- <Input
- value={value}
- onChange={(e) => {
- const newValue = e.target.value;
- // 更新本地状态
- const newConfig = { ...config, configValue: newValue };
- setConfigMap(prev => ({ ...prev, [definition.key]: newConfig }));
- }}
- onBlur={(e) => {
- // 失去焦点时保存
- const newValue = e.target.value;
- if (newValue !== value) {
- instantSaveConfigMutation.mutate({
- configKey: definition.key,
- configValue: newValue
- });
- }
- }}
- placeholder={`请输入${definition.label}`}
- disabled={instantSaveConfigMutation.isPending}
- />
- {instantSaveConfigMutation.isPending && (
- <div className="absolute right-2 top-2">
- <RefreshCw className="h-3 w-3 animate-spin text-muted-foreground" />
- </div>
- )}
- </div>
- )}
- {definition.key === ConfigKey.DEFAULT_PRINTER_SN && value && (
- <p className="text-sm text-muted-foreground">
- 当前默认打印机: <span className="font-mono">{value}</span>
- </p>
- )}
- </div>
- );
- };
- return (
- <div className="space-y-6">
- {/* 标题和操作栏 */}
- <div className="flex items-center justify-between">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">打印配置管理</h2>
- <p className="text-muted-foreground">
- 管理飞鹅打印的配置项,包括基础配置、打印策略和模板配置
- </p>
- </div>
- <div className="flex items-center space-x-2">
- <Button
- variant="outline"
- onClick={() => refetch()}
- disabled={isLoading}
- >
- <RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
- 刷新
- </Button>
- </div>
- </div>
- {/* 配置分组按钮 */}
- <div className="flex space-x-2 mb-4">
- <Button
- variant={activeGroup === ConfigGroup.BASIC ? "default" : "outline"}
- onClick={() => setActiveGroup(ConfigGroup.BASIC)}
- className="flex items-center"
- >
- <Settings className="mr-2 h-4 w-4" />
- 基础配置
- </Button>
- <Button
- variant={activeGroup === ConfigGroup.PRINT_POLICY ? "default" : "outline"}
- onClick={() => setActiveGroup(ConfigGroup.PRINT_POLICY)}
- className="flex items-center"
- >
- <Timer className="mr-2 h-4 w-4" />
- 打印策略
- </Button>
- <Button
- variant={activeGroup === ConfigGroup.TEMPLATE ? "default" : "outline"}
- onClick={() => setActiveGroup(ConfigGroup.TEMPLATE)}
- className="flex items-center"
- >
- <FileText className="mr-2 h-4 w-4" />
- 模板配置
- </Button>
- </div>
- {/* 加载状态 */}
- {isLoading ? (
- <Card className="mt-4">
- <CardContent className="pt-6">
- <div className="flex items-center justify-center py-8">
- <div className="text-center">
- <RefreshCw className="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
- <p className="mt-2 text-sm text-muted-foreground">加载配置中...</p>
- </div>
- </div>
- </CardContent>
- </Card>
- ) : isError ? (
- <Card className="mt-4">
- <CardContent className="pt-6">
- <div className="flex items-center justify-center py-8">
- <div className="text-center">
- <AlertCircle className="h-8 w-8 mx-auto text-red-500" />
- <p className="mt-2 text-sm text-red-600">加载配置失败</p>
- <Button
- variant="outline"
- size="sm"
- className="mt-4"
- onClick={() => refetch()}
- >
- 重试
- </Button>
- </div>
- </div>
- </CardContent>
- </Card>
- ) : (
- <>
- {/* 基础配置 */}
- {activeGroup === ConfigGroup.BASIC && (
- <Card>
- <CardHeader>
- <CardTitle>基础配置</CardTitle>
- <CardDescription>
- 配置飞鹅打印的基础功能,包括启用状态和默认打印机
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-6">
- {configDefinitions
- .filter(def => def.group === ConfigGroup.BASIC)
- .map(renderConfigItem)}
- </CardContent>
- </Card>
- )}
- {/* 打印策略 */}
- {activeGroup === ConfigGroup.PRINT_POLICY && (
- <Card>
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <CardTitle>打印策略</CardTitle>
- <CardDescription>
- 配置打印任务的触发条件、重试策略和超时设置
- </CardDescription>
- </div>
- <div className="flex items-center space-x-2">
- <Button
- onClick={handleGroupBatchSave}
- disabled={isLoading || batchSaveConfigMutation.isPending || Object.keys(groupEdits).length === 0}
- size="sm"
- >
- {batchSaveConfigMutation.isPending ? (
- <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
- ) : (
- <CheckCircle className="mr-2 h-4 w-4" />
- )}
- 批量保存
- {Object.keys(groupEdits).length > 0 && (
- <span className="ml-2 h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
- )}
- </Button>
- {Object.keys(groupEdits).length > 0 && (
- <Button
- onClick={() => setGroupEdits({})}
- variant="outline"
- size="sm"
- disabled={batchSaveConfigMutation.isPending}
- >
- <XCircle className="mr-2 h-4 w-4" />
- 取消编辑
- </Button>
- )}
- </div>
- </div>
- </CardHeader>
- <CardContent className="space-y-6">
- {configDefinitions
- .filter(def => def.group === ConfigGroup.PRINT_POLICY)
- .map(renderBatchConfigItem)}
- {/* 策略说明 */}
- <div className="rounded-lg border bg-muted/50 p-4">
- <h4 className="font-medium mb-2">策略说明</h4>
- <ul className="text-sm text-muted-foreground space-y-1">
- <li className="flex items-start">
- <Clock className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
- <span>
- <strong>防退款延迟</strong>: 支付成功后等待指定时间确认无退款再打印,避免无效打印
- </span>
- </li>
- <li className="flex items-start">
- <Repeat className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
- <span>
- <strong>重试机制</strong>: 打印失败时自动重试,最多重试指定次数,每次间隔指定时间
- </span>
- </li>
- <li className="flex items-start">
- <Timer className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
- <span>
- <strong>超时取消</strong>: 打印任务执行超过指定时间自动取消,避免任务阻塞
- </span>
- </li>
- </ul>
- </div>
- </CardContent>
- </Card>
- )}
- {/* 模板配置 */}
- {activeGroup === ConfigGroup.TEMPLATE && (
- <Card>
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <CardTitle>模板配置</CardTitle>
- <CardDescription>
- 配置小票和发货单的打印模板,支持变量替换
- </CardDescription>
- </div>
- <div className="flex items-center space-x-2">
- <Button
- onClick={handleGroupBatchSave}
- disabled={isLoading || batchSaveConfigMutation.isPending || Object.keys(groupEdits).length === 0}
- size="sm"
- >
- {batchSaveConfigMutation.isPending ? (
- <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
- ) : (
- <CheckCircle className="mr-2 h-4 w-4" />
- )}
- 批量保存
- {Object.keys(groupEdits).length > 0 && (
- <span className="ml-2 h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
- )}
- </Button>
- {Object.keys(groupEdits).length > 0 && (
- <Button
- onClick={() => setGroupEdits({})}
- variant="outline"
- size="sm"
- disabled={batchSaveConfigMutation.isPending}
- >
- <XCircle className="mr-2 h-4 w-4" />
- 取消编辑
- </Button>
- )}
- </div>
- </div>
- </CardHeader>
- <CardContent className="space-y-6">
- {configDefinitions
- .filter(def => def.group === ConfigGroup.TEMPLATE)
- .map(renderBatchConfigItem)}
- {/* 模板变量说明 */}
- <div className="rounded-lg border bg-muted/50 p-4">
- <h4 className="font-medium mb-2">可用变量</h4>
- <div className="grid grid-cols-2 gap-4">
- <div>
- <h5 className="text-sm font-medium mb-1">订单信息</h5>
- <ul className="text-sm text-muted-foreground space-y-1">
- <li><code>{'{orderNo}'}</code> - 订单号</li>
- <li><code>{'{orderTime}'}</code> - 订单时间</li>
- <li><code>{'{totalAmount}'}</code> - 订单总金额</li>
- <li><code>{'{paymentMethod}'}</code> - 支付方式</li>
- </ul>
- </div>
- <div>
- <h5 className="text-sm font-medium mb-1">收货信息</h5>
- <ul className="text-sm text-muted-foreground space-y-1">
- <li><code>{'{receiver}'}</code> - 收货人姓名</li>
- <li><code>{'{phone}'}</code> - 联系电话</li>
- <li><code>{'{address}'}</code> - 收货地址</li>
- <li><code>{'{shippingTime}'}</code> - 发货时间</li>
- </ul>
- </div>
- </div>
- <div className="mt-4">
- <h5 className="text-sm font-medium mb-1">商品信息</h5>
- <ul className="text-sm text-muted-foreground space-y-1">
- <li><code>{'{goodsList}'}</code> - 商品列表(自动格式化)</li>
- <li><code>{'{goodsName}'}</code> - 商品名称</li>
- <li><code>{'{goodsPrice}'}</code> - 商品价格</li>
- <li><code>{'{goodsQuantity}'}</code> - 商品数量</li>
- </ul>
- </div>
- </div>
- </CardContent>
- </Card>
- )}
- </>
- )}
- {/* 配置列表表格视图(备用) */}
- <Card>
- <CardHeader>
- <CardTitle>所有配置项</CardTitle>
- <CardDescription>
- 以表格形式查看所有配置项的当前值
- </CardDescription>
- </CardHeader>
- <CardContent>
- {configList?.data.length === 0 ? (
- <div className="flex flex-col items-center justify-center py-12 text-center">
- <Settings className="h-12 w-12 text-muted-foreground mb-4" />
- <h3 className="text-lg font-medium">暂无配置</h3>
- <p className="text-sm text-muted-foreground mt-2">
- 还没有配置信息,系统将使用默认配置
- </p>
- </div>
- ) : (
- <div className="rounded-md border">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>配置键</TableHead>
- <TableHead>描述</TableHead>
- <TableHead>类型</TableHead>
- <TableHead>当前值</TableHead>
- <TableHead>默认值</TableHead>
- <TableHead className="text-right">操作</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {configDefinitions.map((definition) => {
- const config = configMap[definition.key];
- const value = getConfigValue(definition.key);
- const isDefault = !config || config.configValue === definition.defaultValue;
- return (
- <TableRow key={definition.key}>
- <TableCell className="font-mono text-sm">{definition.key}</TableCell>
- <TableCell>
- <div>
- <div className="font-medium">{definition.label}</div>
- <div className="text-sm text-muted-foreground">
- {definition.description}
- </div>
- </div>
- </TableCell>
- <TableCell>
- <Badge variant="outline" className={typeColorMap[definition.type]}>
- {typeLabelMap[definition.type]}
- </Badge>
- </TableCell>
- <TableCell>
- <div className="max-w-xs truncate" title={getConfigValueDisplay(config || {
- ...definition,
- configValue: value
- } as any)}>
- {definition.type === ConfigType.BOOLEAN ? (
- <div className="flex items-center">
- {value === 'true' ? (
- <CheckCircle className="h-4 w-4 text-green-500 mr-2" />
- ) : (
- <XCircle className="h-4 w-4 text-red-500 mr-2" />
- )}
- <span>{value === 'true' ? '是' : '否'}</span>
- </div>
- ) : (
- <span className="font-mono text-sm">{value}</span>
- )}
- </div>
- </TableCell>
- <TableCell>
- <span className="font-mono text-sm">{definition.defaultValue}</span>
- </TableCell>
- <TableCell className="text-right">
- <div className="flex items-center justify-end space-x-2">
- {!isDefault && (
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleResetConfig(definition.key)}
- disabled={resetConfigMutation.isPending}
- title="重置为默认值"
- >
- <RefreshCw className="h-4 w-4" />
- </Button>
- )}
- <Button
- variant="ghost"
- size="sm"
- onClick={() => {
- // 如果config不存在,创建一个临时的配置对象
- const configToEdit = config || {
- configKey: definition.key,
- configValue: definition.defaultValue,
- configType: definition.type,
- description: definition.description
- };
- openEditDialog(configToEdit);
- }}
- title="编辑"
- >
- 编辑
- </Button>
- </div>
- </TableCell>
- </TableRow>
- );
- })}
- </TableBody>
- </Table>
- </div>
- )}
- </CardContent>
- </Card>
- {/* 编辑配置对话框 */}
- <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
- <DialogContent className="sm:max-w-[500px]">
- <DialogHeader>
- <DialogTitle>编辑配置</DialogTitle>
- <DialogDescription>
- 修改配置项 "{selectedConfig?.configKey}" 的值
- </DialogDescription>
- </DialogHeader>
- <Form {...updateForm}>
- <form onSubmit={updateForm.handleSubmit(handleUpdateConfig)} className="space-y-4">
- {selectedConfig && (
- <>
- <div className="rounded-lg border p-4">
- <div className="space-y-2">
- <div className="flex items-center justify-between">
- <span className="text-sm font-medium">配置键</span>
- <span className="font-mono text-sm">{selectedConfig.configKey}</span>
- </div>
- <div className="flex items-center justify-between">
- <span className="text-sm font-medium">类型</span>
- <Badge variant="outline" className={typeColorMap[selectedConfig.configType]}>
- {typeLabelMap[selectedConfig.configType]}
- </Badge>
- </div>
- {selectedConfig.description && (
- <div>
- <span className="text-sm font-medium">描述</span>
- <p className="text-sm text-muted-foreground mt-1">
- {selectedConfig.description}
- </p>
- </div>
- )}
- </div>
- </div>
- <FormField
- control={updateForm.control}
- name="configValue"
- render={({ field }) => (
- <FormItem>
- <FormLabel>配置值</FormLabel>
- {selectedConfig.configType === ConfigType.BOOLEAN ? (
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="选择配置值" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="true">是(启用)</SelectItem>
- <SelectItem value="false">否(禁用)</SelectItem>
- </SelectContent>
- </Select>
- ) : selectedConfig.configType === ConfigType.JSON ? (
- <FormControl>
- <Textarea
- placeholder="请输入JSON格式的配置值"
- className="font-mono text-sm"
- rows={6}
- {...field}
- />
- </FormControl>
- ) : (
- <FormControl>
- <Input placeholder="请输入配置值" {...field} />
- </FormControl>
- )}
- <FormMessage />
- </FormItem>
- )}
- />
- </>
- )}
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => setIsEditDialogOpen(false)}
- disabled={updateConfigMutation.isPending}
- >
- 取消
- </Button>
- <Button type="submit" disabled={updateConfigMutation.isPending}>
- {updateConfigMutation.isPending ? '更新中...' : '更新配置'}
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- </div>
- );
- };
|