容器管理-编辑可入库区
选中数据排序
选择文字排序
兼容修改后的其他调用方法
11个文件已修改
5个文件已添加
997 ■■■■■ 已修改文件
rsf-admin/src/config/MyDataProvider.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/basContainer/BasContainerEdit.jsx 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/basContainer/BasContainerPanel.jsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/basStation/CrossZoneAreaField.jsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/AreasSortInput.jsx 279 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/DictionarySelect.jsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/SortableAreasInput.jsx 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/AgvServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/BasContainer.java 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/AreasDeserializer.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/AreasSerializer.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/AreasTypeHandler.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/config/MyDataProvider.js
@@ -38,7 +38,15 @@
  // get a list of records based on an array of ids
  getMany: async (resource, params) => {
    // console.log("getMany", resource, params);
    const res = await request.post(resource + "/many/" + params.ids);
    // Old format: [1, 2, 3] (array of integers)
    // New format: [{id: 1, sort: 1}, {id: 2, sort: 2}] (array of objects)
    const ids = params.ids.map(id => {
      if (typeof id === 'object' && id !== null && 'id' in id) {
        return id.id;
      }
      return id;
    });
    const res = await request.post(resource + "/many/" + ids.join(','));
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -90,9 +98,18 @@
  // update a list of records based on an array of ids and a common patch
  updateMany: async (resource, params) => {
    console.log("updateMany", resource, params);
    // Extract IDs from params.ids - handle both formats:
    // Old format: [1, 2, 3] (array of integers)
    // New format: [{id: 1, sort: 1}, {id: 2, sort: 2}] (array of objects)
    const ids = params.ids.map(id => {
      if (typeof id === 'object' && id !== null && 'id' in id) {
        return id.id;
      }
      return id; // Already a number
    });
    const res = await request.post(
      resource + "/update/many",
      params.ids.map((id) => ({ id, ...params.data })),
      ids.map((id) => ({ id, ...params.data })),
    );
    const { code, msg, data } = res.data;
    if (code === 200) {
@@ -121,7 +138,16 @@
  // delete a list of records based on an array of ids
  deleteMany: async (resource, params) => {
    console.log("deleteMany", resource, params);
    const res = await request.post(resource + "/remove/" + params?.ids);
    // Extract IDs from params.ids - handle both formats:
    // Old format: [1, 2, 3] (array of integers)
    // New format: [{id: 1, sort: 1}, {id: 2, sort: 2}] (array of objects)
    const ids = params?.ids ? params.ids.map(id => {
      if (typeof id === 'object' && id !== null && 'id' in id) {
        return id.id;
      }
      return id; // Already a number
    }) : [];
    const res = await request.post(resource + "/remove/" + ids.join(','));
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
rsf-admin/src/page/basicInfo/basContainer/BasContainerEdit.jsx
@@ -30,6 +30,7 @@
import MemoInput from "../../components/MemoInput";
import StatusSelectInput from "../../components/StatusSelectInput";
import DictionarySelect from "../../components/DictionarySelect";
import AreasSortInput from "../../components/AreasSortInput";
const FormToolbar = () => {
    const { getValues } = useFormContext();
@@ -51,6 +52,51 @@
            mutationMode={EDIT_MODE}
            actions={<CustomerTopToolBar />}
            aside={<EditBaseAside />}
            transform={(data) => {
                // 保存前转换:将 areas 从纯ID数组转换为 [{id, sort}] 格式
                // 从隐藏字段 areasSort 获取排序信息
                const areas = data.areas || [];
                const areasSort = data.areasSort || [];
                if (areas.length > 0) {
                    if (typeof areas[0] === 'number') {
                        // 如果是纯ID数组,使用 areasSort 中的排序信息
                        if (areasSort.length > 0 && typeof areasSort[0] === 'object') {
                            // 使用 areasSort 中的排序信息,但只保留 areas 中存在的ID
                            const areaIds = new Set(areas);
                            const sortedAreas = areasSort
                                .filter(item => areaIds.has(item.id))
                                .sort((a, b) => (a.sort || 0) - (b.sort || 0));
                            // 如果 areasSort 中有所有ID的排序信息,使用它
                            if (sortedAreas.length === areas.length) {
                                data.areas = sortedAreas;
                            } else {
                                // 否则,为缺失的ID添加默认排序
                                const existingIds = new Set(sortedAreas.map(item => item.id));
                                const missingIds = areas.filter(id => !existingIds.has(id));
                                const maxSort = sortedAreas.length > 0
                                    ? Math.max(...sortedAreas.map(item => item.sort || 1))
                                    : 0;
                                const newItems = missingIds.map((id, index) => ({
                                    id: id,
                                    sort: maxSort + index + 1,
                                }));
                                data.areas = [...sortedAreas, ...newItems];
                            }
                        } else {
                            // 如果没有排序信息,使用默认排序
                            data.areas = areas.map((id, index) => ({
                                id: id,
                                sort: index + 1,
                            }));
                        }
                    }
                    // 删除临时字段
                    delete data.areasSort;
                }
                return data;
            }}
        >
            <SimpleForm
                shouldUnregister
@@ -82,7 +128,26 @@
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <ReferenceArrayInput source="areas" reference="warehouseAreas">
                            <ReferenceArrayInput
                                source="areas"
                                reference="warehouseAreas"
                                sort={{ field: 'name', order: 'ASC' }}
                                format={(value) => {
                                    // 从后端接收时:将 [{id, sort}] 转换为 [id, id, ...]
                                    if (!value || !Array.isArray(value)) return [];
                                    if (value.length === 0) return [];
                                    // 如果是对象数组,提取id
                                    if (typeof value[0] === 'object' && value[0] !== null && value[0].id !== undefined) {
                                        return value.map(item => item.id);
                                    }
                                    // 如果已经是纯ID数组,直接返回
                                    return value;
                                }}
                                parse={(value) => {
                                    // 保存时:保持原值,由 AreasSortInput 处理转换
                                    return value;
                                }}
                            >
                                <SelectArrayInput
                                    label="table.field.basContainer.areas"
                                    optionText="name"
@@ -92,6 +157,10 @@
                                />
                            </ReferenceArrayInput>
                        </Stack>
                        {/* 下方显示已选库区和排序编辑 */}
                        <AreasSortInput source="areas" />
                        {/* 隐藏字段:存储排序信息 */}
                        <TextInput source="areasSort" style={{ display: 'none' }} />
                    </Grid>
                    <Grid item xs={12} md={4}>
rsf-admin/src/page/basicInfo/basContainer/BasContainerPanel.jsx
@@ -55,7 +55,16 @@
                        <Grid item xs={6}>
                            <PanelTypography
                                title="table.field.basContainer.areas" 
                                property={record.areas}
                                property={
                                    record.areas && Array.isArray(record.areas)
                                        ? record.areas.map(area => {
                                            if (typeof area === 'object' && area !== null && 'id' in area) {
                                                return area.id;
                                            }
                                            return area;
                                        }).join(', ')
                                        : record.areas
                                }
                            />
                        </Grid>
rsf-admin/src/page/basicInfo/basStation/CrossZoneAreaField.jsx
@@ -24,9 +24,43 @@
        setLoading(true);
        try {
            const res = await request.post(`/warehouseAreas/many/${record.areas.join(',')}`);
            // 提取排序信息和ID
            // Old format: [1, 2, 3] (array of integers)
            // New format: [{id: 1, sort: 1}, {id: 2, sort: 2}] (array of objects)
            const isObjectArray = record.areas.length > 0 &&
                typeof record.areas[0] === 'object' &&
                record.areas[0] !== null &&
                'id' in record.areas[0];
            let areaIds = [];
            let sortMap = new Map(); // 存储 id -> sort 的映射
            if (isObjectArray) {
                // 对象数组格式,提取ID和排序信息
                areaIds = record.areas.map(area => {
                    const id = area.id;
                    sortMap.set(id, area.sort || 0);
                    return id;
                });
            } else {
                // 纯ID数组格式
                areaIds = record.areas.map(id => Number(id));
            }
            const res = await request.post(`/warehouseAreas/many/${areaIds.join(',')}`);
            if (res?.data?.code === 200) {
                setAreaNames(res.data.data || []);
                let areas = res.data.data || [];
                // 如果有排序信息,按排序值排序
                if (sortMap.size > 0) {
                    areas = areas.sort((a, b) => {
                        const sortA = sortMap.get(a.id) || 0;
                        const sortB = sortMap.get(b.id) || 0;
                        return sortA - sortB;
                    });
                }
                setAreaNames(areas);
            }
        } catch (error) {
            console.error('获取区域名称失败:', error);
rsf-admin/src/page/components/AreasSortInput.jsx
New file
@@ -0,0 +1,279 @@
/**
 *
 * @author chen.lin
 * @time 2026-02-02
 * 库区排序编辑组件
 * 功能:显示已选中的库区,允许修改排序值
 * 数据格式:[{"id": 1, "sort": 1}, {"id": 2, "sort": 2}]
 *
 * 使用隐藏字段存储排序信息,保存时合并到 areas 字段
 */
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import {
    Box,
    TextField,
    Chip,
    IconButton,
    Paper,
    Typography,
    Stack,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import request from '@/utils/request';
const AreasSortInput = ({ source }) => {
    const { setValue, watch } = useFormContext();
    const sortSource = `${source}Sort`; // 隐藏字段名:areasSort
    const [areas, setAreas] = useState([]);
    const [selectedAreas, setSelectedAreas] = useState([]);
    const [loading, setLoading] = useState(false);
    const currentValue = watch(source) || [];
    const prevSelectedAreasRef = useRef([]);
    // 加载所有库区选项(用于获取名称)
    useEffect(() => {
        const loadAreas = async () => {
            setLoading(true);
            try {
                const res = await request.post('/warehouseAreas/list', {});
                if (res?.data?.code === 200) {
                    setAreas(res.data.data || []);
                } else {
                    console.error('加载库区失败:', res?.data?.msg);
                }
            } catch (error) {
                console.error('加载库区失败:', error);
            } finally {
                setLoading(false);
            }
        };
        loadAreas();
    }, []);
    // 初始化选中项:将后端数据格式转换为前端格式
    useEffect(() => {
        // currentValue 现在应该是纯ID数组 [1, 2, 3],因为 ReferenceArrayInput 的 format 已经处理了
        if (currentValue && currentValue.length > 0) {
            // 检查是否是纯ID数组
            const isIdArray = currentValue.every(item => typeof item === 'number' || typeof item === 'string');
            if (isIdArray) {
                // 纯ID数组格式 [1, 2, 3],需要与现有数据合并
                const currentIds = new Set(currentValue.map(id => Number(id)));
                // 保留已有的排序信息(如果存在)
                const existingAreas = prevSelectedAreasRef.current.filter(item =>
                    currentIds.has(Number(item.id))
                );
                const existingIds = new Set(existingAreas.map(item => Number(item.id)));
                // 找出新增的ID
                const newIds = currentValue
                    .map(id => Number(id))
                    .filter(id => !existingIds.has(id));
                // 为新增的ID创建排序项(默认排序为已有最大排序值+1)
                const maxSort = existingAreas.length > 0
                    ? Math.max(...existingAreas.map(item => item.sort || 1), 0)
                    : 0;
                const newItems = newIds.map((id, index) => ({
                    id: id,
                    sort: maxSort + index + 1,
                }));
                // 合并已有项和新项
                const converted = [...existingAreas, ...newItems];
                setSelectedAreas(converted);
                prevSelectedAreasRef.current = converted;
                // 保存排序信息到隐藏字段
                setValue(sortSource, converted, { shouldValidate: false });
            } else {
                // 如果已经是对象数组格式 [{id, sort}](从后端直接加载的情况)
                const sorted = [...currentValue].sort((a, b) => {
                    const sortA = a.sort || 0;
                    const sortB = b.sort || 0;
                    return sortA - sortB;
                });
                setSelectedAreas(sorted);
                prevSelectedAreasRef.current = sorted;
                // 保存排序信息到隐藏字段
                setValue(sortSource, sorted, { shouldValidate: false });
                // 同时更新 ReferenceArrayInput 的值为纯ID数组
                const ids = sorted.map(item => item.id);
                setValue(source, ids, { shouldValidate: false });
            }
        } else {
            setSelectedAreas([]);
            prevSelectedAreasRef.current = [];
            // 清空排序信息
            setValue(sortSource, [], { shouldValidate: false });
        }
    }, [currentValue, source, setValue]);
    // 处理删除库区
    const handleDeleteArea = (id) => {
        const filtered = selectedAreas.filter(item => item.id !== id);
        setSelectedAreas(filtered);
        prevSelectedAreasRef.current = filtered;
        // 更新表单值为纯ID数组(ReferenceArrayInput 需要)
        const ids = filtered.map(item => item.id);
        setValue(source, ids, { shouldValidate: true, shouldDirty: true, shouldTouch: true });
        // 更新排序信息
        setValue(sortSource, filtered, { shouldValidate: false, shouldDirty: true, shouldTouch: true });
    };
    // 处理修改排序值
    const handleSortChange = (id, newSort) => {
        const sortValue = parseInt(newSort) || 1;
        const updated = selectedAreas.map(item => {
            if (item.id === id) {
                return { ...item, sort: sortValue };
            }
            return item;
        });
        // 按排序值排序
        const sorted = [...updated].sort((a, b) => {
            const sortA = a.sort || 0;
            const sortB = b.sort || 0;
            return sortA - sortB;
        });
        setSelectedAreas(sorted);
        prevSelectedAreasRef.current = sorted;
        // 更新排序信息到隐藏字段,设置 shouldDirty: true 以触发表单的 dirty 状态
        setValue(sortSource, sorted, { shouldValidate: false, shouldDirty: true, shouldTouch: true });
    };
    // 获取库区名称
    const getAreaName = (id) => {
        if (!id) return '未知';
        const area = areas.find(a => a.id === id);
        return area ? area.name : `ID: ${id}`;
    };
    // 确保列表始终按排序值排序
    const sortedAreas = useMemo(() => {
        if (!selectedAreas || selectedAreas.length === 0) {
            return [];
        }
        return [...selectedAreas].sort((a, b) => {
            const sortA = a.sort || 0;
            const sortB = b.sort || 0;
            return sortA - sortB;
        });
    }, [selectedAreas]);
    // 如果没有选中的库区,不显示
    if (!sortedAreas || sortedAreas.length === 0) {
        return null;
    }
    return (
        <Box sx={{ mt: 2 }}>
            <Typography
                variant="body2"
                sx={{
                    mb: 1,
                    color: 'text.secondary',
                    width: '100%',
                }}
            >
                已选库区(可修改排序):
            </Typography>
            {/* 表头 */}
            <Paper
                elevation={0}
                sx={{
                    p: 1,
                    display: 'flex',
                    alignItems: 'center',
                    gap: 2,
                    width: '100%',
                    bgcolor: 'grey.100',
                    mb: 0.5,
                }}
            >
                <Box sx={{ flex: 1, minWidth: 0 }}>
                    <Typography variant="body2" sx={{ fontWeight: 'medium' }}>
                        库区
                    </Typography>
                </Box>
                <Box sx={{ width: 120, textAlign: 'center' }}>
                    <Typography variant="body2" sx={{ fontWeight: 'medium' }}>
                        排序
                    </Typography>
                </Box>
                <Box sx={{ width: 48 }}></Box> {/* 删除按钮占位 */}
            </Paper>
            <Stack spacing={1}>
                {sortedAreas.map((item) => {
                    // 确保 item.id 存在且有效
                    if (!item || item.id === undefined || item.id === null) {
                        return null;
                    }
                    return (
                        <Paper
                            key={item.id}
                            elevation={1}
                            sx={{
                                p: 1.5,
                                display: 'flex',
                                alignItems: 'center',
                                gap: 2,
                                width: '100%',
                                height: '30px',
                                minHeight: 'auto',
                                maxWidth: '100%',
                                boxSizing: 'border-box',
                            }}
                        >
                            {/* 左边:库区名称 */}
                            <Box sx={{ flex: 1, minWidth: 0 }}>
                                <Chip
                                    label={getAreaName(item.id)}
                                    size="small"
                                    sx={{ maxWidth: '100%' }}
                                />
                            </Box>
                            {/* 右边:排序输入框(无 label) */}
                            <TextField
                                type="number"
                                value={item.sort || ''}
                                onChange={(e) => handleSortChange(item.id, e.target.value)}
                                size="small"
                                placeholder="排序"
                                sx={{
                                    width: 120,
                                    '& .MuiInputBase-root': {
                                        height: '30px',
                                    },
                                    '& .MuiInputBase-input': {
                                        height: '30px',
                                        padding: '8.5px 14px',
                                    }
                                }}
                                inputProps={{
                                    min: 1,
                                    step: 1
                                }}
                            />
                            {/* 删除按钮 */}
                            <IconButton
                                size="small"
                                onClick={() => handleDeleteArea(item.id)}
                                color="error"
                            >
                                <DeleteIcon fontSize="small" />
                            </IconButton>
                        </Paper>
                    );
                })}
            </Stack>
        </Box>
    );
};
export default AreasSortInput;
rsf-admin/src/page/components/DictionarySelect.jsx
@@ -1,10 +1,11 @@
import EditIcon from '@mui/icons-material/Edit';
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import {
    Button, useListContext, SelectInput,
    required, SelectArrayInput,
    useTranslate, useNotify
} from 'react-admin';
import { useFormContext } from 'react-hook-form';
import request from '@/utils/request';
const DictionarySelect = (props) => {
@@ -19,8 +20,12 @@
    } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const { watch } = useFormContext();
    const [list, setList] = useState([]);
    const [loading, setLoading] = useState(false);
    // 获取当前表单值
    const currentValue = watch(name);
    useEffect(() => {
        http();
@@ -54,12 +59,33 @@
        }
    };
    // 确保当前值在选项中,如果不在且正在加载,添加占位选项
    const choices = useMemo(() => {
        if (!list || list.length === 0) {
            // 如果列表为空但当前有值,添加占位选项以避免警告
            if (currentValue !== undefined && currentValue !== null && currentValue !== '') {
                return [{ id: currentValue, name: String(currentValue) }];
            }
            return [];
        }
        // 检查当前值是否在选项中
        const valueExists = list.some(item => String(item.id) === String(currentValue));
        // 如果当前值不在选项中,添加它(可能是加载延迟导致的)
        if (currentValue !== undefined && currentValue !== null && currentValue !== '' && !valueExists) {
            return [...list, { id: currentValue, name: String(currentValue) }];
        }
        return list;
    }, [list, currentValue]);
    const InputComponent = multiple ? SelectArrayInput : SelectInput;
    return (
        <InputComponent
            source={name}
            choices={list}
            choices={choices}
            isLoading={loading}
            {...parmas}
        />
rsf-admin/src/page/components/SortableAreasInput.jsx
New file
@@ -0,0 +1,208 @@
import React, { useState, useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import {
    Box,
    TextField,
    Autocomplete,
    Chip,
    IconButton,
    Paper,
    Typography,
    Stack,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import request from '@/utils/request';
/**
 * @author chen.lin
 * @time 2026-02-02
 * 可排序的库区选择组件
 * 数据格式:[{"id": 1, "sort": 1}, {"id": 2, "sort": 2}]
 *
 * 功能:
 * 1. 上方:多选框选择库区
 * 2. 下方:显示已选中的库区,每行显示库区名称和排序输入框
 * 3. 默认排序为1,可以修改排序值
 */
const SortableAreasInput = ({ source, label, validate, ...props }) => {
    const { setValue, watch } = useFormContext();
    const [areas, setAreas] = useState([]);
    const [selectedAreas, setSelectedAreas] = useState([]);
    const [loading, setLoading] = useState(false);
    const currentValue = watch(source) || [];
    // 加载所有库区选项
    useEffect(() => {
        const loadAreas = async () => {
            setLoading(true);
            try {
                const res = await request.post('/warehouseAreas/list', {});
                if (res?.data?.code === 200) {
                    setAreas(res.data.data || []);
                } else {
                    console.error('加载库区失败:', res?.data?.msg);
                }
            } catch (error) {
                console.error('加载库区失败:', error);
            } finally {
                setLoading(false);
            }
        };
        loadAreas();
    }, []);
    // 初始化选中项:将后端数据格式转换为前端格式
    useEffect(() => {
        if (currentValue && currentValue.length > 0) {
            // 如果已经是对象数组格式 [{id, sort}]
            if (typeof currentValue[0] === 'object' && currentValue[0].id !== undefined) {
                const sorted = [...currentValue].sort((a, b) => {
                    const sortA = a.sort || 0;
                    const sortB = b.sort || 0;
                    return sortA - sortB;
                });
                setSelectedAreas(sorted);
            } else {
                // 如果是旧格式 [1, 2, 3],转换为新格式,默认排序为1
                const converted = currentValue.map((id, index) => ({
                    id: id,
                    sort: index + 1,
                }));
                setSelectedAreas(converted);
                // 同步更新表单值
                setValue(source, converted, { shouldValidate: false });
            }
        } else {
            setSelectedAreas([]);
        }
    }, [currentValue, source, setValue]);
    // 处理添加库区
    const handleAddArea = (event, newValue) => {
        if (newValue && !selectedAreas.find(item => item.id === newValue.id)) {
            // 默认排序为1,如果已有数据,使用最大排序值+1
            const maxSort = selectedAreas.length > 0
                ? Math.max(...selectedAreas.map(item => item.sort || 1))
                : 0;
            const newItem = {
                id: newValue.id,
                sort: maxSort + 1,
            };
            const updated = [...selectedAreas, newItem];
            setSelectedAreas(updated);
            setValue(source, updated, { shouldValidate: true });
        }
    };
    // 处理删除库区
    const handleDeleteArea = (id) => {
        const filtered = selectedAreas.filter(item => item.id !== id);
        setSelectedAreas(filtered);
        setValue(source, filtered, { shouldValidate: true });
    };
    // 处理修改排序值
    const handleSortChange = (id, newSort) => {
        const sortValue = parseInt(newSort) || 1;
        const updated = selectedAreas.map(item => {
            if (item.id === id) {
                return { ...item, sort: sortValue };
            }
            return item;
        });
        setSelectedAreas(updated);
        setValue(source, updated, { shouldValidate: true });
    };
    // 获取库区名称
    const getAreaName = (id) => {
        const area = areas.find(a => a.id === id);
        return area ? area.name : `ID: ${id}`;
    };
    // 过滤已选中的选项
    const availableOptions = areas.filter(
        area => !selectedAreas.find(item => item.id === area.id)
    );
    return (
        <Box>
            {/* 多选框 */}
            <Autocomplete
                multiple
                options={availableOptions}
                getOptionLabel={(option) => option.name || ''}
                onChange={handleAddArea}
                loading={loading}
                value={[]} // 始终为空,因为已选中的在下方面板显示
                renderInput={(params) => (
                    <TextField
                        {...params}
                        label={label}
                        placeholder="选择库区..."
                        variant="outlined"
                        size="small"
                    />
                )}
                sx={{ mb: 2 }}
            />
            {/* 已选中的库区列表,显示键值对(库区名称:排序值) */}
            {selectedAreas.length > 0 && (
                <Box sx={{ mt: 2 }}>
                    <Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
                        已选库区(可修改排序):
                    </Typography>
                    <Stack spacing={1}>
                        {selectedAreas.map((item) => (
                            <Paper
                                key={item.id}
                                elevation={1}
                                sx={{
                                    p: 1.5,
                                    display: 'flex',
                                    alignItems: 'center',
                                    gap: 2,
                                }}
                            >
                                {/* 左边:库区名称 */}
                                <Box sx={{ flex: 1, minWidth: 0 }}>
                                    <Chip
                                        label={getAreaName(item.id)}
                                        size="small"
                                        sx={{ maxWidth: '100%' }}
                                    />
                                </Box>
                                {/* 右边:排序输入框 */}
                                <TextField
                                    type="number"
                                    label="排序"
                                    value={item.sort || ''}
                                    onChange={(e) => handleSortChange(item.id, e.target.value)}
                                    size="small"
                                    sx={{ width: 120 }}
                                    inputProps={{
                                        min: 1,
                                        step: 1
                                    }}
                                />
                                {/* 删除按钮 */}
                                <IconButton
                                    size="small"
                                    onClick={() => handleDeleteArea(item.id)}
                                    color="error"
                                >
                                    <DeleteIcon fontSize="small" />
                                </IconButton>
                            </Paper>
                        ))}
                    </Stack>
                </Box>
            )}
        </Box>
    );
};
export default SortableAreasInput;
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/AgvServiceImpl.java
@@ -345,7 +345,7 @@
            for (BasContainer container : containers) {
                String codeType = container.getCodeType();  // 获取正则表达式
                if (barcode.matches(codeType)) {  // 判断条码是否符合这个正则
                    List<Integer> areaList2 = container.getAreas();
                    List<Integer> areaList2 = container.getAreasIds();
                    if (!areaList2.contains(Integer.valueOf(area))) {
                        matches2 = false;
                        continue;
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java
@@ -141,7 +141,7 @@
            for (BasContainer container : containers) {
                String codeType = container.getCodeType();  // 获取正则表达式
                if (barcode.matches(codeType)) {  // 判断条码是否符合这个正则
                    List<Integer> areaList2 = container.getAreas();
                    List<Integer> areaList2 = container.getAreasIds();
                    if (!areaList2.contains(Integer.parseInt(area))) {
                        matches2 = false;
                        continue;
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java
@@ -1026,7 +1026,7 @@
                        .map(WarehouseRoleMenu::getMenuId)
                        .collect(Collectors.toSet());
                // 获取 areaList 并转换为 Long 类型的 Set
                List<Integer> areaList = container.getAreas();
                List<Integer> areaList = container.getAreasIds();
                Set<Long> areaSet = new HashSet<>();
                if (areaList != null) {
                    areaList.forEach(area -> areaSet.add(area.longValue()));
@@ -1066,7 +1066,7 @@
                        .map(WarehouseRoleMenu::getMenuId)
                        .collect(Collectors.toSet());
                // 获取 areaList 并转换为 Long 类型的 Set
                List<Integer> areaList = container.getAreas();
                List<Integer> areaList = container.getAreasIds();
                Set<Long> areaSet = new HashSet<>();
                if (areaList != null) {
                    areaList.forEach(area -> areaSet.add(area.longValue()));
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
@@ -51,6 +51,10 @@
    @GetMapping("/basContainer/{id}")
    public R get(@PathVariable("id") Long id) {
        BasContainer basContainer = basContainerService.getById(id);
        // 确保返回的areas按sort字段排序
        if (basContainer != null) {
            basContainer.sortAreas();
        }
        return R.ok().add(basContainer);
    }
@@ -62,6 +66,10 @@
        basContainer.setCreateTime(new Date());
        basContainer.setUpdateBy(getLoginUserId());
        basContainer.setUpdateTime(new Date());
        // 确保areas按sort字段排序
        basContainer.sortAreas();
        BasContainer container = basContainerService.getOne(new LambdaQueryWrapper<BasContainer>().eq(BasContainer::getContainerType, basContainer.getContainerType()));
        if (null != container) {
            return R.error("该类型已被初始化");
@@ -78,6 +86,10 @@
    public R update(@RequestBody BasContainer basContainer) {
        basContainer.setUpdateBy(getLoginUserId());
        basContainer.setUpdateTime(new Date());
        // 确保areas按sort字段排序
        basContainer.sortAreas();
        if (!basContainerService.updateById(basContainer)) {
            return R.error("Update Fail");
        }
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/BasContainer.java
@@ -6,17 +6,16 @@
import java.util.Date;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vincent.rsf.server.manager.utils.AreasDeserializer;
import com.vincent.rsf.server.manager.utils.AreasSerializer;
import com.vincent.rsf.server.manager.utils.AreasTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.vincent.rsf.server.system.entity.DictData;
import com.vincent.rsf.server.system.service.DictDataService;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableLogic;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import com.vincent.rsf.framework.common.Cools;
@@ -24,8 +23,10 @@
import com.vincent.rsf.server.system.service.UserService;
import com.vincent.rsf.server.system.entity.User;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.stream.Collectors;
@Data
@Accessors(chain = true)
@@ -57,11 +58,14 @@
    private String codeType;
    /**
     * 可入库区
     * 可入库区(包含排序信息)
     * 格式: [{"id": 1, "sort": 1}, {"id": 2, "sort": 2}]
     */
    @ApiModelProperty(value = "可入库区")
    @TableField(typeHandler = JacksonTypeHandler.class)
    private List<Integer> areas;
    @ApiModelProperty(value = "可入库区(包含排序信息)")
    @TableField(typeHandler = AreasTypeHandler.class)
    @JsonDeserialize(using = AreasDeserializer.class)
    @JsonSerialize(using = AreasSerializer.class)
    private List<Map<String, Object>> areas;
    /**
     * 是否删除 1: 是 0: 否
@@ -118,7 +122,7 @@
    public BasContainer() {
    }
    public BasContainer(Long containerType, String codeType, List<Integer> areas, Integer deleted, Integer status,
    public BasContainer(Long containerType, String codeType, List<Map<String, Object>> areas, Integer deleted, Integer status,
            Integer tenantId, Long createBy, Date createTime, Long updateBy, Date updateTime, String memo) {
        this.containerType = containerType;
        this.codeType = codeType;
@@ -207,4 +211,42 @@
        }
    }
    /**
     * 获取排序后的库区ID列表(向后兼容方法)
     * @return 排序后的库区ID列表
     */
    public List<Integer> getAreasIds() {
        if (Cools.isEmpty(this.areas)) {
            return new ArrayList<>();
        }
        return this.areas.stream()
                .sorted((a, b) -> {
                    Integer sortA = a.get("sort") != null ? ((Number) a.get("sort")).intValue() : Integer.MAX_VALUE;
                    Integer sortB = b.get("sort") != null ? ((Number) b.get("sort")).intValue() : Integer.MAX_VALUE;
                    return sortA.compareTo(sortB);
                })
                .map(area -> {
                    Object id = area.get("id");
                    if (id instanceof Number) {
                        return ((Number) id).intValue();
                    }
                    return null;
                })
                .filter(id -> id != null)
                .collect(Collectors.toList());
    }
    /**
     * 对areas按sort字段进行排序
     */
    public void sortAreas() {
        if (this.areas != null && !this.areas.isEmpty()) {
            this.areas.sort((a, b) -> {
                Integer sortA = a.get("sort") != null ? ((Number) a.get("sort")).intValue() : Integer.MAX_VALUE;
                Integer sortB = b.get("sort") != null ? ((Number) b.get("sort")).intValue() : Integer.MAX_VALUE;
                return sortA.compareTo(sortB);
            });
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -509,7 +509,7 @@
            for (BasContainer container : containers) {
                String codeType = container.getCodeType();  // 获取正则表达式
                if (barcode.matches(codeType)) {  // 判断条码是否符合这个正则
                    List<Integer> areaList2 = container.getAreas();
                    List<Integer> areaList2 = container.getAreasIds();
                    if (!areaList2.contains(Integer.parseInt(area))) {
                        matches2 = false;
                        continue;
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/AreasDeserializer.java
New file
@@ -0,0 +1,68 @@
package com.vincent.rsf.server.manager.utils;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 *
 * @author chen.lin
 * @time 2026-02-02
 * Areas 字段自定义反序列化器
 * 支持两种格式:
 * 1. [1, 2, 3] - 纯ID数组(向后兼容)
 * 2. [{"id": 1, "sort": 1}, {"id": 2, "sort": 2}] - 对象数组(新格式)
 *
 *
 */
public class AreasDeserializer extends JsonDeserializer<List<Map<String, Object>>> {
    @Override
    public List<Map<String, Object>> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonToken currentToken = p.getCurrentToken();
        // 处理 null 值
        if (currentToken == JsonToken.VALUE_NULL) {
            return new ArrayList<>();
        }
        // 处理数组
        if (currentToken == JsonToken.START_ARRAY) {
            List<Map<String, Object>> result = new ArrayList<>();
            JsonToken token = p.nextToken();
            while (token != null && token != JsonToken.END_ARRAY) {
                if (token == JsonToken.VALUE_NUMBER_INT) {
                    // 处理纯ID数组格式 [1, 2, 3]
                    int id = p.getIntValue();
                    Map<String, Object> area = new HashMap<>();
                    area.put("id", id);
                    area.put("sort", result.size() + 1); // 默认排序
                    result.add(area);
                    token = p.nextToken();
                } else if (token == JsonToken.START_OBJECT) {
                    // 处理对象数组格式 [{"id": 1, "sort": 1}]
                    ObjectMapper mapper = (ObjectMapper) p.getCodec();
                    @SuppressWarnings("unchecked")
                    Map<String, Object> area = mapper.readValue(p, Map.class);
                    result.add(area);
                    token = p.nextToken();
                } else {
                    token = p.nextToken();
                }
            }
            return result;
        }
        // 如果既不是数组也不是null,返回空列表
        return new ArrayList<>();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/AreasSerializer.java
New file
@@ -0,0 +1,57 @@
package com.vincent.rsf.server.manager.utils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @author chen.lin
 * @time 2026-02-02
 * Areas 字段自定义序列化器
 * 将 List<Map<String, Object>> 序列化为 JSON 数组
 * 支持混合类型(Integer 和 Map)的向后兼容
 *
 */
public class AreasSerializer extends JsonSerializer<Object> {
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        if (!(value instanceof List)) {
            gen.writeObject(value);
            return;
        }
        @SuppressWarnings("unchecked")
        List<Object> list = (List<Object>) value;
        gen.writeStartArray();
        for (Object item : list) {
            if (item instanceof Map) {
                // 已经是 Map 格式
                @SuppressWarnings("unchecked")
                Map<String, Object> map = (Map<String, Object>) item;
                gen.writeObject(map);
            } else if (item instanceof Number) {
                // 如果是 Number(向后兼容),转换为 Map 格式
                Map<String, Object> areaMap = new HashMap<>();
                areaMap.put("id", ((Number) item).intValue());
                areaMap.put("sort", 1); // 默认排序
                gen.writeObject(areaMap);
            } else {
                // 其他类型,尝试直接写入
                gen.writeObject(item);
            }
        }
        gen.writeEndArray();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/AreasTypeHandler.java
New file
@@ -0,0 +1,115 @@
package com.vincent.rsf.server.manager.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @author chen.lin
 * @time 2026-02-02
 * Areas 字段自定义 TypeHandler
 * 处理数据库和 Java 对象之间的转换
 * 支持两种格式:
 * 1. [1, 2, 3] - 纯ID数组(向后兼容)
 * 2. [{"id": 1, "sort": 1}, {"id": 2, "sort": 2}] - 对象数组(新格式)
 *
 */
@MappedTypes({List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class AreasTypeHandler extends BaseTypeHandler<List<Map<String, Object>>> {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final TypeReference<List<Object>> LIST_TYPE_REF = new TypeReference<List<Object>>() {};
    /**
     * 解析 JSON 字符串为 List<Map<String, Object>>
     */
    private List<Map<String, Object>> parse(String json) {
        if (json == null || json.trim().isEmpty()) {
            return new ArrayList<>();
        }
        try {
            // 先解析为 List<Object>
            List<Object> rawList = OBJECT_MAPPER.readValue(json, LIST_TYPE_REF);
            if (rawList == null || rawList.isEmpty()) {
                return new ArrayList<>();
            }
            List<Map<String, Object>> result = new ArrayList<>();
            // 遍历所有元素并转换
            for (int i = 0; i < rawList.size(); i++) {
                Object item = rawList.get(i);
                if (item instanceof Map) {
                    // 已经是对象数组格式 [{"id": 1, "sort": 1}]
                    @SuppressWarnings("unchecked")
                    Map<String, Object> map = (Map<String, Object>) item;
                    result.add(map);
                } else if (item instanceof Number) {
                    // 纯ID数组格式 [1, 2, 3],转换为对象数组
                    Map<String, Object> area = new HashMap<>();
                    area.put("id", ((Number) item).intValue());
                    area.put("sort", i + 1);
                    result.add(area);
                }
                // 忽略其他类型
            }
            return result;
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse areas JSON: " + json, e);
        }
    }
    /**
     * 将 List<Map<String, Object>> 转换为 JSON 字符串
     */
    private String toJson(List<Map<String, Object>> obj) {
        if (obj == null) {
            return null;
        }
        try {
            return OBJECT_MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize areas to JSON", e);
        }
    }
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, List<Map<String, Object>> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, toJson(parameter));
    }
    @Override
    public List<Map<String, Object>> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        return parse(json);
    }
    @Override
    public List<Map<String, Object>> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        return parse(json);
    }
    @Override
    public List<Map<String, Object>> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        return parse(json);
    }
}