容器管理-编辑可入库区
选中数据排序
选择文字排序
兼容修改后的其他调用方法
| | |
| | | // 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({ |
| | |
| | | // 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) { |
| | |
| | | // 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({ |
| | |
| | | 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(); |
| | |
| | | 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 |
| | |
| | | /> |
| | | </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" |
| | |
| | | /> |
| | | </ReferenceArrayInput> |
| | | </Stack> |
| | | {/* 下方显示已选库区和排序编辑 */} |
| | | <AreasSortInput source="areas" /> |
| | | {/* 隐藏字段:存储排序信息 */} |
| | | <TextInput source="areasSort" style={{ display: 'none' }} /> |
| | | |
| | | </Grid> |
| | | <Grid item xs={12} md={4}> |
| | |
| | | <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> |
| | | |
| | |
| | | |
| | | 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); |
| New file |
| | |
| | | /** |
| | | * |
| | | * @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; |
| | |
| | | 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) => { |
| | |
| | | } = 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(); |
| | |
| | | } |
| | | }; |
| | | |
| | | // 确保当前值在选项中,如果不在且正在加载,添加占位选项 |
| | | 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} |
| | | /> |
| New file |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | .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())); |
| | |
| | | .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())); |
| | |
| | | @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); |
| | | } |
| | | |
| | |
| | | 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("该类型已被初始化"); |
| | |
| | | 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"); |
| | | } |
| | |
| | | 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; |
| | |
| | | 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) |
| | |
| | | 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: 否 |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取排序后的库区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); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | 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; |
| New file |
| | |
| | | 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<>(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |