From 11e44b3012e829ff7c85363ce1a7c2c0457c72f0 Mon Sep 17 00:00:00 2001
From: vincentlu <t1341870251@gmail.com>
Date: 星期二, 17 三月 2026 13:59:27 +0800
Subject: [PATCH] #

---
 zy-acs-flow/src/page/loc/LocList.jsx                                                  |   15 ++-
 zy-acs-flow/src/page/loc/LocCreate.jsx                                                |    5 +
 zy-acs-flow/src/page/loc/LocEmptyData.jsx                                             |   48 +++++++++
 zy-acs-flow/src/page/loc/LocInit.jsx                                                  |    5 +
 zy-acs-flow/src/page/loc/useLocImport.jsx                                             |   17 +++
 zy-acs-flow/public/imports/loc_import_template.xlsx                                   |    0 
 zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/LocController.java |  183 ++++++++++++++++++++++++++++++++++++
 7 files changed, 268 insertions(+), 5 deletions(-)

diff --git a/zy-acs-flow/public/imports/loc_import_template.xlsx b/zy-acs-flow/public/imports/loc_import_template.xlsx
index 5dae55f..d2f7fe9 100644
--- a/zy-acs-flow/public/imports/loc_import_template.xlsx
+++ b/zy-acs-flow/public/imports/loc_import_template.xlsx
Binary files differ
diff --git a/zy-acs-flow/src/page/loc/LocCreate.jsx b/zy-acs-flow/src/page/loc/LocCreate.jsx
index afebd46..cd6251c 100644
--- a/zy-acs-flow/src/page/loc/LocCreate.jsx
+++ b/zy-acs-flow/src/page/loc/LocCreate.jsx
@@ -31,6 +31,10 @@
 import StatusSelectInput from "../components/StatusSelectInput";
 import MemoInput from "../components/MemoInput";
 import { compDirectChoices } from "./compDirect";
+import ImportButton from '../components/ImportButton';
+import { useLocImport } from './useLocImport';
+
+const IMPORT_TEMP_URL = '/imports/loc_import_template.xlsx';
 
 const LocCreate = (props) => {
     const { open, setOpen } = props;
@@ -219,6 +223,7 @@
                         <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                             <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}  >
                                 <SaveButton />
+                                <ImportButton type="xlsx" importTemp={IMPORT_TEMP_URL} useImport={useLocImport} onceBatch={10} />
                             </Toolbar>
                         </DialogActions>
                     </Form>
diff --git a/zy-acs-flow/src/page/loc/LocEmptyData.jsx b/zy-acs-flow/src/page/loc/LocEmptyData.jsx
new file mode 100644
index 0000000..ee621cc
--- /dev/null
+++ b/zy-acs-flow/src/page/loc/LocEmptyData.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Box, Button, SvgIcon, Typography } from '@mui/material';
+import { HotTub } from '@mui/icons-material';
+import CorporateFareIcon from '@mui/icons-material/CorporateFare';
+import { useTranslate } from 'react-admin';
+
+const LocEmptyData = ({ onInit }) => {
+    const translate = useTranslate();
+
+    return (
+        <Box
+            display="flex"
+            flexDirection="column"
+            alignItems="center"
+            justifyContent="flex-start"
+            height="100vh"
+            pt={10}
+        >
+            <SvgIcon component={HotTub} sx={{ fontSize: '18em', mb: 2, opacity: .5 }} />
+            <Typography variant="h1" gutterBottom sx={{
+                fontWeight: 'bold',
+                fontSize: '2em',
+                opacity: .5,
+                mt: 2
+            }}>
+                {translate('create.empty.title')}
+            </Typography>
+            <Typography variant="subtitle1" gutterBottom sx={{
+                fontSize: '1em',
+                opacity: .5,
+                mt: 2
+            }}>
+                {translate('create.empty.desc')}
+            </Typography>
+            <Button
+                variant="contained"
+                color="primary"
+                startIcon={<CorporateFareIcon />}
+                onClick={onInit}
+                sx={{ fontSize: '1em', mt: 2 }}
+            >
+                {translate('page.loc.init')}
+            </Button>
+        </Box>
+    );
+};
+
+export default LocEmptyData;
diff --git a/zy-acs-flow/src/page/loc/LocInit.jsx b/zy-acs-flow/src/page/loc/LocInit.jsx
index 1ca4cb3..e1cad02 100644
--- a/zy-acs-flow/src/page/loc/LocInit.jsx
+++ b/zy-acs-flow/src/page/loc/LocInit.jsx
@@ -29,6 +29,10 @@
 import CheckIcon from '@mui/icons-material/Check';
 import request from '@/utils/request'
 import { compDirectChoices } from "./compDirect";
