chen.lin
2 天以前 b3a8cec76cd3d2d3aa6d470e1c28ec161bc1a16b
路径管理-初始化功能优化
1个文件已添加
10个文件已修改
679 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/TabsBar.jsx 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/deviceSite/DeviceSiteEdit.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/deviceSite/DeviceSiteList.jsx 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/deviceSite/InitModal.jsx 324 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasStationController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/DeviceSiteController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/DeviceSiteParame.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/DeviceSiteRowParam.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/DeviceSiteServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -1481,6 +1481,7 @@
        locInit: 'loc init',
        init: "Init",
        siteInit: 'site init',
        pathInit: 'Path init',
        batch: 'batch',
        pick: 'Pick',
        check: 'Check',
rsf-admin/src/i18n/zh.js
@@ -1545,6 +1545,7 @@
        unenable: '禁用',
        locInit: '库位初始化',
        siteInit: '站点初始化',
        pathInit: '路径初始化',
        continue: '继续收货',
        batch: '批量操作',
        confirm: '确认',
rsf-admin/src/layout/TabsBar.jsx
@@ -79,6 +79,8 @@
    return normalizePath(path1) === normalizePath(path2);
};
const LONG_PRESS_MS = 300;
const TabsBar = () => {
    const location = useLocation();
    const navigate = useNavigate();
@@ -91,6 +93,12 @@
    const [contextMenuTab, setContextMenuTab] = useState(null);
    const contextMenuOpenRef = useRef(false);
    contextMenuOpenRef.current = contextMenu !== null;
    const [draggingIndex, setDraggingIndex] = useState(null);
    const [dropIndicatorIndex, setDropIndicatorIndex] = useState(null);
    const longPressTimerRef = useRef(null);
    const longPressIndexRef = useRef(null);
    const justFinishedDragRef = useRef(false);
    // 在标签页右键,阻止浏览器默认菜单
    useEffect(() => {
@@ -226,6 +234,10 @@
    // 切换标签页
    const handleTabChange = (event, newValue) => {
        if (justFinishedDragRef.current) {
            justFinishedDragRef.current = false;
            return;
        }
        const targetTab = tabs[newValue];
        if (targetTab && targetTab.path !== location.pathname) {
            navigate(targetTab.path);
@@ -415,6 +427,91 @@
        return () => document.removeEventListener('mousedown', onDocClick, true);
    }, [contextMenu]);
    const clearLongPressTimer = useCallback(() => {
        if (longPressTimerRef.current) {
            clearTimeout(longPressTimerRef.current);
            longPressTimerRef.current = null;
        }
        longPressIndexRef.current = null;
    }, []);
    const handleTabPointerDown = useCallback((e, index) => {
        if (index < 0) return;
        longPressIndexRef.current = index;
        longPressTimerRef.current = setTimeout(() => {
            longPressTimerRef.current = null;
            setDraggingIndex(index);
            setDropIndicatorIndex(index);
        }, LONG_PRESS_MS);
    }, []);
    const handleTabPointerUp = useCallback(() => {
        clearLongPressTimer();
    }, [clearLongPressTimer]);
    useEffect(() => {
        if (draggingIndex === null) return;
        const getDropIndex = (clientX) => {
            const nodes = tabsBarRef.current?.querySelectorAll('[data-tab-index]');
            if (!nodes?.length) return draggingIndex;
            for (let i = 0; i < nodes.length; i++) {
                const rect = nodes[i].getBoundingClientRect();
                const mid = rect.left + rect.width / 2;
                if (clientX <= mid) {
                    const drop = i;
                    if (draggingIndex === 0) return drop <= 0 ? 0 : draggingIndex;
                    return drop <= 0 ? 1 : drop;
                }
            }
            const drop = nodes.length;
            return draggingIndex === 0 ? 0 : drop;
        };
        const clientXFromEvent = (e) => e.clientX ?? e.touches?.[0]?.clientX;
        const onMove = (e) => {
            const x = clientXFromEvent(e);
            if (x != null) setDropIndicatorIndex(getDropIndex(x));
        };
        const onTouchMove = (e) => {
            e.preventDefault();
            onMove(e);
        };
        const onUp = () => {
            justFinishedDragRef.current = true;
            setDraggingIndex((di) => {
                setDropIndicatorIndex((dropIdx) => {
                    if (di !== null && dropIdx !== null && di !== dropIdx) {
                        const newTabs = [...tabs];
                        const [item] = newTabs.splice(di, 1);
                        const insertAt = dropIdx > di ? dropIdx - 1 : dropIdx;
                        newTabs.splice(insertAt, 0, item);
                        const dashboard = newTabs.find((t) => t.path === '/dashboard');
                        if (dashboard && newTabs[0].path !== '/dashboard') {
                            const idx = newTabs.indexOf(dashboard);
                            newTabs.splice(idx, 1);
                            newTabs.unshift(dashboard);
                        }
                        saveTabs(newTabs);
                        setTabs(newTabs);
                    }
                    return null;
                });
                return null;
            });
        };
        document.addEventListener('mousemove', onMove, true);
        document.addEventListener('mouseup', onUp, true);
        document.addEventListener('touchmove', onTouchMove, { passive: false, capture: true });
        document.addEventListener('touchend', onUp, true);
        document.addEventListener('touchcancel', onUp, true);
        return () => {
            document.removeEventListener('mousemove', onMove, true);
            document.removeEventListener('mouseup', onUp, true);
            document.removeEventListener('touchmove', onTouchMove, true);
            document.removeEventListener('touchend', onUp, true);
            document.removeEventListener('touchcancel', onUp, true);
        };
    }, [draggingIndex, tabs]);
    return (
        <Box
            ref={tabsBarRef}
@@ -449,12 +546,20 @@
                        key={tab.path}
                        label={
                            <Box
                                data-tab-index={index}
                                onContextMenu={(e) => handleContextMenu(e, tab)}
                                onMouseDown={(e) => handleTabPointerDown(e, index)}
                                onMouseUp={handleTabPointerUp}
                                onMouseLeave={handleTabPointerUp}
                                onTouchStart={(e) => handleTabPointerDown(e, index)}
                                onTouchEnd={handleTabPointerUp}
                                onTouchCancel={handleTabPointerUp}
                                sx={{
                                    display: 'flex',
                                    alignItems: 'center',
                                    gap: 0.5,
                                    width: '100%',
                                    ...(draggingIndex === index && { opacity: 0.7 }),
                                }}
                            >
                                {tab.path === '/dashboard' && (
rsf-admin/src/page/basicInfo/deviceSite/DeviceSiteEdit.jsx
@@ -131,6 +131,19 @@
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <TextInput
                                label="table.field.deviceSite.target"
                                source="target"
                                parse={v => v}
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                                label="table.field.deviceSite.channel"
                                source="channel"
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <WarehouseSelect
                                label={translate("table.field.deviceSite.areaIdStart")}
                                name="areaIdStart"
rsf-admin/src/page/basicInfo/deviceSite/DeviceSiteList.jsx
@@ -33,7 +33,7 @@
    DeleteButton,
    Button
} from 'react-admin';
import { Box, Typography, Card, Stack } from '@mui/material';
import { Box, Typography, Card, Stack, Button as MuiButton } from '@mui/material';
import { styled } from '@mui/material/styles';
import DeviceSiteCreate from "./DeviceSiteCreate";
import DeviceSitePanel from "./DeviceSitePanel";
@@ -46,6 +46,7 @@
import * as Common from '@/utils/common';
import InitModal from "./InitModal";
import CabinIcon from '@mui/icons-material/Cabin';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -57,7 +58,10 @@
    '& .column-name': {
    },
    '& .opt': {
        width: 200
        width: 260,
        minWidth: 260,
        overflow: 'visible',
        '& > *': { flexShrink: 0 }
    },
}));
@@ -87,12 +91,57 @@
    />,
]
const CopyButton = ({ setInitCopyData, setInitDialogOpen }) => {
    const record = useRecordContext();
    const translate = useTranslate();
    if (!record) return null;
    const label = translate('toolbar.copy') || '复制';
    return (
        <MuiButton
            size="small"
            color="primary"
            sx={{
                padding: '2px 8px',
                fontSize: '.75rem',
                minWidth: 'auto',
                flexShrink: 0,
                '& .MuiButton-startIcon': { marginRight: 0.5 },
            }}
            startIcon={<ContentCopyIcon fontSize="small" />}
            onClick={(e) => {
                e.stopPropagation();
                setInitCopyData({
                    rows: [{
                        deviceSiteName: record.deviceSite ?? '',
                        siteName: record.site ?? '',
                        target: record.target ?? '',
                    }],
                    channel: record.channel != null ? String(record.channel) : '',
                    type: record.type,
                    typeIds: record.type != null ? [record.type] : [],
                    deviceType: record.device ?? '',
                    deviceCode: record.deviceCode ?? '',
                    areaIdStart: record.areaIdStart,
                    areaIdEnd: record.areaIdEnd,
                    name: record.name ?? '',
                    wcsCode: record.target ?? '',
                    label: record.label ?? '',
                });
                setInitDialogOpen(true);
            }}
        >
            <span style={{ whiteSpace: 'nowrap' }}>{label}</span>
        </MuiButton>
    );
};
const DeviceSiteList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const [initDialog, setInitDialog] = useState(false);
    const [initCopyData, setInitCopyData] = useState(null);
    return (
        <Box display="flex">
@@ -128,7 +177,7 @@
                                    mt: 2
                                }}
                                onClick={() => { setInitDialog(true) }}>
                                {translate('toolbar.siteInit')}
                                {translate('toolbar.pathInit')}
                            </Button>
                        </Box>
                    }
