소스 검색

✨ feat(PrintConfigManagement): 新增批量保存功能并重构配置项渲染逻辑

- 新增表格编辑状态管理(tableEdits)和分组编辑状态管理(groupEdits)
- 新增批量保存配置的 mutation 逻辑,支持分组和表格两种批量保存模式
- 重构配置项渲染逻辑,分离即时保存模式(renderConfigItem)和批量保存模式(renderBatchConfigItem)
- 为打印策略和模板配置分组添加批量保存按钮、取消编辑按钮及编辑状态指示器
- 移除原有的 handleBatchSave 函数,替换为 handleTableBatchSave 和 handleGroupBatchSave
- 优化用户体验,在切换分组时自动清空分组编辑记录
yourname 1 개월 전
부모
커밋
8a25c3b418
1개의 변경된 파일250개의 추가작업 그리고 24개의 파일을 삭제
  1. 250 24
      packages/feie-printer-management-ui-mt/src/components/PrintConfigManagement.tsx

+ 250 - 24
packages/feie-printer-management-ui-mt/src/components/PrintConfigManagement.tsx

@@ -253,6 +253,8 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
   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);
@@ -290,6 +292,11 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
     }
   }, [configList]);
 
+  // 当切换分组时,清空分组编辑记录
+  useEffect(() => {
+    setGroupEdits({});
+  }, [activeGroup]);
+
   // 更新配置表单
   const updateForm = useForm<UpdateConfigFormValues>({
     resolver: zodResolver(updateConfigSchema),
@@ -363,11 +370,56 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
     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 || '';
   };
 
-  // 渲染配置项组件
+  // 渲染配置项组件(批量保存模式)
+  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);
@@ -581,14 +751,6 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
             <RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
             刷新
           </Button>
-          <Button
-            onClick={handleBatchSave}
-            disabled={isLoading}
-            variant="outline"
-          >
-            <RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
-            刷新数据
-          </Button>
         </div>
       </div>
 
@@ -674,15 +836,47 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
           {activeGroup === ConfigGroup.PRINT_POLICY && (
             <Card>
               <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>
               <CardContent className="space-y-6">
                 {configDefinitions
                   .filter(def => def.group === ConfigGroup.PRINT_POLICY)
-                  .map(renderConfigItem)}
+                  .map(renderBatchConfigItem)}
 
                 {/* 策略说明 */}
                 <div className="rounded-lg border bg-muted/50 p-4">
@@ -716,15 +910,47 @@ export const PrintConfigManagement: React.FC<PrintConfigManagementProps> = ({
           {activeGroup === ConfigGroup.TEMPLATE && (
             <Card>
               <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>
               <CardContent className="space-y-6">
                 {configDefinitions
                   .filter(def => def.group === ConfigGroup.TEMPLATE)
-                  .map(renderConfigItem)}
+                  .map(renderBatchConfigItem)}
 
                 {/* 模板变量说明 */}
                 <div className="rounded-lg border bg-muted/50 p-4">