+import ImportButton from '../components/ImportButton';
+import { useLocImport } from './useLocImport';
+
+const IMPORT_TEMP_URL = '/imports/loc_import_template.xlsx';
 
 const LocInit = (props) => {
     const { open, setOpen } = props;
@@ -187,6 +191,7 @@
                     <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                         <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}  >
                             <Button variant="contained" type="submit" startIcon={<CheckIcon />}>{translate('ra.action.confirm')}</Button>
+                            <ImportButton type="xlsx" importTemp={IMPORT_TEMP_URL} useImport={useLocImport} onceBatch={10} />
                         </Toolbar>
                     </DialogActions>
                 </Form>
diff --git a/zy-acs-flow/src/page/loc/LocList.jsx b/zy-acs-flow/src/page/loc/LocList.jsx
index ecff579..8e791f4 100644
--- a/zy-acs-flow/src/page/loc/LocList.jsx
+++ b/zy-acs-flow/src/page/loc/LocList.jsx
@@ -38,7 +38,6 @@
 import { styled } from '@mui/material/styles';
 import LocCreate from "./LocCreate";
 import LocPanel from "./LocPanel";
-import EmptyData from "../components/EmptyData";
 import MyCreateButton from "../components/MyCreateButton";
 import MyExportButton from '../components/MyExportButton';
 import PageDrawer from "../components/PageDrawer";
@@ -51,6 +50,11 @@
 import BulkUpdateButton from "../components/BulkUpdateButton";
 import LocBulkUpdateContent from './LocBulkUpdateContent';
 import { getCompDirectLabel } from './compDirect';
+import LocEmptyData from "./LocEmptyData";
+import ImportButton from '../components/ImportButton';
+import { useLocImport } from './useLocImport';
+
+const IMPORT_TEMP_URL = '/imports/loc_import_template.xlsx';
 
 const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
     '& .css-1vooibu-MuiSvgIcon-root': {
@@ -69,7 +73,7 @@
 
 const filters = [
     <SearchInput source="condition" alwaysOn />,
-    <TextInput source="locNo" label="table.field.loc.locNo" alwaysOn />,
+    <TextInput source="locNo" label="table.field.loc.locNo" />,
     <ReferenceInput source="locSts" label="table.field.loc.locSts" reference="locSts" alwaysOn>
         <AutocompleteInput label="table.field.loc.locSts" optionText="name" filterToQuery={(val) => ({ name: val })} />
     </ReferenceInput>,
@@ -135,7 +139,7 @@
                     marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                 }}
                 title={"menu.loc"}
-                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
+                empty={<LocEmptyData onInit={() => { setInitDialog(true); }} />}
                 filters={filters}
                 sort={{ field: "locNo", order: "asc" }}
                 actions={(
@@ -146,6 +150,7 @@
                         }}><CorporateFareIcon /></Button>
                         <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                         <SelectColumnsButton preferenceKey='loc' />
+                        <ImportButton type="xlsx" importTemp={IMPORT_TEMP_URL} useImport={useLocImport} onceBatch={10} />
                         <MyExportButton />
                     </TopToolbar>
                 )}
@@ -156,8 +161,8 @@
                     preferenceKey='loc'
                     bulkActionButtons={<LocBulkActionButtons />}
                     rowClick={(id, resource, record) => false}
-                    expand={() => <LocPanel />}
-                    expandSingle={true}
+                    expand={false}
+                    // expandSingle={true}
                     omit={['id', 'locType', 'uuid', 'zpallet', 'barcode'
                         , 'statusBool', 'updateBy', 'createTime', 'createBy', 'memo']}
                     rowSx={rowSx(drawerVal || null)}
diff --git a/zy-acs-flow/src/page/loc/useLocImport.jsx b/zy-acs-flow/src/page/loc/useLocImport.jsx
new file mode 100644
index 0000000..15e413e
--- /dev/null
+++ b/zy-acs-flow/src/page/loc/useLocImport.jsx
@@ -0,0 +1,17 @@
+import { useCallback } from 'react';
+import request from '@/utils/request';
+
+export function useLocImport() {
+    const processBatch = useCallback(async (batch) => {
+        const res = await request.post('/loc/import', batch);
+        const { code, msg } = res.data;
+        if (code !== 200) {
+            console.error(msg);
+            throw new Error(`Batch import failed: ${msg}`);
+        }
+    }, []);
+
+    return {
+        processBatch,
+    };
+}
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/LocController.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/LocController.java
index bf5b8a7..01e3979 100644
--- a/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/LocController.java
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/LocController.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zy.acs.common.constant.CommonConstant;
 import com.zy.acs.common.utils.Utils;
 import com.zy.acs.framework.common.Cools;
 import com.zy.acs.framework.common.R;
@@ -12,9 +13,15 @@
 import com.zy.acs.manager.common.domain.PageParam;
 import com.zy.acs.manager.common.utils.ExcelUtil;
 import com.zy.acs.manager.manager.controller.param.LocInitParam;
+import com.zy.acs.manager.manager.entity.Code;
 import com.zy.acs.manager.manager.entity.Loc;
+import com.zy.acs.manager.manager.entity.LocSts;
+import com.zy.acs.manager.manager.entity.LocType;
 import com.zy.acs.manager.manager.entity.Zone;
+import com.zy.acs.manager.manager.service.CodeService;
 import com.zy.acs.manager.manager.service.LocService;
+import com.zy.acs.manager.manager.service.LocStsService;
+import com.zy.acs.manager.manager.service.LocTypeService;
 import com.zy.acs.manager.manager.service.ZoneService;
 import com.zy.acs.manager.system.controller.BaseController;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -33,6 +40,12 @@
     private LocService locService;
     @Autowired
     private ZoneService zoneService;
+    @Autowired
+    private CodeService codeService;
+    @Autowired
+    private LocStsService locStsService;
+    @Autowired
+    private LocTypeService locTypeService;
 
     @PreAuthorize("hasAuthority('manager:loc:list')")
     @PostMapping("/loc/page")
@@ -133,6 +146,64 @@
     }
 
     @PreAuthorize("hasAuthority('manager:loc:save')")
+    @PostMapping("/loc/import")
+    public R importBatch(@RequestBody List<Map<String, Object>> rows) {
+        if (Cools.isEmpty(rows)) {
+            return R.ok();
+        }
+        Date now = new Date();
+        Long userId = getLoginUserId();
+        for (Map<String, Object> row : rows) {
+            Zone zone = resolveZone(readCell(row, "zone"));
+            Integer rowNo = parseInteger(row.get("row"), "row", true);
+            Integer bayNo = parseInteger(row.get("bay"), "bay", true);
+            Integer levNo = parseInteger(row.get("lev"), "lev", true);
+            String locNo = buildLocNo(zone, rowNo, bayNo, levNo);
+
+            Long locStsId = resolveLocStsId(readCell(row, "loc_sts"));
+            Long locTypeId = resolveLocTypeId(readCell(row, "loc_type"));
+            Long codeId = resolveCodeId(readCell(row, "code"));
+            Integer compDirect = parseInteger(row.get("comp_direct"), "comp_direct", false);
+            Double offset = parseDouble(row.get("offset"), "offset");
+
+            Loc loc = locService.selectByLocNo(locNo);
+            boolean exists = loc != null;
+            if (!exists) {
+                loc = new Loc();
+                loc.setUuid(locNo);
+                loc.setLocNo(locNo);
+                loc.setName(locNo);
+                loc.setStatus(1);
+                loc.setDeleted(0);
+                loc.setCreateBy(userId);
+                loc.setCreateTime(now);
+            }
+            loc.setZoneId(zone.getId());
+            loc.setRow(rowNo);
+            loc.setBay(bayNo);
+            loc.setLev(levNo);
+            loc.setLocSts(locStsId);
+            loc.setLocType(locTypeId);
+            loc.setCode(codeId);
+            loc.setCompDirect(compDirect);
+            loc.setOffset(offset);
+            loc.setUpdateBy(userId);
+            loc.setUpdateTime(now);
+
+            if (!exists) {
+                if (!locService.save(loc)) {
+                    throw new CoolException(locNo + " save fail !");
+                }
+            } else {
+                if (!locService.updateById(loc)) {
+                    throw new CoolException(locNo + " update fail !");
+                }
+            }
+        }
+        return R.ok();
+    }
+
+    @PreAuthorize("hasAuthority('manager:loc:save')")
     @OperationLog
     @PostMapping("/loc/init")
     public R init(@RequestBody LocInitParam param) {
@@ -186,4 +257,116 @@
         return R.ok("initialize success");
     }
 
