feat:添加批量放置货架功能,添加导出弹窗,可导出筛选的数据
| | |
| | | endTime: "end time", |
| | | errTime: "err time", |
| | | errDesc: "error", |
| | | batchshelving: "Batch Shelving", |
| | | |
| | | |
| | | }, |
| | | |
| | | mission: { |
| | | backpack: 'Backpack', |
| | | code: 'Code', |
| | |
| | | code: 'Send Code', |
| | | }, |
| | | }, |
| | | |
| | | settings: { |
| | | resetPwd: { |
| | | currPwd: 'Current Password', |
| | |
| | | }, |
| | | }, |
| | | }, |
| | | batch: { |
| | | title: 'Batch Shelving Settings', |
| | | startX: 'Start X (mm)', |
| | | startY: 'Start Y (mm)', |
| | | autoOffset: 'Auto Offset', |
| | | rows: 'Rows', |
| | | cols: 'Columns', |
| | | gapX: 'Row Spacing (mm)', |
| | | gapY: 'Column Spacing (mm)', |
| | | angle: 'Shelf Angle (deg)', |
| | | }, |
| | | devices: { |
| | | title: 'Icons', |
| | | shelf: 'SHELF', |
| | |
| | | monitor: 'Console', |
| | | save: 'Save Map', |
| | | clear: 'Clear Map', |
| | | batchshelving: "Batch Shelving", |
| | | adapt: 'ADAPT', |
| | | rotate: 'ROTATE', |
| | | flip: 'FLIP', |
| | |
| | | }, |
| | | }, |
| | | } |
| | | |
| | | }; |
| | | |
| | | export default customEnglishMessages; |
| | |
| | | endTime: "结束时间", |
| | | duration: "持续时长", |
| | | state: "状态", |
| | | |
| | | }, |
| | | action: { |
| | | uuid: "编号", |
| | |
| | | endTime: "完成时间", |
| | | errTime: "异常时间", |
| | | errDesc: "异常", |
| | | batchshelving: "批量上架", |
| | | }, |
| | | mission: { |
| | | backpack: '背篓', |
| | |
| | | code: '获取验证码', |
| | | }, |
| | | }, |
| | | |
| | | settings: { |
| | | resetPwd: { |
| | | currPwd: '当前密码', |
| | |
| | | }, |
| | | }, |
| | | }, |
| | | batch: { |
| | | title: '货架阵列设置', |
| | | startX: '起点 X (毫米)', |
| | | startY: '起点 Y (毫米)', |
| | | autoOffset: '自动偏移', |
| | | rows: '行数', |
| | | cols: '列数', |
| | | gapX: '行间距 (毫米)', |
| | | gapY: '列间距 (毫米)', |
| | | angle: '货架角度 (度)', |
| | | }, |
| | | devices: { |
| | | title: '图标库', |
| | | shelf: '货架', |
| | |
| | | monitor: '日志监控', |
| | | save: '保存地图', |
| | | clear: '清空地图', |
| | | batchshelving: "批量上架", |
| | | adapt: '适配', |
| | | rotate: '旋转', |
| | | flip: '翻转', |
| | |
| | | addArea: '添加区域', |
| | | cancelAddArea: '取消添加', |
| | | areaList: '区域列表', |
| | | |
| | | }, |
| | | mode: { |
| | | observer: '观察模式', |
| | |
| | | }, |
| | | }, |
| | | } |
| | | |
| | | }; |
| | | |
| | | export default customChineseMessages; |
| New file |
| | |
| | | import React, { useState, useEffect } from 'react'; |
| | | import { Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Stack } from '@mui/material'; |
| | | import { useTranslate } from 'react-admin'; |
| | | |
| | | const BatchShelfDialog = ({ open, onClose, onConfirm, mapContainer }) => { |
| | | const translate = useTranslate(); |
| | | const [startX, setStartX] = useState(0); |
| | | const [startY, setStartY] = useState(0); |
| | | const [rows, setRows] = useState(20); |
| | | const [cols, setCols] = useState(15); |
| | | const [gapX, setGapX] = useState(1000); |
| | | const [gapY, setGapY] = useState(1000); |
| | | const [angle, setAngle] = useState(0); |
| | | |
| | | // 自动偏移:找到已有货架的最大 X 和 Y,并加上对应间距作为新起点 |
| | | const handleAutoOffset = () => { |
| | | if (!mapContainer) { |
| | | console.warn('地图容器未传入,无法自动偏移'); |
| | | return; |
| | | } |
| | | let maxX = -Infinity; |
| | | let maxY = -Infinity; |
| | | mapContainer.children.forEach(child => { |
| | | if (child.data && (child.data.type === 'shelf' || child.data.type === 'SHELF')) { |
| | | maxX = Math.max(maxX, child.x); |
| | | maxY = Math.max(maxY, child.y); |
| | | } |
| | | }); |
| | | setStartX(isFinite(maxX) ? maxX + gapX : 0); |
| | | setStartY(isFinite(maxY) ? maxY + gapY : 0); |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | onConfirm({ |
| | | startX: parseFloat(startX), |
| | | startY: parseFloat(startY), |
| | | rows: parseInt(rows, 10), |
| | | cols: parseInt(cols, 10), |
| | | gapX: parseFloat(gapX), |
| | | gapY: parseFloat(gapY), |
| | | angle: parseFloat(angle), |
| | | }); |
| | | }; |
| | | |
| | | // 辅助函数:处理手动输入,允许空值(置为0) |
| | | const handleNumberInput = (setter) => (e) => { |
| | | const val = e.target.value; |
| | | if (val === '') { |
| | | setter(0); |
| | | } else { |
| | | const num = parseFloat(val); |
| | | if (!isNaN(num)) setter(num); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth> |
| | | <DialogTitle>{translate('page.map.batch.title', { _: '货架阵列设置' })}</DialogTitle> |
| | | <DialogContent> |
| | | <Stack spacing={2} sx={{ mt: 1 }}> |
| | | <Stack direction="row" spacing={1} alignItems="center"> |
| | | <TextField |
| | | label={translate('page.map.batch.startX', { _: '起点 X (毫米)' })} |
| | | type="text" |
| | | value={startX} |
| | | onChange={handleNumberInput(setStartX)} |
| | | fullWidth |
| | | /> |
| | | <TextField |
| | | label={translate('page.map.batch.startY', { _: '起点 Y (毫米)' })} |
| | | type="text" |
| | | value={startY} |
| | | onChange={handleNumberInput(setStartY)} |
| | | fullWidth |
| | | /> |
| | | <Button variant="outlined" onClick={handleAutoOffset}> |
| | | {translate('page.map.batch.autoOffset', { _: '自动偏移' })} |
| | | </Button> |
| | | </Stack> |
| | | |
| | | <TextField |
| | | label={translate('page.map.batch.rows', { _: '行数' })} |
| | | type="number" |
| | | value={rows} |
| | | onChange={e => setRows(parseInt(e.target.value, 10))} |
| | | inputProps={{ min: 1 }} |
| | | fullWidth |
| | | /> |
| | | <TextField |
| | | label={translate('page.map.batch.cols', { _: '列数' })} |
| | | type="number" |
| | | value={cols} |
| | | onChange={e => setCols(parseInt(e.target.value, 10))} |
| | | inputProps={{ min: 1 }} |
| | | fullWidth |
| | | /> |
| | | <TextField |
| | | label={translate('page.map.batch.gapX', { _: '行间距 (毫米)' })} |
| | | type="number" |
| | | value={gapX} |
| | | onChange={e => setGapX(parseFloat(e.target.value))} |
| | | inputProps={{ step: 100, min: 0 }} |
| | | fullWidth |
| | | /> |
| | | <TextField |
| | | label={translate('page.map.batch.gapY', { _: '列间距 (毫米)' })} |
| | | type="number" |
| | | value={gapY} |
| | | onChange={e => setGapY(parseFloat(e.target.value))} |
| | | inputProps={{ step: 100, min: 0 }} |
| | | fullWidth |
| | | /> |
| | | <TextField |
| | | label={translate('page.map.batch.angle', { _: '货架角度 (度)' })} |
| | | type="number" |
| | | value={angle} |
| | | onChange={e => setAngle(parseFloat(e.target.value))} |
| | | inputProps={{ step: 1 }} |
| | | fullWidth |
| | | /> |
| | | </Stack> |
| | | </DialogContent> |
| | | <DialogActions> |
| | | <Button onClick={onClose}>{translate('ra.action.cancel')}</Button> |
| | | <Button onClick={handleSubmit} variant="contained" color="primary"> |
| | | {translate('ra.action.confirm')} |
| | | </Button> |
| | | </DialogActions> |
| | | </Dialog> |
| | | ); |
| | | }; |
| | | |
| | | export default BatchShelfDialog; |
| | |
| | | import AreaFab from "./header/AreaFab"; |
| | | import MoreOperate from "./header/MoreOperate"; |
| | | import NewsLogDialog from "./NewsLogDialog"; |
| | | import BatchShelfDialog from './BatchShelfDialog'; |
| | | import { DEVICE_TYPE } from './constants'; // 确认路径 |
| | | |
| | | let player; |
| | | let websocket; |
| | | |
| | | const Map = () => { |
| | | |
| | | const notify = useNotification(); |
| | | const [sidebarOpen] = useSidebarState(); |
| | | const translate = useTranslate(); |
| | | |
| | | |
| | | |
| | | // 调试输出 |
| | | console.log('batchshelving translation:', translate('batchshelving')); |
| | | const theme = useTheme(); |
| | | const themeMode = theme.palette.mode; |
| | | |
| | |
| | | |
| | | const [curSprite, setCurSprite] = useState(null); |
| | | const [batchSprites, setBatchSprites] = useState([]); |
| | | |
| | | const [batchDialogOpen, setBatchDialogOpen] = useState(false); |
| | | const [history, setHistory] = useState([]); |
| | | const [historyIndex, setHistoryIndex] = useState(-1); |
| | | const startBatchPlace = () => { |
| | | setBatchDialogOpen(true); |
| | | }; |
| | | const recordAction = (action) => { |
| | | const newHistory = history.slice(0, historyIndex + 1); |
| | | newHistory.push(action); |
| | | setHistory(newHistory); |
| | | setHistoryIndex(newHistory.length - 1); |
| | | }; |
| | | |
| | | const undo = () => { |
| | | if (historyIndex < 0) return; |
| | | const action = history[historyIndex]; |
| | | if (action.type === 'ADD_SHELVES') { |
| | | action.sprites.forEach(sprite => { |
| | | if (sprite.parent) mapContainer.removeChild(sprite); |
| | | sprite.destroy(); |
| | | }); |
| | | if (batchSprites.some(s => action.sprites.includes(s))) { |
| | | setBatchSprites([]); |
| | | setCurSprite(null); |
| | | } |
| | | } |
| | | setHistoryIndex(historyIndex - 1); |
| | | }; |
| | | const handleBatchConfirm = async ({ startX, startY, rows, cols, gapX, gapY, angle }) => { |
| | | const start = { x: startX, y: startY }; |
| | | const angleRad = angle * Math.PI / 180; |
| | | console.log(`从 (${startX}, ${startY}) 生成 ${rows} 行 ${cols} 列,间距(${gapX},${gapY}),角度${angle}°`); |
| | | |
| | | const existingPositions = new Set(); |
| | | if (mapContainer) { |
| | | mapContainer.children.forEach(child => { |
| | | if (child.data?.type === DEVICE_TYPE.SHELF) { |
| | | const key = `${Math.round(child.x * 10)}_${Math.round(child.y * 10)}`; |
| | | existingPositions.add(key); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | let created = 0; |
| | | const newSprites = []; |
| | | for (let i = 0; i < rows; i++) { |
| | | for (let j = 0; j < cols; j++) { |
| | | const posX = start.x + j * gapX; |
| | | const posY = start.y + i * gapY; |
| | | const key = `${Math.round(posX * 10)}_${Math.round(posY * 10)}`; |
| | | if (existingPositions.has(key)) continue; |
| | | |
| | | const sprite = Tool.generateSprite(DEVICE_TYPE.SHELF); |
| | | if (!sprite) continue; |
| | | Tool.initSprite(sprite, DEVICE_TYPE.SHELF); |
| | | sprite.x = posX; |
| | | sprite.y = posY; |
| | | sprite.rotation = angleRad; |
| | | mapContainer.addChild(sprite); |
| | | Tool.beMovable(sprite); |
| | | created++; |
| | | newSprites.push(sprite); |
| | | } |
| | | } |
| | | |
| | | if (newSprites.length) { |
| | | recordAction({ type: 'ADD_SHELVES', sprites: newSprites }); |
| | | } |
| | | |
| | | notify.success(`成功生成 ${created} 个货架,请记得保存地图`); |
| | | setBatchDialogOpen(false); |
| | | }; |
| | | |
| | | |
| | | const [rcsStatus, setRcsStatus] = useState(null); |
| | | const [showRoutes, setShowRoutes] = useState(false); |
| | |
| | | Http.saveMapData(curZone); |
| | | }} |
| | | /> |
| | | <Button |
| | | variant="contained" |
| | | color="secondary" |
| | | onClick={startBatchPlace} |
| | | sx={{ textTransform: 'none' }} |
| | | > |
| | | {translate('batchshelving',{_: '批量货架'})} |
| | | </Button> |
| | | |
| | | <Button onClick={undo} disabled={historyIndex < 0}> |
| | | {translate('ra.action.undo', { _: '撤销' })} |
| | | </Button> |
| | | </> |
| | | )} |
| | | |
| | |
| | | open={logDialogOpen} |
| | | onClose={() => setLogDialogOpen(false)} |
| | | /> |
| | | <BatchShelfDialog |
| | | open={batchDialogOpen} |
| | | onClose={() => setBatchDialogOpen(false)} |
| | | onConfirm={handleBatchConfirm} |
| | | mapContainer={mapContainer} |
| | | /> |
| | | </Box> |
| | | ); |
| | | } |
| | | |
| | | const MapPage = () => { |
| | | |
| | | |
| | | return ( |
| | | |
| | | <NotificationProvider> |
| | | <Map /> |
| | | </NotificationProvider> |
| | |
| | | let sprite; |
| | | switch (deviceType) { |
| | | case DEVICE_TYPE.SHELF: |
| | | sprite = new PIXI.Sprite(PIXI.Texture.from(shelf, { resourceOptions: { scale: 1 } })); |
| | | // sprite.width = 50; |
| | | // sprite.height = 50; |
| | | sprite = new PIXI.Sprite(PIXI.Texture.from(shelf)); |
| | | sprite.scale.set(2.0, 2.0); // 调整合适的大小 |
| | | sprite.zIndex = DEVICE_Z_INDEX.SHELF; |
| | | break; |
| | | case DEVICE_TYPE.CHARGE: |
| | |
| | | import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; |
| | | import { useNavigate } from 'react-router-dom'; |
| | | import React, { useState } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | |
| | | SelectColumnsButton, |
| | | EditButton, |
| | | FilterButton, |
| | | CreateButton, |
| | | ExportButton, |
| | | BulkDeleteButton, |
| | | WrapperField, |
| | | useRecordContext, |
| | | useTranslate, |
| | | useListContext, |
| | | useCreatePath, |
| | | TextField, |
| | | NumberField, |
| | | DateField, |
| | | BooleanField, |
| | | ReferenceField, |
| | | TextInput, |
| | | DateTimeInput, |
| | | DateInput, |
| | | SelectInput, |
| | | NumberInput, |
| | | ReferenceInput, |
| | | ReferenceArrayInput, |
| | | AutocompleteInput, |
| | | DeleteButton, |
| | | FunctionField, |
| | | useNotify, |
| | | downloadCSV |
| | | } from 'react-admin'; |
| | | import { Box, Typography, Card, Stack } from '@mui/material'; |
| | | import { Box, Button, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; |
| | | import { styled } from '@mui/material/styles'; |
| | | import CodeCreate from "./CodeCreate"; |
| | | import CodePanel from "./CodePanel"; |
| | | import EmptyData from "../components/EmptyData"; |
| | | import MyCreateButton from "../components/MyCreateButton"; |
| | | import MyExportButton from '../components/MyExportButton'; |
| | | import PageDrawer from "../components/PageDrawer"; |
| | | import MyField from "../components/MyField"; |
| | | import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import * as Common from '@/utils/common'; |
| | | import ImportButton from '../components/ImportButton' |
| | | import ImportButton from '../components/ImportButton'; |
| | | import { useCodeImport } from './useCodeImport'; |
| | | |
| | | // import * as importTemp from './importTemp.csv?raw'; |
| | | // const IMPORT_TEMP_URL = `data:text/csv;name=crm_contacts_sample.csv;charset=utf-8,${encodeURIComponent(importTemp.default)}`; |
| | | import PageDrawer from "../components/PageDrawer"; |
| | | import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import { useLocation } from 'react-router-dom'; |
| | | import DownloadIcon from '@mui/icons-material/Download'; |
| | | // 或者 import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; |
| | | |
| | | const IMPORT_TEMP_URL = '/imports/code_import_template.xlsx'; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | | '& .css-1vooibu-MuiSvgIcon-root': { |
| | | height: '.9em' |
| | | }, |
| | | '& .RaDatagrid-row': { |
| | | cursor: 'auto' |
| | | }, |
| | | '& .column-name': { |
| | | }, |
| | | '& .opt': { |
| | | width: 200 |
| | | }, |
| | | '& .css-1vooibu-MuiSvgIcon-root': { height: '.9em' }, |
| | | '& .RaDatagrid-row': { cursor: 'auto' }, |
| | | '& .opt': { width: 200 }, |
| | | })); |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <DateInput label='common.time.after' source="timeStart" alwaysOn />, |
| | | <DateInput label='common.time.before' source="timeEnd" alwaysOn />, |
| | | |
| | | <TextInput source="uuid" label="table.field.code.uuid" />, |
| | | <TextInput source="data" label="table.field.code.data" alwaysOn />, |
| | | <NumberInput source="x" label="table.field.code.x" />, |
| | | <NumberInput source="y" label="table.field.code.y" />, |
| | | <SelectInput source="corner" label="table.field.code.corner" |
| | | choices={[ |
| | | { id: '1', name: 'common.enums.true' }, |
| | | { id: '0', name: 'common.enums.false' }, |
| | | ]} |
| | | <SelectInput |
| | | source="corner" |
| | | label="table.field.code.corner" |
| | | choices={[{ id: '1', name: 'common.enums.true' }, { id: '0', name: 'common.enums.false' }]} |
| | | alwaysOn |
| | | />, |
| | | // <TextInput source="scale" label="table.field.code.scale" />, |
| | | // <SelectInput source="spin" label="table.field.code.spin" |
| | | // choices={[ |
| | | // { id: 0, name: 'page.code.enums.spin.na' }, |
| | | // { id: 1, name: 'page.code.enums.spin.cw' }, |
| | | // { id: 2, name: 'page.code.enums.spin.ccw' }, |
| | | // ]} |
| | | // alwaysOn |
| | | // emptyText={false} |
| | | // />, |
| | | <TextInput label="common.field.memo" source="memo" />, |
| | | <SelectInput |
| | | label="common.field.status" |
| | | source="status" |
| | | choices={[ |
| | | { id: '1', name: 'common.enums.statusTrue' }, |
| | | { id: '0', name: 'common.enums.statusFalse' }, |
| | | ]} |
| | | choices={[{ id: '1', name: 'common.enums.statusTrue' }, { id: '0', name: 'common.enums.statusFalse' }]} |
| | | />, |
| | | ] |
| | | ]; |
| | | |
| | | const CodeList = () => { |
| | | // ===================== 自定义导出按钮 ===================== |
| | | const CustomExportButton = () => { |
| | | const [open, setOpen] = useState(false); |
| | | const notify = useNotify(); |
| | | const translate = useTranslate(); |
| | | |
| | | // 直接从浏览器地址栏解析 filter 参数 |
| | | const getFilterFromUrl = () => { |
| | | const hash = window.location.hash; // 例如 "#/code?filter=%7B%22corner%22%3A%220%22%7D" |
| | | const match = hash.match(/[?&]filter=([^&]+)/); |
| | | if (match) { |
| | | try { |
| | | const filterJson = decodeURIComponent(match[1]); |
| | | return JSON.parse(filterJson); |
| | | } catch (e) { |
| | | console.error('解析 filter 失败', e); |
| | | return {}; |
| | | } |
| | | } |
| | | return {}; |
| | | }; |
| | | |
| | | const handleExport = async (useCurrentFilter) => { |
| | | try { |
| | | let filter = {}; |
| | | if (useCurrentFilter) { |
| | | filter = getFilterFromUrl(); |
| | | } |
| | | console.log('最终导出使用的 filter:', filter); |
| | | |
| | | const token = localStorage.getItem('Authorization'); |
| | | // 将 filter 转换为查询字符串,例如 corner=0 |
| | | const queryParams = new URLSearchParams(filter).toString(); |
| | | const url = `/api/code/export${queryParams ? '?' + queryParams : ''}`; |
| | | console.log('请求 URL:', url); |
| | | |
| | | const response = await fetch(url, { |
| | | method: 'POST', // 改回 POST(因为之前 POST 能返回 Excel) |
| | | headers: { |
| | | 'Authorization': `Bearer ${token}`, |
| | | 'Content-Type': 'application/json', |
| | | }, |
| | | // 尝试同时将 filter 以 JSON 格式放在 body 中(看后端是否读取) |
| | | body: JSON.stringify(filter), |
| | | }); |
| | | |
| | | if (!response.ok) throw new Error(`导出失败 ${response.status}`); |
| | | |
| | | // 检查响应类型,如果是 JSON 说明后端报错了 |
| | | const contentType = response.headers.get('content-type'); |
| | | if (contentType && contentType.includes('application/json')) { |
| | | const errorJson = await response.json(); |
| | | console.error('后端返回错误:', errorJson); |
| | | notify(`导出失败: ${errorJson.msg || '未知错误'}`, { type: 'error' }); |
| | | return; |
| | | } |
| | | |
| | | const blob = await response.blob(); |
| | | const link = document.createElement('a'); |
| | | link.href = URL.createObjectURL(blob); |
| | | link.download = `code_export_${new Date().toISOString().slice(0,19)}.xlsx`; |
| | | link.click(); |
| | | URL.revokeObjectURL(link.href); |
| | | notify('导出成功', { type: 'success' }); |
| | | } catch (error) { |
| | | console.error(error); |
| | | notify('导出失败', { type: 'error' }); |
| | | } |
| | | setOpen(false); |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | <Button |
| | | color="primary" |
| | | onClick={() => setOpen(true)} |
| | | startIcon={<DownloadIcon />} // 添加向下箭头图标 |
| | | size="small" // 与 ImportButton 保持一致的小尺寸 |
| | | sx={{ textTransform: 'none' }} // 可选:正常大小写,匹配导入按钮 |
| | | > |
| | | {translate('ra.action.export') || '导出'} |
| | | </Button> |
| | | <Dialog open={open} onClose={() => setOpen(false)}> |
| | | <DialogTitle>{translate('code.export.title',{_: '选择导出范围'})}</DialogTitle> |
| | | <DialogContent>{translate('code.export.message',{_: '请选择要导出的数据范围'}) }</DialogContent> |
| | | <DialogActions> |
| | | <Button onClick={() => handleExport(true)}> |
| | | {translate('code.export.current',{_: '导出当前筛选结果'}) } |
| | | </Button> |
| | | <Button onClick={() => handleExport(false)}> |
| | | {translate('code.export.all',{_: '导出全部数据' } )} |
| | | </Button> |
| | | <Button onClick={() => setOpen(false)}> |
| | | {translate('ra.action.cancel') || '取消'} |
| | | </Button> |
| | | </DialogActions> |
| | | </Dialog> |
| | | </> |
| | | ); |
| | | }; |
| | | // ======================================================= |
| | | const CodeList = () => { |
| | | const translate = useTranslate(); |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | |
| | |
| | | }), |
| | | marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0, |
| | | }} |
| | | title={"menu.code"} |
| | | empty={<EmptyData onClick={() => { setCreateDialog(true) }} />} |
| | | title="menu.code" |
| | | empty={<EmptyData onClick={() => setCreateDialog(true)} />} |
| | | filters={filters} |
| | | sort={{ field: "id", order: "asc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <MyCreateButton onClick={() => { setCreateDialog(true) }} /> |
| | | <MyCreateButton onClick={() => setCreateDialog(true)} /> |
| | | <SelectColumnsButton preferenceKey='code' /> |
| | | <ImportButton type="xlsx" importTemp={IMPORT_TEMP_URL} useImport={useCodeImport} onceBatch={10} /> |
| | | <MyExportButton /> |
| | | <CustomExportButton /> {/* 替换原有的导出按钮 */} |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | |
| | | <NumberField source="x" label="table.field.code.x" /> |
| | | <NumberField source="y" label="table.field.code.y" /> |
| | | <BooleanField source="cornerBool" label="table.field.code.corner" sortable={false} /> |
| | | {/* <TextField source="scale" label="table.field.code.scale" /> */} |
| | | {/* <FunctionField |
| | | label="table.field.code.spin" |
| | | sortable={false} |
| | | render={(record) => { |
| | | switch (record.spin) { |
| | | case 0: |
| | | return translate('page.code.enums.spin.na'); |
| | | case 1: |
| | | return translate('page.code.enums.spin.cw'); |
| | | case 2: |
| | | return translate('page.code.enums.spin.ccw'); |
| | | default: |
| | | return 'N/A'; |
| | | } |
| | | }} |
| | | /> */} |
| | | <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}> |
| | | <TextField source="nickname" /> |
| | | </ReferenceField> |
| | |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | | <CodeCreate |
| | | open={createDialog} |
| | | setOpen={setCreateDialog} |
| | | /> |
| | | <PageDrawer |
| | | title='Code Detail' |
| | | drawerVal={drawerVal} |
| | | setDrawerVal={setDrawerVal} |
| | | > |
| | | </PageDrawer> |
| | | <CodeCreate open={createDialog} setOpen={setCreateDialog} /> |
| | | <PageDrawer title='Code Detail' drawerVal={drawerVal} setDrawerVal={setDrawerVal} /> |
| | | </Box> |
| | | ) |
| | | } |
| | | ); |
| | | }; |
| | | |
| | | export default CodeList; |
| | |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.zy.acs.common.utils.GsonUtils; |
| | | import com.zy.acs.common.utils.QrCodeCodecSupport; |
| | |
| | | @PreAuthorize("hasAuthority('manager:code:list')") |
| | | @PostMapping("/code/export") |
| | | public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception { |
| | | ExcelUtil.build(ExcelUtil.create(codeService.list(), Code.class), response); |
| | | // 1. 提取筛选条件(兼容前端 { filter: {...} } 格式) |
| | | Map<String, Object> filter = map; |
| | | if (map != null && map.containsKey("filter")) { |
| | | Object filterObj = map.get("filter"); |
| | | if (filterObj instanceof Map) { |
| | | filter = (Map<String, Object>) filterObj; |
| | | } |
| | | } |
| | | |
| | | // 2. 构建查询条件(MyBatis-Plus) |
| | | QueryWrapper<Code> wrapper = new QueryWrapper<>(); |
| | | if (filter != null && !filter.isEmpty()) { |
| | | // 根据前端可能传递的字段添加条件(字段名需与数据库列或实体属性对应) |
| | | if (filter.containsKey("corner")) { |
| | | wrapper.eq("corner", filter.get("corner")); |
| | | } |
| | | if (filter.containsKey("condition")) { |
| | | String condition = (String) filter.get("condition"); |
| | | wrapper.and(w -> w.like("data", condition).or().like("uuid", condition)); |
| | | } |
| | | if (filter.containsKey("timeStart")) { |
| | | wrapper.ge("create_time", filter.get("timeStart")); |
| | | } |
| | | if (filter.containsKey("timeEnd")) { |
| | | wrapper.le("create_time", filter.get("timeEnd")); |
| | | } |
| | | if (filter.containsKey("x")) { |
| | | wrapper.eq("x", filter.get("x")); |
| | | } |
| | | if (filter.containsKey("y")) { |
| | | wrapper.eq("y", filter.get("y")); |
| | | } |
| | | if (filter.containsKey("memo")) { |
| | | wrapper.like("memo", filter.get("memo")); |
| | | } |
| | | if (filter.containsKey("status")) { |
| | | wrapper.eq("status", filter.get("status")); |
| | | } |
| | | // 还可以添加其他字段如 data, uuid 等 |
| | | } |
| | | |
| | | // 3. 查询符合条件的数据 |
| | | List<Code> list = codeService.list(wrapper); |
| | | |
| | | // 4. 导出 Excel |
| | | ExcelUtil.build(ExcelUtil.create(list, Code.class), response); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('manager:code:save')") |
| | |
| | | static-path-pattern: /** |
| | | datasource: |
| | | driver-class-name: com.mysql.jdbc.Driver |
| | | url: jdbc:mysql://localhost:3306/rcs_ctu_stable_hik?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai |
| | | url: jdbc:mysql://localhost:3306/rcs_ctu_stable_hik?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true |
| | | username: root |
| | | password: xltys1995 |
| | | password: Mysql20260414 |
| | | type: com.alibaba.druid.pool.DruidDataSource |
| | | druid: |
| | | initial-size: 5 |
| | |
| | | |
| | | redis: |
| | | host: localhost |
| | | password: xltys1995 |
| | | password: Redis20260417 |
| | | port: 6379 |
| | | max: 30 |
| | | min: 10 |