|
@@ -253,6 +253,8 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
|
const [selectedConfig, setSelectedConfig] = useState<FeieConfig | null>(null);
|
|
const [selectedConfig, setSelectedConfig] = useState<FeieConfig | null>(null);
|
|
|
const [configMap, setConfigMap] = useState<Record<string, FeieConfig>>({});
|
|
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 queryClient = useQueryClient();
|
|
|
const feieClient = createFeiePrinterClient(baseURL);
|
|
const feieClient = createFeiePrinterClient(baseURL);
|
|
@@ -290,6 +292,11 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
}
|
|
}
|
|
|
}, [configList]);
|
|
}, [configList]);
|
|
|
|
|
|
|
|
|
|
+ // 当切换分组时,清空分组编辑记录
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ setGroupEdits({});
|
|
|
|
|
+ }, [activeGroup]);
|
|
|
|
|
+
|
|
|
// 更新配置表单
|
|
// 更新配置表单
|
|
|
const updateForm = useForm<UpdateConfigFormValues>({
|
|
const updateForm = useForm<UpdateConfigFormValues>({
|
|
|
resolver: zodResolver(updateConfigSchema),
|
|
resolver: zodResolver(updateConfigSchema),
|
|
@@ -363,11 +370,56 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
setIsEditDialogOpen(true);
|
|
setIsEditDialogOpen(true);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 处理批量保存(现在主要用于强制刷新)
|
|
|
|
|
- const handleBatchSave = () => {
|
|
|
|
|
- // 由于现在有即时保存,批量保存主要用于刷新数据
|
|
|
|
|
- toast.info('配置已实时保存,正在刷新数据...');
|
|
|
|
|
- refetch();
|
|
|
|
|
|
|
+ // 批量保存配置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);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 处理重置配置
|
|
// 处理重置配置
|
|
@@ -402,7 +454,125 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
return definition?.defaultValue || '';
|
|
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 renderConfigItem = (definition: ConfigItemDefinition) => {
|
|
|
const config = configMap[definition.key];
|
|
const config = configMap[definition.key];
|
|
|
const value = getConfigValue(definition.key);
|
|
const value = getConfigValue(definition.key);
|
|
@@ -581,14 +751,6 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
|
刷新
|
|
刷新
|
|
|
</Button>
|
|
</Button>
|
|
|
- <Button
|
|
|
|
|
- onClick={handleBatchSave}
|
|
|
|
|
- disabled={isLoading}
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- >
|
|
|
|
|
- <RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
|
|
|
- 刷新数据
|
|
|
|
|
- </Button>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -674,15 +836,47 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
{activeGroup === ConfigGroup.PRINT_POLICY && (
|
|
{activeGroup === ConfigGroup.PRINT_POLICY && (
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
- <CardTitle>打印策略</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- 配置打印任务的触发条件、重试策略和超时设置
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
|
|
+ <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>
|
|
</CardHeader>
|
|
|
<CardContent className="space-y-6">
|
|
<CardContent className="space-y-6">
|
|
|
{configDefinitions
|
|
{configDefinitions
|
|
|
.filter(def => def.group === ConfigGroup.PRINT_POLICY)
|
|
.filter(def => def.group === ConfigGroup.PRINT_POLICY)
|
|
|
- .map(renderConfigItem)}
|
|
|
|
|
|
|
+ .map(renderBatchConfigItem)}
|
|
|
|
|
|
|
|
{/* 策略说明 */}
|
|
{/* 策略说明 */}
|
|
|
<div className="rounded-lg border bg-muted/50 p-4">
|
|
<div className="rounded-lg border bg-muted/50 p-4">
|
|
@@ -716,15 +910,47 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
|
|
|
{activeGroup === ConfigGroup.TEMPLATE && (
|
|
{activeGroup === ConfigGroup.TEMPLATE && (
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
- <CardTitle>模板配置</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- 配置小票和发货单的打印模板,支持变量替换
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
|
|
+ <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>
|
|
</CardHeader>
|
|
|
<CardContent className="space-y-6">
|
|
<CardContent className="space-y-6">
|
|
|
{configDefinitions
|
|
{configDefinitions
|
|
|
.filter(def => def.group === ConfigGroup.TEMPLATE)
|
|
.filter(def => def.group === ConfigGroup.TEMPLATE)
|
|
|
- .map(renderConfigItem)}
|
|
|
|
|
|
|
+ .map(renderBatchConfigItem)}
|
|
|
|
|
|
|
|
{/* 模板变量说明 */}
|
|
{/* 模板变量说明 */}
|
|
|
<div className="rounded-lg border bg-muted/50 p-4">
|
|
<div className="rounded-lg border bg-muted/50 p-4">
|