+    private Zone resolveZone(String identifier) {
+        if (Cools.isEmpty(identifier)) {
+            throw new CoolException("zone is required");
+        }
+        Zone zone = zoneService.getOne(new LambdaQueryWrapper<Zone>().eq(Zone::getUuid, identifier), false);
+        if (zone == null) {
+            zone = zoneService.getOne(new LambdaQueryWrapper<Zone>().eq(Zone::getName, identifier), false);
+        }
+        if (zone == null) {
+            throw new CoolException("zone " + identifier + " not found");
+        }
+        return zone;
+    }
+
+    private Long resolveLocStsId(String identifier) {
+        if (Cools.isEmpty(identifier)) {
+            throw new CoolException("loc_sts is required");
+        }
+        LocSts locSts = locStsService.getOne(new LambdaQueryWrapper<LocSts>().eq(LocSts::getUuid, identifier), false);
+        if (locSts == null) {
+            locSts = locStsService.getOne(new LambdaQueryWrapper<LocSts>().eq(LocSts::getName, identifier), false);
+        }
+        if (locSts == null) {
+            throw new CoolException("loc_sts " + identifier + " not found");
+        }
+        return locSts.getId();
+    }
+
+    private Long resolveLocTypeId(String identifier) {
+        if (Cools.isEmpty(identifier)) {
+            return null;
+        }
+        LocType locType = locTypeService.getOne(new LambdaQueryWrapper<LocType>().eq(LocType::getUuid, identifier), false);
+        if (locType == null) {
+            locType = locTypeService.getOne(new LambdaQueryWrapper<LocType>().eq(LocType::getName, identifier), false);
+        }
+        if (locType == null) {
+            throw new CoolException("loc_type " + identifier + " not found");
+        }
+        return locType.getId();
+    }
+
+    private Long resolveCodeId(String data) {
+        if (Cools.isEmpty(data)) {
+            return null;
+        }
+        String codeData = Utils.zeroFill(data, CommonConstant.QR_CODE_LEN);
+        Code code = codeService.getCacheByData(codeData);
+        if (code == null) {
+            throw new CoolException("code " + data + " not exist");
+        }
+        return code.getId();
+    }
+
+    private Integer parseInteger(Object value, String field, boolean required) {
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        String text = value == null ? null : String.valueOf(value).trim();
+        if (Cools.isEmpty(text)) {
+            if (required) {
+                throw new CoolException(field + " is required");
+            }
+            return null;
+        }
+        try {
+            return Double.valueOf(text).intValue();
+        } catch (NumberFormatException ex) {
+            throw new CoolException(field + " format error: " + text);
+        }
+    }
+
+    private Double parseDouble(Object value, String field) {
+        if (value instanceof Number) {
+            return ((Number) value).doubleValue();
+        }
+        String text = value == null ? null : String.valueOf(value).trim();
+        if (Cools.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return Double.valueOf(text);
+        } catch (NumberFormatException ex) {
+            throw new CoolException(field + " format error: " + text);
+        }
+    }
+
+    private String readCell(Map<String, Object> row, String key) {
+        if (row == null) {
+            return null;
+        }
+        Object value = row.get(key);
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value).trim();
+        return Cools.isEmpty(text) ? null : text;
+    }
+
+    private String buildLocNo(Zone zone, Integer row, Integer bay, Integer lev) {
+        if (zone == null || row == null || bay == null || lev == null) {
+            throw new CoolException("zone/row/bay/lev cannot be null");
+        }
+        if (Cools.isEmpty(zone.getUuid())) {
+            throw new CoolException("zone uuid is empty");
+        }
+        return Utils.zeroFill(zone.getUuid(), 2)
+                + String.format("%03d", row)
+                + String.format("%03d", bay)
+                + String.format("%02d", lev);
+    }
+
 }

--
Gitblit v1.9.1