@@ -149,7 +198,7 @@
                    preferenceKey='deviceSite'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => false}
                    omit={['id', 'createTime', 'createBy', 'memo', 'label','name','target','statusBool','updateBy']}
                    omit={['id', 'createTime', 'createBy', 'memo', 'statusBool', 'updateBy']}
                >
                    <NumberField source="id" />
                    <TextField source="site" label="table.field.deviceSite.site" />
@@ -171,6 +220,7 @@
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <CopyButton setInitCopyData={setInitCopyData} setInitDialogOpen={setInitDialog} />
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
@@ -190,6 +240,8 @@
            <InitModal
                open={initDialog}
                setOpen={setInitDialog}
                initialData={initCopyData}
                onClose={() => setInitCopyData(null)}
            />
        </Box>
    )
@@ -205,7 +257,7 @@
    return (
        <>
            <Button onClick={() => setCreateDialog(true)} label={"toolbar.siteInit"}>
            <Button onClick={() => setCreateDialog(true)} label={"toolbar.pathInit"}>
                <CabinIcon />
            </Button>
            <InitModal
rsf-admin/src/page/basicInfo/deviceSite/InitModal.jsx
@@ -1,25 +1,11 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import React, { useState, useEffect } from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    BooleanInput,
    DateInput,
    SaveButton,
    SelectInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    Toolbar,
    required,
    useDataProvider,
    useNotify,
    Form,
    useCreateController,
    useListContext,
    useNotify,
    useRefresh,
    SelectArrayInput
    TextInput,
    SelectInput,
} from 'react-admin';
import {
    Dialog,
@@ -27,7 +13,6 @@
    DialogContent,
    DialogTitle,
    Grid,
    TextField,
    Box,
    Button,
    Paper,
@@ -37,71 +22,244 @@
    TableBody,
    TableRow,
    TableCell,
    Tooltip,
    IconButton,
    styled
    MenuItem,
    Select,
    FormControl,
    TextField,
} from '@mui/material';
import DialogCloseButton from "../../components/DialogCloseButton";
import DictionarySelect from "../../components/DictionarySelect";
import { useForm, Controller, useWatch, FormProvider, useFormContext } from "react-hook-form";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { Add, Edit, Delete } from '@mui/icons-material';
import _ from 'lodash';
import { DataGrid } from '@mui/x-data-grid';
import { Add, Delete } from '@mui/icons-material';
import { ReferenceInput, AutocompleteInput } from 'react-admin';
const defaultRow = () => ({ deviceSite: '', site: '', target: '' });
const InitModal = ({ open, setOpen }) => {
const InitModal = ({ open, setOpen, initialData = null, onClose }) => {
    const refresh = useRefresh();
    const translate = useTranslate();
    const notify = useNotify();
    const [disabled, setDisabled] = useState(false)
    const [disabled, setDisabled] = useState(false);
    const [rows, setRows] = useState([defaultRow()]);
    const [stationOptions, setStationOptions] = useState([]);
    useEffect(() => {
        if (!open) return;
        request.post('/basStation/list', {})
            .then((res) => {
                if (res?.data?.code === 200 && res?.data?.data) {
                    const list = Array.isArray(res.data.data) ? res.data.data : (res.data.data?.records || []);
                    const opts = list.map((item) => ({ id: item.id, stationName: item.stationName ?? item.name ?? item.id }));
                    setStationOptions(opts);
                    if (initialData?.rows?.length && opts.length) {
                        const resolved = initialData.rows.map((r) => {
                            const deviceSiteId = r.deviceSite || (r.deviceSiteName && opts.find((o) => o.stationName === r.deviceSiteName)?.id);
                            const siteId = r.site || (r.siteName && opts.find((o) => o.stationName === r.siteName)?.id);
                            return {
                                deviceSite: deviceSiteId != null ? String(deviceSiteId) : '',
                                site: siteId != null ? String(siteId) : '',
                                target: r.target ?? '',
                            };
                        });
                        setRows(resolved.length ? resolved : [defaultRow()]);
                    }
                }
            })
            .catch(() => {});
    }, [open, initialData]);
    useEffect(() => {
        if (open && !initialData?.rows?.length) {
            setRows([defaultRow()]);
        }
    }, [open, initialData]);
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
            if (typeof onClose === 'function') onClose();
        }
    };
    const handleReset = (e) => {
        e.preventDefault();
    const addRow = () => setRows((prev) => [...prev, defaultRow()]);
    const removeRow = (index) => {
        if (rows.length <= 1) return;
        setRows((prev) => prev.filter((_, i) => i !== index));
    };
    const handleChange = (value, name) => {
        setFormData((prevData) => ({
            ...prevData,
            [name]: value
        }));
    const changeRow = (index, field, value) => {
        setRows((prev) => prev.map((r, i) => (i === index ? { ...r, [field]: value } : r)));
    };
    const handleSubmit = async (value) => {
        setDisabled(true)
        const res = await request.post(`/deviceSite/init`, value);
        const validRows = rows.filter(
            (r) => (r.deviceSite !== '' && r.deviceSite != null) && (r.site !== '' && r.site != null) && (r.target !== '' && (r.target || '').trim() !== '')
        );
        if (validRows.length === 0) {
            notify('请至少填写一行完整的设备站点、作业站点、目标站点', { type: 'error' });
            return;
        }
        if (!(value.channel != null && String(value.channel).trim() !== '')) {
            notify('巷道不能为空,多个请用英文逗号分隔,如 1,2,3', { type: 'error' });
            return;
        }
        setDisabled(true);
        const payload = {
            ...value,
            // 名称、wcs编号、站点标签 已从界面注释,不再提交
            name: null,
            wcsCode: null,
            label: null,
            rows: validRows.map((r) => ({
                deviceSite: String(r.deviceSite),
                site: String(r.site),
                target: String(r.target || '').trim(),
            })),
        };
        const res = await request.post('/deviceSite/init', payload);
        if (res?.data?.code === 200) {
            setOpen(false);
            refresh();
        } else {
            notify(res.data.msg);
            notify(res?.data?.msg || '初始化失败');
        }
        setDisabled(false)
    }
        setDisabled(false);
    };
    const formDefaultValues = initialData ? {
        channel: initialData.channel != null ? String(initialData.channel) : '',
        deviceType: initialData.deviceType ?? '',
        // deviceCode 接驳位已注释,不默认填入
        // deviceCode: initialData.deviceCode ?? '',
        areaIdStart: initialData.areaIdStart ?? undefined,
        areaIdEnd: initialData.areaIdEnd ?? undefined,
        flagInit: 0,
        // name、wcsCode、label 已注释,不默认填入
        // name: initialData.name ?? '',
        // wcsCode: initialData.wcsCode ?? '',
        // label: initialData.label ?? '',
        typeIds: Array.isArray(initialData.typeIds) ? initialData.typeIds : (initialData.type != null ? [initialData.type] : undefined),
    } : undefined;
    return (
        <Dialog open={open} maxWidth="lg" fullWidth>
            <Form onSubmit={handleSubmit}>
        <Dialog open={open} maxWidth="lg" fullWidth onClose={handleClose}>
            <Form onSubmit={handleSubmit} defaultValues={formDefaultValues} key={open ? (initialData ? 'copy' : 'new') : 'closed'}>
                <DialogCloseButton onClose={handleClose} />
                <DialogTitle>{translate('toolbar.siteInit')}</DialogTitle>
                <DialogTitle>{translate('toolbar.pathInit')}</DialogTitle>
                <DialogContent sx={{ mt: 2 }}>
                    <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                        <Grid container spacing={2}>
                            <Grid item xs={12}>
                                <TableContainer component={Paper} variant="outlined">
                                    <Table size="small">
                                        <TableHead>
                                            <TableRow>
                                                <TableCell>{translate('table.field.deviceSite.deviceSite')}</TableCell>
                                                <TableCell>{translate('table.field.deviceSite.site')}</TableCell>
                                                <TableCell>{translate('table.field.deviceSite.target')}</TableCell>
                                                <TableCell width={80}>操作</TableCell>
                                            </TableRow>
                                        </TableHead>
                                        <TableBody>
                                            {rows.map((r, index) => (
                                                <TableRow key={index}>
                                                    <TableCell>
                                                        <FormControl fullWidth size="small">
                                                            <Select
                                                                displayEmpty
                                                                value={r.deviceSite ?? ''}
                                                                onChange={(e) => changeRow(index, 'deviceSite', e.target.value)}
                                                                renderValue={(v) => {
                                                                    const o = stationOptions.find((s) => String(s.id) === String(v));
                                                                    return o ? o.stationName : (v ? String(v) : '');
                                                                }}
                                                            >
                                                                <MenuItem value="">请选择</MenuItem>
                                                                {stationOptions.map((opt) => (
                                                                    <MenuItem key={opt.id} value={opt.id}>
                                                                        {opt.stationName}
                                                                    </MenuItem>
                                                                ))}
                                                            </Select>
                                                        </FormControl>
                                                    </TableCell>
                                                    <TableCell>
                                                        <FormControl fullWidth size="small">
                                                            <Select
                                                                displayEmpty
                                                                value={r.site ?? ''}
                                                                onChange={(e) => changeRow(index, 'site', e.target.value)}
                                                                renderValue={(v) => {
                                                                    const o = stationOptions.find((s) => String(s.id) === String(v));
                                                                    return o ? o.stationName : (v ? String(v) : '');
                                                                }}
                                                            >
                                                                <MenuItem value="">请选择</MenuItem>
                                                                {stationOptions.map((opt) => (
                                                                    <MenuItem key={opt.id} value={opt.id}>
                                                                        {opt.stationName}
                                                                    </MenuItem>
                                                                ))}
                                                            </Select>
                                                        </FormControl>
                                                    </TableCell>
                                                    <TableCell>
                                                        <TextField
                                                            size="small"
                                                            fullWidth
                                                            placeholder={translate('table.field.deviceSite.target')}
                                                            value={r.target ?? ''}
                                                            onChange={(e) => changeRow(index, 'target', e.target.value)}
                                                        />
                                                    </TableCell>
                                                    <TableCell>
                                                        <IconButton
                                                            size="small"
                                                            onClick={() => removeRow(index)}
                                                            disabled={rows.length <= 1}
                                                        >
                                                            <Delete fontSize="small" />
                                                        </IconButton>
                                                    </TableCell>
                                                </TableRow>
                                            ))}
                                        </TableBody>
                                    </Table>
                                </TableContainer>
                                <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
                                    <Button size="small" startIcon={<Add />} onClick={addRow}>
                                        新增一行
                                    </Button>
                                </Box>
                            </Grid>
                            {/* 名称、wcs编号、站点标签 已注释,不显示也不默认填入 */}
                            {/* <Grid item xs={4}>
                                <TextInput
                                    source="name"
                                    label="table.field.deviceSite.name"
                                    size="small"
                                    fullWidth
                                />
                            </Grid>
                            <Grid item xs={4}>
                                <TextInput
                                    source="wcsCode"
                                    label="table.field.deviceSite.wcsCode"
                                    size="small"
                                    fullWidth
                                />
                            </Grid>
                            <Grid item xs={4}>
                                <TextInput
                                    source="label"
                                    label="table.field.deviceSite.label"
                                    size="small"
                                    fullWidth
                                />
                            </Grid> */}
                            <Grid item xs={4}>
                                <DictionarySelect
                                    label={translate("table.field.deviceSite.type")}
@@ -109,9 +267,7 @@
                                    dictTypeCode="sys_task_type"
                                    multiple
                                />
                            </Grid>
                            <Grid item xs={4}>
                                <DictionarySelect
                                    label={translate("table.field.deviceSite.device")}
@@ -119,44 +275,32 @@
                                    dictTypeCode="sys_device_type"
                                />
                            </Grid>
                            {/* 接驳位 deviceCode 已注释 */}
                            {/* <Grid item xs={4}>
                                <TextInput
                                    source="deviceCode"
                                    label="table.field.deviceSite.deviceCode"
                                    size="small"
                                    fullWidth
                                />
                            </Grid> */}
                            <Grid item xs={4}>
                                <TextInput
                                    label={translate("table.field.deviceSite.channel")}
                                    name="channel"
                                    source="channel"
                                    label="table.field.deviceSite.channel"
                                    size="small"
                                    type="number"
                                    fullWidth
                                    placeholder="英文逗号分隔多个,如 1,2,3"
                                />
                            </Grid>
                            <Grid item xs={4}>
                            <ReferenceInput
                                source="deviceSites"
                                reference="basStation"
                            >
                                <SelectInput
                                    label="table.field.deviceSite.deviceSite"
                                    optionText="stationName"
                                />
                            </ReferenceInput>
                            </Grid>
                            <Grid item xs={4}>
                            <ReferenceInput
                                source="site"
                                reference="basStation"
                            >
                                <SelectInput
                                    label="table.field.deviceSite.site"
                                    optionText="stationName"
                                />
                            </ReferenceInput>
                            </Grid>
                            <Grid item xs={4}>
                                <TextInput
                                    label={translate("table.field.deviceSite.target")}
                                    name="target"
                                    placeholder={translate('common.action.inputPlaceholder')}
                                    size="small"
                                // type="number"
                                    source="flagInit"
                                    label="table.field.deviceSite.flagInit"
                                    choices={[
                                        { id: 0, name: '否' },
                                        { id: 1, name: '是' },
                                    ]}
                                />
                            </Grid>
                            <Grid item xs={6} display="flex" gap={1}>
@@ -169,20 +313,7 @@
                                    <AutocompleteInput optionValue="id" optionText="name" label={translate('table.field.deviceSite.areaIdEnd')} />
                                </ReferenceInput>
                            </Grid>
                            <Grid item xs={4}>
                                <SelectInput
                                    label="table.field.deviceSite.flagInit"
                                    source="flagInit"
                                    choices={[
                                        { id: 0, name: '否' },
                                        { id: 1, name: '是' },
                                    ]}
                                />
                            </Grid>
                        </Grid>
                    </Box>
                </DialogContent>
                <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
@@ -191,11 +322,10 @@
                            {translate('toolbar.confirm')}
                        </Button>
                    </Box>
                </DialogActions>
            </Form>
        </Dialog>
    );
}
};
export default InitModal;
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasStationController.java
@@ -16,6 +16,7 @@
import com.vincent.rsf.server.system.controller.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
@@ -23,6 +24,7 @@
import java.util.stream.Collectors;
@RestController
@Api(tags = "站点管理")
public class BasStationController extends BaseController {
    @Autowired
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/DeviceSiteController.java
@@ -18,6 +18,8 @@
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -26,8 +28,10 @@
import java.util.*;
@RestController
@Api(tags = "站点管理")
@Api(tags = "路径管理")
public class DeviceSiteController extends BaseController {
    private static final Logger log = LoggerFactory.getLogger(DeviceSiteController.class);
    @Autowired
    private DeviceSiteService deviceSiteService;
@@ -128,7 +132,7 @@
    }
    @PreAuthorize("hasAuthority('manager:deviceSite:save')")
    @ApiOperation("站点初始化")
    @ApiOperation("路径初始化")
    @PostMapping("/deviceSite/init")
    public R initDeviceSite(@RequestBody DeviceSiteParame param) {
        if (Objects.isNull(param)) {
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/DeviceSiteParame.java
@@ -1,9 +1,12 @@
package com.vincent.rsf.server.manager.controller.params;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
@Data
@@ -16,8 +19,27 @@
    @ApiModelProperty("设备类型")
    private String deviceType;
    /** 作业类型,前端可能传字符串数组如 ["109"],通过 setter 统一转为 Long */
    @Setter(AccessLevel.NONE)
    @ApiModelProperty("作业类型")
    private List<Long> typeIds;
    /** 兼容前端传 ["109"] 等字符串数组 */
    public void setTypeIds(List<?> typeIds) {
        if (typeIds == null) {
            this.typeIds = null;
            return;
        }
        this.typeIds = new ArrayList<>();
        for (Object o : typeIds) {
            if (o == null) continue;
            if (o instanceof Number) {
                this.typeIds.add(((Number) o).longValue());
            } else {
                this.typeIds.add(Long.parseLong(o.toString().trim()));
            }
        }
    }
    @ApiModelProperty("作业站点")
    private String site;
@@ -31,8 +53,13 @@
    @ApiModelProperty("目标站点")
    private String target;
    @ApiModelProperty("巷道")
    private Integer channel;
    /** 多行:每行一组 设备站点、作业站点、目标站点,每行对应一条记录(再按巷道、作业类型展开) */
    @ApiModelProperty("路径行列表:设备站点、作业站点、目标站点为一组,每行一条")
    private List<DeviceSiteRowParam> rows;
    /** 巷道,英文逗号分隔多个,如 "1,2,3" */
    @ApiModelProperty("巷道,英文逗号分隔多个")
    private String channel;
    @ApiModelProperty("源库区")
    private Long areaIdStart;
@@ -40,5 +67,13 @@
    @ApiModelProperty("目标库区")
    private Long areaIdEnd;
    @ApiModelProperty("名称(公共,用于本批生成的所有路径)")
    private String name;
    @ApiModelProperty("WCS编号(公共)")
    private String wcsCode;
    @ApiModelProperty("站点标签(公共)")
    private String label;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/DeviceSiteRowParam.java
New file
@@ -0,0 +1,22 @@
package com.vincent.rsf.server.manager.controller.params;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 路径管理初始化:单行(设备站点、作业站点、目标站点一组)
 */
@Data
@Accessors(chain = true)
public class DeviceSiteRowParam {
    @ApiModelProperty("设备站点(站点ID)")
    private String deviceSite;
    @ApiModelProperty("作业站点(站点ID)")
    private String site;
    @ApiModelProperty("目标站点")
    private String target;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/DeviceSiteServiceImpl.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.manager.controller.params.DeviceSiteParame;
import com.vincent.rsf.server.manager.controller.params.DeviceSiteRowParam;
import com.vincent.rsf.server.manager.entity.BasStation;
import com.vincent.rsf.server.manager.mapper.DeviceSiteMapper;
import com.vincent.rsf.server.manager.entity.DeviceSite;
@@ -15,26 +16,27 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@Service("deviceSiteService")
public class DeviceSiteServiceImpl extends ServiceImpl<DeviceSiteMapper, DeviceSite> implements DeviceSiteService {
    /** 与表 man_device_site.target 列长度一致,超长截断避免 Data too long */
    private static final int TARGET_MAX_LENGTH = 255;
    @Autowired
    private BasStationService basStationService;
    /**
     * 初始化站点
     * @param param
     * @return
     * 初始化站点:多行(设备站点、作业站点、目标站点为一组),巷道英文逗号分隔,每组×巷道×作业类型生成多条记录
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean initSites(DeviceSiteParame param) {
        if (param.getFlagInit() == 1) {
        if (param.getFlagInit() != null && param.getFlagInit() == 1) {
            List<DeviceSite> list = this.list(new LambdaQueryWrapper<DeviceSite>().select(DeviceSite::getId).last("limit 1"));
            if (!list.isEmpty()) {
                if (!this.remove(new LambdaQueryWrapper<>())) {
@@ -42,47 +44,55 @@
                }
            }
        }
        if (Objects.isNull(param.getDeviceSites()) || StringUtils.isBlank(param.getDeviceSites())) {
            throw new CoolException("初始化失败: 设备作业站点不能为空!!");
        }
        if (Objects.isNull(param.getSite()) || StringUtils.isBlank(param.getSite())) {
            throw new CoolException("初始化失败: 作业站点不能为空!!");
        }
        if (Objects.isNull(param.getTypeIds()) || param.getTypeIds().isEmpty()) {
            throw new CoolException("初始化失败: 作业类型不能为空!!");
        }
        if (Objects.isNull(param.getTarget()) || param.getTarget().isEmpty()) {
            throw new CoolException("初始化失败: 目标站点不能为空!!");
        if (StringUtils.isBlank(param.getChannel())) {
            throw new CoolException("初始化失败: 巷道不能为空!!");
        }
        List<String> sites = Arrays.asList(StringUtils.split(param.getSite(), ","));
        List<String> dvSites = Arrays.asList(StringUtils.split(param.getDeviceSites(), ","));
        List<String> targets = Arrays.asList(StringUtils.split(param.getTarget(), ","));
        List<Integer> channels = parseChannels(param.getChannel());
        if (channels.isEmpty()) {
            throw new CoolException("初始化失败: 巷道格式错误,请用英文逗号分隔,如 1,2,3!!");
        }
        List<DeviceSiteRowParam> rows = param.getRows();
        if (Objects.isNull(rows) || rows.isEmpty()) {
            throw new CoolException("初始化失败: 请至少添加一行(设备站点、作业站点、目标站点)!!");
        }
        List<DeviceSite> deviceSites =  new ArrayList<>();
        for (String site : sites) {
            BasStation basStation = basStationService.getById(site);
            if (null == basStation) {
                throw new CoolException("初始化失败: 站点未找到!!");
        for (DeviceSiteRowParam row : rows) {
            if (StringUtils.isBlank(row.getDeviceSite()) || StringUtils.isBlank(row.getSite()) || StringUtils.isBlank(row.getTarget())) {
                throw new CoolException("初始化失败: 每行的设备站点、作业站点、目标站点均不能为空!!");
            }
            for (String deviceSite : dvSites) {
                BasStation basStation2 = basStationService.getById(deviceSite);
                if (null == basStation2) {
            BasStation siteStation = basStationService.getById(Long.parseLong(row.getSite().trim()));
            if (siteStation == null) {
                    throw new CoolException("初始化失败: 作业站点未找到!!");
                }
                for (Long id : param.getTypeIds()) {
                    for (String target : targets) {
                        DeviceSite site1 = new DeviceSite();
                        site1.setType(id + "")
                                .setSite(basStation.getStationName())
            BasStation deviceStation = basStationService.getById(Long.parseLong(row.getDeviceSite().trim()));
            if (deviceStation == null) {
                throw new CoolException("初始化失败: 设备站点未找到!!");
            }
            for (Long typeId : param.getTypeIds()) {
                for (Integer ch : channels) {
                    DeviceSite ds = new DeviceSite();
                    String siteName = siteStation.getStationName();
                    String deviceSiteName = deviceStation.getStationName();
                    String targetVal = truncate(row.getTarget().trim(), TARGET_MAX_LENGTH);
                    String commonName = StringUtils.isNotBlank(param.getName()) ? param.getName().trim() : null;
                    String commonLabel = StringUtils.isNotBlank(param.getLabel()) ? param.getLabel().trim() : null;
                    ds.setType(String.valueOf(typeId))
                            .setSite(siteName)
                                .setDevice(param.getDeviceType())
                                .setDeviceSite(basStation2.getStationName())
                                .setTarget(target)
                            .setDeviceSite(deviceSiteName)
                            .setTarget(targetVal)
                                .setDeviceCode(param.getDeviceCode())
                                .setAreaIdStart(param.getAreaIdStart())
                                .setAreaIdEnd(param.getAreaIdEnd())
                                .setChannel(param.getChannel())
                                ;
                        deviceSites.add(site1);
                    }
                            .setChannel(ch)
                            .setName(commonName != null ? commonName : (deviceSiteName + "-" + siteName + "-" + targetVal + "-" + ch))
                            .setLabel(commonLabel);
                    deviceSites.add(ds);
                }
            }
        }
@@ -91,4 +101,28 @@
        }
        return true;
    }
    private static String truncate(String s, int maxLen) {
        if (s == null) return null;
        return s.length() <= maxLen ? s : s.substring(0, maxLen);
    }
    /** 巷道英文逗号分割,解析为整数列表 */
    private List<Integer> parseChannels(String channelStr) {
        if (StringUtils.isBlank(channelStr)) {
            return Collections.emptyList();
        }
        String[] parts = channelStr.split(",");
        List<Integer> list = new ArrayList<>();
        for (String p : parts) {
            String t = (p == null) ? "" : p.trim();
            if (t.isEmpty()) continue;
            try {
                list.add(Integer.parseInt(t));
            } catch (NumberFormatException e) {
                return Collections.emptyList();
            }
        }
        return list;
    }
}