diff --git a/src/components/DataPreview/index.tsx b/src/components/DataPreview/index.tsx index 353536507a2fffe36f2b30c7c1fa605b212661b8..d3e8b2b4dc1876d6435ea1bd57d6ae2d0430fbb9 100644 --- a/src/components/DataPreview/index.tsx +++ b/src/components/DataPreview/index.tsx @@ -32,6 +32,8 @@ import { is } from '@/ts/base/common/lang/type'; import { AssignStatusType } from '@/ts/base/enum'; import AssignLayout from './assign/assignLayout'; import OrderViewer from './order'; +import WareHousing from '@/components/Space'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; const officeExt = ['.md', '.pdf', '.xls', '.xlsx', '.doc', '.docx', '.ppt', '.pptx']; const videoExt = ['.mp4', '.avi', '.mov', '.mpg', '.swf', '.flv', '.mpeg']; @@ -48,6 +50,7 @@ type EntityType = | IReception | IWorkDarft | IAssignTask + | IWareHousing | undefined; type ArgsType = { @@ -167,6 +170,15 @@ const DataPreview: React.FC<{ return ; } } + if (flag === 'space' && entity.typeName === '空间') { + return <>; + } + if (flag === 'space' && entity.typeName === '仓储空间') { + return renderEntityBody( + entity, + , + ); + } return renderEntityBody(entity); } return argsInfo?.empty ? ( diff --git a/src/components/DataPreview/layout/index.tsx b/src/components/DataPreview/layout/index.tsx index f128e47ea22c5ffc09ee4a21d178c223f5a7f0e2..27cc499d7b2e7bc2e1132b2d1955ee525baee4b0 100644 --- a/src/components/DataPreview/layout/index.tsx +++ b/src/components/DataPreview/layout/index.tsx @@ -21,7 +21,7 @@ interface IProps { number?: number; children?: React.ReactNode; // 子组件 onActionChanged?: (key: React.Key) => void; - helpDoc: FileItemShare[] + helpDoc: FileItemShare[]; } type ActionItemType = { label: string; @@ -88,51 +88,50 @@ const PreviewLayout: React.FC = (props) => { return ( <>
- { - const selected = action.key === props.selectKey; - return ( - { - if (props.onActionChanged) { - props.onActionChanged(action.key); - } - }}> - - - ); - })}> - - - {props.entity.name} {extraName} - - {nameNumber > 0 && ({nameNumber})} - - } - avatar={} - description={renderDesc()} - /> -
- {(props.helpDoc && props.helpDoc.length != 0 && - - - - )} -
-
+ {props.entity.typeName != '仓储空间' && ( + { + const selected = action.key === props.selectKey; + return ( + { + if (props.onActionChanged) { + props.onActionChanged(action.key); + } + }}> + + + ); + })}> + + + {props.entity.name} {extraName} + + {nameNumber > 0 && ({nameNumber})} + + } + avatar={} + description={renderDesc()} + /> +
+ {props.helpDoc && props.helpDoc.length != 0 && ( + + + + )} +
+
+ )}
{ + return { + key: target.abstractSpace.key, + item: target.abstractSpace, + label: target.name, + itemType: target.abstractSpace.typeName, + menus: loadFileMenus(target.abstractSpace), + tag: [target.typeName], + icon: , + children: children, + }; +}; + +/** 编译空间树 */ +const buildSpaceTree = ( + spaces: IAbstractSpace[], + typeNames: string[], +): MenuItemType[] => { + return spaces.map((space) => { + let children: MenuItemType[] = []; + if (space.typeName === '空间') { + children = buildSpaceTree(space.children, typeNames); + } + return { + key: space.key, + item: space, + label: space.name, + tag: [space.typeName], + icon: , + itemType: space.typeName, + menus: loadFileMenus(space), + children: children, + }; + }); +}; + +/** 获取个人菜单 */ +const getSpaceMenu = (directory: IDirectory, typeNames: string[]) => { + return createMenu( + directory.target, + buildSpaceTree(directory.target.abstractSpace.children, typeNames), + ); +}; + +/** 加载设置模块菜单 */ +export const loadSettingMenu = ( + directory: IDirectory, + typeNames?: string[] | undefined, +) => { + if (!typeNames) { + typeNames = ['人员', '单位', '空间', '仓储空间']; + } + const rootMenu: MenuItemType = getSpaceMenu(directory, typeNames); + return rootMenu; +}; diff --git a/src/components/OpenSpaceDialog/content/index.tsx b/src/components/OpenSpaceDialog/content/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7703fc7740a30ee1ba0e8cfa5e73a95ef5198b47 --- /dev/null +++ b/src/components/OpenSpaceDialog/content/index.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import DirectoryViewer from '@/components/Directory/views'; +import useCtrlUpdate from '@/hooks/useCtrlUpdate'; +import { IAbstractSpace } from '@/ts/core'; +import { loadFileMenus } from '@/executor/fileOperate'; +import { command } from '@/ts/base'; +import orgCtrl from '@/ts/controller'; +import useAsyncLoad from '@/hooks/useAsyncLoad'; +import { Spin } from 'antd'; +import { cleanMenus } from '@/utils/tools'; +import { ISpace } from '@/ts/core/abstractSpace/abstractSpaceInfo'; + +interface IProps { + accepts?: string[]; + selects?: ISpace[]; + excludeIds?: string[]; + current: IAbstractSpace | 'disk'; + spaceContents?: ISpace[]; + onFocused?: (file: ISpace | undefined) => void; + onSelected?: (files: ISpace[]) => void; + showFile?: boolean; +} +/** + * 空间 + */ +const Space: React.FC = (props) => { + if (!props.current) return <>; + const [space] = useState( + props.current === 'disk' ? orgCtrl.user.abstractSpace : props.current, + ); + const [key] = useCtrlUpdate(space); + const [currentTag, setCurrentTag] = useState('全部'); + const [loaded] = useAsyncLoad(() => space.loadContent(false)); + const [focusFile, setFocusFile] = useState(); + useEffect(() => { + command.emitter('preview', 'dialog', focusFile); + }, [focusFile]); + + const contextMenu = (file?: ISpace) => { + const entity = file ?? space; + return { + items: cleanMenus(loadFileMenus(entity)) || [], + onClick: ({ key }: { key: string }) => { + command.emitter('executor', key, entity, space.key); + }, + }; + }; + + const selectHanlder = (file: ISpace, selected: boolean) => { + if (props.selects && props.onSelected) { + if (selected) { + props.onSelected([...props.selects, file]); + } else { + props.onSelected(props.selects.filter((i) => i.key !== file.key)); + } + } + }; + + const fileFocused = (file: ISpace | undefined) => { + if (file) { + if (focusFile && file.key === focusFile.key) { + return true; + } + return props.selects?.find((i) => i.key === file.key) !== undefined; + } + return false; + }; + + const clickHanlder = (file: ISpace | undefined) => { + // if(file.tag) + const focused = fileFocused(file); + if (focused) { + setFocusFile(undefined); + props.onFocused?.apply(this, [undefined]); + } else { + setFocusFile(file); + props.onFocused?.apply(this, [file]); + } + if (file && props.onSelected) { + selectHanlder(file, !focused); + } + }; + + const getContent = () => { + const contents: ISpace[] = []; + if (props.current === 'disk') { + contents.push( + orgCtrl.user.abstractSpace, + ...orgCtrl.user.companys.map((i) => i.abstractSpace), + ); + } else { + contents.push(...props.current!.content(props.showFile)); + } + return contents; + }; + + return ( + + setCurrentTag(t)} + fileOpen={(entity) => clickHanlder(entity as ISpace)} + contextMenu={(entity) => contextMenu(entity as ISpace)} + /> + + ); +}; +export default Space; diff --git a/src/components/OpenSpaceDialog/index.tsx b/src/components/OpenSpaceDialog/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d2817c3fa67aa8d3ab422728038ddce385bf641a --- /dev/null +++ b/src/components/OpenSpaceDialog/index.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import MainLayout from '../MainLayout'; +import Content from './content'; +import useMenuUpdate from '@/hooks/useMenuUpdate'; +import FullScreenModal from '../Common/fullScreen'; +import { Button, Divider, Space } from 'antd'; +import orgCtrl, { Controller } from '@/ts/controller'; +import { MenuItemType } from 'typings/globelType'; +import { ISpace } from '@/ts/core/abstractSpace/abstractSpaceInfo'; +import { IDirectory } from '@/ts/core'; +import { loadSettingMenu } from './config'; + +export interface ISpaceDialogProps { + directory: IDirectory; + typeNames?: string[]; + title?: string; + accepts: string[]; + spaceContents?: ISpace[]; + onOk: (files: ISpace[]) => void; + onCancel: () => void; + leftShow?: boolean; + rightShow?: boolean; + showFile?: boolean; + multiple?: boolean; + maxCount?: number; + excludeIds?: string[]; + onSelectMenuChanged?: (menu: MenuItemType) => void; +} + +const OpenSpaceDialog: React.FC = (props) => { + const [selectedSpaces, setSelectedSpaces] = useState([]); + const [key, rootMenu, selectMenu, setSelectMenu] = useMenuUpdate(() => { + return loadSettingMenu(props.directory); + }, new Controller(orgCtrl.currentKey)); + if (!selectMenu || !rootMenu) return <>; + return ( + { + props.onCancel(); + setSelectedSpaces([]); + }} + destroyOnClose + width={'80vw'} + bodyHeight={'70vh'} + footer={ + } wrap size={2}> + + + }> + { + setSelectMenu(data); + props.onSelectMenuChanged?.apply(this, [data]); + }} + siderMenuData={rootMenu}> + { + if (!props.multiple) { + if (space) { + setSelectedSpaces([space]); + } else { + setSelectedSpaces([]); + } + } + }} + onSelected={(spaces) => { + if (props.multiple) { + if (props.maxCount && spaces.length > props.maxCount) { + setSelectedSpaces(spaces.slice(-props.maxCount)); + } else { + setSelectedSpaces(spaces); + } + } + }} + /> + + + ); +}; + +export default OpenSpaceDialog; diff --git a/src/components/Space/components/canvas.tsx b/src/components/Space/components/canvas.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2f71def4d1d14309580314fdbd11f201bc510563 --- /dev/null +++ b/src/components/Space/components/canvas.tsx @@ -0,0 +1,104 @@ +import React, { useState, useRef } from 'react'; +import style from '@/components/Space/less/index.module.less'; +import { XGoodsShelves, XWareHousingSpace } from '@/ts/base/schema'; +interface IProps { + warehouse: XWareHousingSpace; + scale: number; + renderShelf: (shelf: XGoodsShelves) => JSX.Element; + addNewShelf: (e: React.MouseEvent) => void; + activeTool: string; +} +// 画布组件 +const Canvas: React.FC = ({ + warehouse, + scale, + renderShelf, + addNewShelf, + activeTool, +}) => { + // 拖拽状态管理 + const [isDragging, setIsDragging] = useState(false); + const [panOffset, setPanOffset] = useState({ x: 0, y: 0 }); + const dragStartRef = useRef({ x: 0, y: 0 }); + const containerRef = useRef(null); + const canvasRef = useRef(null); + + // 计算网格尺寸(随缩放比例变化) + const gridSize = 50 * scale; + + // 处理鼠标按下事件(开始拖拽) + const handleMouseDown = (e: React.MouseEvent) => { + if (activeTool === 'hand' && e.button === 0) { + // 仅左键拖拽 + setIsDragging(true); + dragStartRef.current = { + x: e.clientX - panOffset.x, + y: e.clientY - panOffset.y, + }; + e.preventDefault(); // 防止文本选中 + } + }; + + // 处理鼠标移动事件(拖拽中) + const handleMouseMove = (e: React.MouseEvent) => { + if (isDragging) { + const newX = e.clientX - dragStartRef.current.x; + const newY = e.clientY - dragStartRef.current.y; + + setPanOffset({ + x: newX, + y: newY, + }); + } + }; + + // 处理鼠标释放事件(结束拖拽) + const handleMouseUp = () => { + setIsDragging(false); + }; + + // 处理点击事件(添加货架) + const handleClick = (e: React.MouseEvent) => { + // 拖拽操作时不添加货架 + if (!isDragging && activeTool === 'shelf') { + addNewShelf(e); + } + }; + + // 动态光标样式 + const getCursorStyle = () => { + if (isDragging) return 'grabbing'; + if (activeTool === 'shelf') return 'crosshair'; + if (activeTool === 'hand') return 'grab'; + return 'default'; + }; + + return ( +
+
+ {warehouse?.goodsShelves?.map(renderShelf)} +
+
+ ); +}; + +export default Canvas; diff --git a/src/components/Space/components/header.tsx b/src/components/Space/components/header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..795637783bf8815d310a6f2428389c19a1a9d6d5 --- /dev/null +++ b/src/components/Space/components/header.tsx @@ -0,0 +1,69 @@ +import { Tool } from '@/components/Space/tool'; +import { FaWarehouse } from 'react-icons/fa'; +import React from 'react'; +import style from '@/components/Space/less/index.module.less'; +import { XWareHousingSpace } from '@/ts/base/schema'; + +interface IProps { + tools: Tool[]; + activeTool: string; + handleToolSelect: (toolId: string) => void; + warehouse: XWareHousingSpace; + //inventory: ReturnType; + //saveWarehouse: () => void; + scale: number; +} +const Header: React.FC = ({ + tools, + activeTool, + handleToolSelect, + warehouse, + //inventory, + //saveWarehouse, + scale, +}) => { + return ( +
+
+
+ +

{warehouse.name}

+
+ +
+ {tools.map((tool) => ( +
handleToolSelect(tool.id)} + title={tool.name}> + {tool.icon} +
+ ))} +
+
+ +
+
+ 缩放: {(scale * 100).toFixed(0)}% +
+
+ 货架: {warehouse.goodsShelves?.length} +
+ {/*
*/} + {/* 库位: {inventory.totalSlots}*/} + {/*
*/} +
+ + {/*
*/} + {/* */} + {/*
*/} +
+ ); +}; + +export default Header; diff --git a/src/components/Space/components/rightPanel.tsx b/src/components/Space/components/rightPanel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..127b78d52dcf5418a46404c2c75311fa0bd4cb43 --- /dev/null +++ b/src/components/Space/components/rightPanel.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import style from '@/components/Space/less/index.module.less'; +import ShelfProperties from './shelfProperties'; +import WarehouseOverview from './warehouseOverview'; +import { XGoodsShelves } from '@/ts/base/schema'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; +import { IGoodsShelvesSlot } from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; +interface IProps { + selectedShelf: XGoodsShelves | undefined; + updateShelf: (shelfId: string, updates: Partial) => void; + slots: IGoodsShelvesSlot[]; + warehouse: IWareHousing; + //inventory: ReturnType; +} +// 右侧面板组件 +const RightPanel: React.FC = ({ + selectedShelf, + updateShelf, + slots, + warehouse, + //inventory, +}) => { + return ( +
+ {selectedShelf ? ( + + ) : ( + //inventory={inventory} /> + )} +
+ ); +}; + +export default RightPanel; diff --git a/src/components/Space/components/shelfProperties.tsx b/src/components/Space/components/shelfProperties.tsx new file mode 100644 index 0000000000000000000000000000000000000000..62158bab7cc8be5641f5fdac9e59c442897d6692 --- /dev/null +++ b/src/components/Space/components/shelfProperties.tsx @@ -0,0 +1,267 @@ +import React, { useState, useEffect } from 'react'; +import style from '@/components/Space/less/index.module.less'; +import { XGoodsShelves } from '@/ts/base/schema'; +import SlotGridView from '@/components/Space/components/slotGridView'; +import { Button } from 'antd'; +import { FaEdit, FaSave } from 'react-icons/fa'; +import SlotEditModal from '@/components/Space/components/soleEditModal'; +import { IGoodsShelvesSlot } from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; + +interface IProps { + slots: IGoodsShelvesSlot[]; + shelf: XGoodsShelves; + updateShelf: (shelfId: string, updates: Partial) => void; +} + +// 货架属性组件 +const ShelfProperties: React.FC = ({ slots, shelf, updateShelf }) => { + const [selectedSlot, setSelectedSlot] = useState(null); + const [formData, setFormData] = useState({ + name: shelf.name, + code: shelf.code, + positionX: shelf.position[0], + positionY: shelf.position[1], + width: shelf.width, + length: shelf.length, + rows: shelf.rows, + cols: shelf.cols, + }); + const [isEditing, setIsEditing] = useState(false); + + // 当货架属性变化时更新表单数据 + useEffect(() => { + setFormData({ + name: shelf.name, + code: shelf.code, + positionX: shelf.position[0], + positionY: shelf.position[1], + width: shelf.width, + length: shelf.length, + rows: shelf.rows, + cols: shelf.cols, + }); + }, [shelf]); + + // 处理表单字段变化 + const handleInputChange = (field: keyof typeof formData, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // 保存所有货架信息 + const saveShelfInfo = () => { + updateShelf(shelf.id, { + name: formData.name, + code: formData.code, + position: [formData.positionX, formData.positionY], + width: formData.width, + length: formData.length, + rows: formData.rows, + cols: formData.cols, + }); + setIsEditing(false); + }; + + // 处理库位点击 + const handleSlotClick = (slot: IGoodsShelvesSlot) => { + setSelectedSlot(slot); + }; + + // 取消编辑 + const cancelEdit = () => { + setFormData({ + name: shelf.name, + code: shelf.code, + positionX: shelf.position[0], + positionY: shelf.position[1], + width: shelf.width, + length: shelf.length, + rows: shelf.rows, + cols: shelf.cols, + }); + setIsEditing(false); + }; + + return ( +
+ {selectedSlot && ( + setSelectedSlot(null)} + /> + )} + +
+
+

货架信息

+ {isEditing ? ( +
+ + +
+ ) : ( + + )} +
+ +
+ + {isEditing ? ( +
+ handleInputChange('name', e.target.value)} + />{' '} +
+ ) : ( +
{shelf.name}
+ )} +
+ +
+ + {isEditing ? ( +
+ handleInputChange('code', e.target.value)} + />{' '} +
+ ) : ( +
{shelf.code}
+ )} +
+ +
+ + {isEditing ? ( +
+ + handleInputChange('positionX', parseFloat(e.target.value) || 0) + } + /> + + handleInputChange('positionY', parseFloat(e.target.value) || 0) + } + /> +
+ ) : ( +
+ {shelf.position[0].toFixed(2)}, {shelf.position[1].toFixed(2)} +
+ )} +
+ +
+ + {isEditing ? ( +
+ + handleInputChange('length', parseFloat(e.target.value) || 1) + } + /> + + handleInputChange('width', parseFloat(e.target.value) || 1) + } + /> +
+ ) : ( +
+ {shelf.length.toFixed(2)}× {shelf.width.toFixed(2)} +
+ )} +
+ +
+ + {isEditing ? ( +
+ handleInputChange('rows', parseInt(e.target.value) || 0)} + /> + handleInputChange('cols', parseInt(e.target.value) || 0)} + /> +
+ ) : ( +
+ {shelf.rows} × {shelf.cols} +
+ )} +
+
+ +
+

库位分布图

+
+ +
+
+
+ 空闲 +
+
+
+ 使用中 +
+
+
+ 警告 +
+
+
+ 已满 +
+
+
+
+
+ ); +}; + +export default ShelfProperties; diff --git a/src/components/Space/components/slotGridView.tsx b/src/components/Space/components/slotGridView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9e82a8a5b39a55ef72ab6ac6f3176f1a6553bc5e --- /dev/null +++ b/src/components/Space/components/slotGridView.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import style from '@/components/Space/less/index.module.less'; +import { XGoodsShelves } from '@/ts/base/schema'; +import { IGoodsShelvesSlot } from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; +interface IProps { + shelf: XGoodsShelves; + slots: IGoodsShelvesSlot[]; + onSlotClick: (slot: IGoodsShelvesSlot) => void; +} +// 库位二维网格视图组件 +const SlotGridView: React.FC = ({ shelf, slots, onSlotClick }) => { + // 将一维数组转换为二维数组,并按位置排序 + const grid: IGoodsShelvesSlot[][] = []; + const slotsWithPosition = slots.filter((slot) => slot.metadata.position); + const sortedSlots = [...slotsWithPosition].sort((a, b) => { + // 优先按位置排序 [行, 列] + const posA = a.metadata.position || [0, 0]; + const posB = b.metadata.position || [0, 0]; + + // 先比较行位置 + if (posA[0] !== posB[0]) { + return posA[0] - posB[0]; + } + // 行相同再比较列位置 + return posA[1] - posB[1]; + }); + + // 创建二维网格 + for (let row = 0; row < shelf.rows; row++) { + grid[row] = []; + + // 找出当前行的所有库位 + const rowSlots = sortedSlots.filter((slot) => slot.metadata.position?.[0] === row); + + // 按列位置排序 + rowSlots.sort( + (a, b) => (a.metadata.position?.[1] || 0) - (b.metadata.position?.[1] || 0), + ); + + // 填充当前行 + for (let col = 0; col < shelf.cols; col++) { + if (col < rowSlots.length) { + grid[row][col] = rowSlots[col]; + } + } + } + // const grid: XGoodsShelvesSlot[][] = []; + // for (let row = 0; row < shelf.rows; row++) { + // const rowSlots: XGoodsShelvesSlot[] = []; + // for (let col = 0; col < shelf.cols; col++) { + // const index = row * shelf.cols + col; + // if (index < slots.length) { + // rowSlots.push(slots[index].metadata); + // } + // } + // grid.push(rowSlots); + // } + + return ( +
+ {grid.map((row, rowIndex) => ( +
+ {row.map((slot) => { + const utilization = + slot.metadata.capacity > 0 + ? Math.round( + ((slot.metadata?.inventory?.length ?? 0) / slot.metadata.capacity) * + 100, + ) + : 0; + + let bgColor = '#e8f5e9'; // 空闲 + if (utilization > 90) bgColor = '#ffcdd2'; // 已满 + else if (utilization > 70) bgColor = '#ffe0b2'; // 警告 + else if (utilization > 0) bgColor = '#c8e6c9'; // 使用中 + + const isEmpty = slot.metadata.capacity === 0; + + return ( +
!isEmpty && onSlotClick(slot)}> + {!isEmpty && ( +
+
{slot.metadata.name}
+
{utilization}%
+
+ )} +
+ ); + })} +
+ ))} +
+ ); +}; + +export default SlotGridView; diff --git a/src/components/Space/components/soleEditModal.tsx b/src/components/Space/components/soleEditModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2117655f6f37dc1eadfac4723b6b569bbf78a1f7 --- /dev/null +++ b/src/components/Space/components/soleEditModal.tsx @@ -0,0 +1,68 @@ +import React, { useRef } from 'react'; +import { ProFormColumnsType, ProFormInstance } from '@ant-design/pro-components'; +import SchemaForm from '@/components/SchemaForm'; +import { schema } from '@/ts/base'; +import { IGoodsShelvesSlot } from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; + +interface Iprops { + slot: IGoodsShelvesSlot; + onClose: () => void; +} +/* + 编辑 +*/ +const SlotEditModal = (props: Iprops) => { + let title = '库位编辑'; + const formRef = useRef(); + const columns: ProFormColumnsType[] = [ + { + title: '名称', + dataIndex: 'name', + formItemProps: { + rules: [{ required: true, message: '名称为必填项' }], + }, + }, + { + title: '代码', + dataIndex: 'code', + formItemProps: { + rules: [{ required: true, message: '代码为必填项' }], + }, + }, + { + title: '容量', + dataIndex: 'capacity', + valueType: 'digit', + fieldProps: { + min: 0, + }, + formItemProps: { + rules: [{ required: true, message: 'capacity为必填项' }], + }, + }, + ]; + return ( + + formRef={formRef} + open + title={title} + width={640} + columns={columns} + initialValues={props.slot.metadata} + rowProps={{ + gutter: [24, 0], + }} + onOpenChange={(open: boolean) => { + if (!open) { + props.onClose(); + } + }} + layoutType="ModalForm" + onFinish={async (values) => { + props.slot.update(values); + props.onClose(); + }}> + ); +}; + +export default SlotEditModal; diff --git a/src/components/Space/components/warehouseOverview.tsx b/src/components/Space/components/warehouseOverview.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e25436f57f1c248f0ec87889034c452cb0e4caf6 --- /dev/null +++ b/src/components/Space/components/warehouseOverview.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import style from '@/components/Space/less/index.module.less'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; + +interface IProps { + warehouse: IWareHousing; +} +// 仓库概览组件 +const WarehouseOverview: React.FC = ({ + warehouse, +}) => { + return ( + <> +
+

仓库概览

+ +
+
+
仓库名称
+
{warehouse.metadata.name}
+
+
+
仓库地址
+
{warehouse.metadata.address}
+
+
+
仓库尺寸
+
+ {warehouse.metadata.length}m × {warehouse.metadata.width}m +
+
+
+
货架数量
+
{warehouse.shelves?.length}
+
+
+
库位总容量
+
{warehouse.totalSlotsCapacity}
+
+
+
库位使用容量
+
+ {warehouse.usedSlotsCapacity}/{warehouse.totalSlotsCapacity} +
+
+
+
库位使用率
+
{warehouse.utilization}%
+
+
+
90 + ? '#f44336' + : warehouse.utilization > 70 + ? '#ff9800' + : '#4caf50', + }}> + {warehouse.utilization >= 10 ? `${warehouse.utilization}%` : ''} +
+
+
+
+ + ); +}; + +export default WarehouseOverview; diff --git a/src/components/Space/index.tsx b/src/components/Space/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b96b822d9483d6e9545dd99d67db6ec0daa6f31f --- /dev/null +++ b/src/components/Space/index.tsx @@ -0,0 +1,410 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + FaMousePointer, + FaHandPaper, + FaTrashAlt, + FaBoxes, + FaPlus, + FaMinus, +} from 'react-icons/fa'; +import style from './less/index.module.less'; // 导入样式模块 +import Header from './components/header'; +import Canvas from './components/canvas'; +import RightPanel from './components/rightPanel'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; +import { Tool } from './tool'; +import { XGoodsShelves, XGoodsShelvesSlot } from '@/ts/base/schema'; +import { logger } from '@/ts/base/common'; +import { ISpace } from '@/ts/core/abstractSpace/abstractSpaceInfo'; +import { IGoodsShelvesSlot } from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; + +interface IProps { + root: IWareHousing; +} + +// 主组件 +const WareHousing: React.FC = ({ root }) => { + const PIXELS_PER_METER = 10; + const [warehouse] = useState(root); + const [selectedShelf, setSelectedShelf] = useState( + undefined, + ); + const [content, setContent] = useState(warehouse.shelvesSlots); + const [activeTool, setActiveTool] = useState('select'); + const [scale, setScale] = useState(0.8); + const [draggingShelf, setDraggingShelf] = useState(undefined); + const dragDataRef = useRef({ + startX: 0, + startY: 0, + shelfElement: null as HTMLDivElement | null, + shelfStartPos: [0, 0] as [number, number], + isClick: true, // 新增:用于区分点击和拖拽 + }); + + useEffect(() => { + const id = warehouse.subscribe(() => { + loadContent(warehouse, true); + }); + return () => { + warehouse.unsubscribe(id); + }; + }, [warehouse]); + + // 工具列表 + const tools: Tool[] = [ + { id: 'select', name: '选择工具', icon: }, + { id: 'hand', name: '平移工具', icon: }, + { id: 'shelf', name: '添加货架', icon: }, + { id: 'delete', name: '删除', icon: }, + { id: 'zoom-in', name: '放大', icon: }, + { id: 'zoom-out', name: '缩小', icon: }, + ]; + + const getSlots = (id?: string | undefined): IGoodsShelvesSlot[] => { + return content.filter( + (item: IGoodsShelvesSlot) => item.metadata?.goodsShelveId === id, + ); + }; + + /** 加载目录内容 */ + const loadContent = (file: ISpace, reload: boolean) => { + file.loadContent(reload).then(async () => { + setContent(warehouse.shelvesSlots); + }); + }; + + // 处理工具选择 + const handleToolSelect = (toolId: string) => { + setActiveTool(toolId); + if (toolId === 'zoom-in') { + setScale((prev) => Math.min(prev * 1.2, 1.5)); + } else if (toolId === 'zoom-out') { + setScale((prev) => Math.max(prev / 1.2, 0.5)); + } else if (toolId === 'delete') { + if (!selectedShelf) return; + root.deleteGoodsShelves(selectedShelf).then((res) => { + if (res) { + logger.info(`${selectedShelf.name}删除成功。`); + setSelectedShelf(undefined); + setActiveTool('select'); + } else { + logger.error(`${selectedShelf.name}删除失败。`); + } + }); + } else { + setActiveTool(toolId); + } + }; + + // 添加新货架 + const addNewShelf = (e: React.MouseEvent) => { + if (activeTool !== 'shelf') return; + if (!e.currentTarget) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const x = (e.clientX - rect.left) / (PIXELS_PER_METER * scale) - 1; + const y = (e.clientY - rect.top) / (PIXELS_PER_METER * scale) - 1; + + const newShelf: XGoodsShelves = { + id: 'snowId()', + name: `货架 ${(warehouse.shelves?.length || 0) + 1}`, + code: `SHELF-${(warehouse.shelves?.length || 0) + 1}`, + width: 10, + length: 5, + rows: 0, + cols: 0, + position: [x, y], + }; + root.addGoodsShelves(newShelf).then((res) => { + if (res) { + logger.info(`${newShelf.name}添加成功。`); + } else { + logger.error(`${newShelf.name}添加失败。`); + } + setActiveTool('select'); + }); + }; + + // 更新货架属性 + const updateShelf = (shelfId: string, updates: Partial) => { + if (selectedShelf && selectedShelf.id === shelfId) { + root + .updateGoodsShelves({ ...selectedShelf, ...updates }) + .then((res) => { + if (res) { + logger.info(`${selectedShelf.name}更新成功。`); + updateSlots(shelfId, updates); + setSelectedShelf({ ...selectedShelf, ...updates }); + } else { + logger.error(`${selectedShelf.name}更新失败。`); + } + }) + .catch((e) => { + console.log(e); + }); + } + }; + + // 更新库位信息 + const updateSlots = (shelfId: string, updates: Partial) => { + if (selectedShelf && selectedShelf.id === shelfId) { + // 计算新的行列数 + const newRows = updates.rows ?? selectedShelf.rows; + const newCols = updates.cols ?? selectedShelf.cols; + + // 获取当前所有库位 + const currentSlots = getSlots(selectedShelf.id); + + // 创建需要添加的新库位数组 + const slotsToCreate: XGoodsShelvesSlot[] = []; + // 创建需要删除的库位数组 + const slotsToDelete: IGoodsShelvesSlot[] = []; + + // 1. 找出需要删除的库位 + for (let i = 0; i < currentSlots.length; i++) { + const slot = currentSlots[i]; + const row = Math.floor(i / selectedShelf.cols); + const col = i % selectedShelf.cols; + + // 如果库位在新网格范围外,标记为需要删除 + if (row >= newRows || col >= newCols) { + slotsToDelete.push(slot); + } + } + + // 2. 找出需要创建的新库位 + for (let row = 0; row < newRows; row++) { + for (let col = 0; col < newCols; col++) { + const oldIndex = row * selectedShelf.cols + col; + + // 检查位置是否超出原网格范围 + const isNewPosition = row >= selectedShelf.rows || col >= selectedShelf.cols; + + // 如果位置超出原网格范围,或者原位置没有库位 + if (isNewPosition || oldIndex >= currentSlots.length) { + slotsToCreate.push({ + id: 'snowId()', + name: `库位 ${row + 1}-${col + 1}`, + code: `SLOT-${selectedShelf.code}-${row + 1}-${col + 1}`, + goodsShelveId: shelfId, + position: [row, col], + width: 1.0, + height: 0.8, + capacity: 100, + inventory: [], + } as unknown as XGoodsShelvesSlot); + } + } + } + + // 执行删除操作 + if (slotsToDelete.length > 0) { + for (const slot of slotsToDelete) { + slot.delete(); + } + } + + // 执行创建操作 + if (slotsToCreate.length > 0) { + root.addGoodsShelvesSlot(slotsToCreate).then((results) => { + if (results) { + //logger.info(`${slotsToCreate.length}个新库位创建成功`); + } else { + //logger.error('部分新库位创建失败'); + } + }); + } + } + }; + + // 处理货架拖拽开始 + const handleShelfDragStart = (shelfId: string, e: React.MouseEvent) => { + e.stopPropagation(); + if (activeTool !== 'select') return; + + const shelf = warehouse.shelves.find((s) => s.id === shelfId); + if (!shelf) return; + + dragDataRef.current = { + startX: e.clientX, + startY: e.clientY, + shelfElement: e.currentTarget as HTMLDivElement, + shelfStartPos: [...shelf.position] as [number, number], + isClick: true, // 初始假设是点击 + }; + + setDraggingShelf(shelfId); + setSelectedShelf(shelf); + }; + + // 处理货架拖拽 + const handleShelfDrag = (e: MouseEvent) => { + if (!draggingShelf || !dragDataRef.current.shelfElement) return; + + const { startX, startY, shelfElement, shelfStartPos } = dragDataRef.current; + + // 计算移动距离 + const deltaX = Math.abs(e.clientX - startX); + const deltaY = Math.abs(e.clientY - startY); + + // 如果移动距离超过阈值,则视为拖拽 + if (deltaX > 0 || deltaY > 0) { + dragDataRef.current.isClick = false; + } + + // 只有确定为拖拽时才更新位置 + if (!dragDataRef.current.isClick) { + const moveDeltaX = (e.clientX - startX) / (PIXELS_PER_METER * scale); + const moveDeltaY = (e.clientY - startY) / (PIXELS_PER_METER * scale); + + const newX = Math.max(0, shelfStartPos[0] + moveDeltaX); + const newY = Math.max(0, shelfStartPos[1] + moveDeltaY); + + // 限制货架在仓库范围内 + const maxX = + warehouse.metadata.width - + parseFloat(shelfElement.style.width) / (PIXELS_PER_METER * scale); + const maxY = + warehouse.metadata.length - + parseFloat(shelfElement.style.height) / (PIXELS_PER_METER * scale); + const clampedX = Math.min(newX, maxX); + const clampedY = Math.min(newY, maxY); + + // 直接更新元素位置,避免重新渲染 + shelfElement.style.left = `${clampedX * PIXELS_PER_METER * scale}px`; + shelfElement.style.top = `${clampedY * PIXELS_PER_METER * scale}px`; + } + }; + + // 处理货架拖拽结束 + const handleShelfDragEnd = () => { + if (!draggingShelf || !dragDataRef.current.shelfElement) return; + + const { isClick, shelfElement } = dragDataRef.current; + + if (!isClick) { + // 只有确定为拖拽时才更新位置 + const left = parseFloat(shelfElement.style.left); + const top = parseFloat(shelfElement.style.top); + + // 转换为仓库坐标(米) + const x = left / (PIXELS_PER_METER * scale); + const y = top / (PIXELS_PER_METER * scale); + // 更新状态 + updateShelf(draggingShelf, { position: [x, y] }); + } else { + // 如果是点击,已经通过点击事件处理了选中状态 + // 这里可以添加点击时的其他逻辑 + } + + setDraggingShelf(undefined); + }; + + // 全局鼠标事件监听 + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + handleShelfDrag(e); + }; + + const handleMouseUp = () => { + handleShelfDragEnd(); + }; + + if (draggingShelf) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [draggingShelf, scale, warehouse]); + + // 渲染货架 + const renderShelf = (shelf: XGoodsShelves) => { + const isSelected = selectedShelf?.id === shelf.id; + const isDragging = draggingShelf === shelf.id; + const shelfX = shelf.position[0] * PIXELS_PER_METER * scale; + const shelfY = shelf.position[1] * PIXELS_PER_METER * scale; + const shelfWidth = shelf.width * PIXELS_PER_METER * scale; + const shelfLength = shelf.length * PIXELS_PER_METER * scale; + const slots = getSlots(shelf.id); + const totalSlotsCapacity = slots.reduce( + (acc, slot) => acc + slot.metadata.capacity, + 0, + ); + const usedSlotsCapacity = slots.reduce( + (acc, slot) => acc + (slot.metadata?.inventory?.length ?? 0), + 0, + ); + const utilization = + totalSlotsCapacity > 0 + ? Number(((usedSlotsCapacity / totalSlotsCapacity) * 100).toFixed(2)) + : 0; + // 根据利用率设置背景颜色 + let bgColor = '#e8f5e9'; // 空闲 (默认) + if (utilization > 90) bgColor = '#ffcdd2'; // 已满 (红色) + else if (utilization > 70) bgColor = '#ffe0b2'; // 警告 (橙色) + else if (utilization > 0) bgColor = '#c8e6c9'; // 使用中 (绿色) + let borderColor = '#888'; + if (isSelected) borderColor = '#2196f3'; + else borderColor = '#4caf50'; + + return ( +
handleShelfDragStart(shelf.id, e)}> +
{shelf.name}
+
+ ); + }; + + return ( +
+
+ +
+ setSelectedShelf(null)} + /> + + +
+
+ ); +}; + +export default WareHousing; diff --git a/src/components/Space/less/index.module.less b/src/components/Space/less/index.module.less new file mode 100644 index 0000000000000000000000000000000000000000..2c9116be2da330e7dc4b5e77bd9ba2d90e502796 --- /dev/null +++ b/src/components/Space/less/index.module.less @@ -0,0 +1,794 @@ +@primary: #2c3e50; +@secondary: #3498db; +@accent: #e74c3c; +@light: #ecf0f1; +@dark: #34495e; +@success: #2ecc71; +@warning: #f39c12; +@grid_color: rgba(44, 62, 80, 0.08); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body, html { + height: 100%; + overflow: hidden; +} + +.warehouse_editor { + --primary: @primary; + --secondary: @secondary; + --accent: @accent; + --light: @light; + --dark: @dark; + --success: @success; + --warning: @warning; + --grid_color: @grid_color; + + display: flex; + flex-direction: column; + height: 100vh; + background-color: #f5f7fa; + color: var(--dark); + overflow: hidden; + + .header { + background-color: white; + padding: 10px 15px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + z-index: 10; + flex-wrap: wrap; + gap: 10px; + + .header_left { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + + .logo { + display: flex; + align-items: center; + gap: 8px; + color: var(--primary); + + .logo_icon { + font-size: 20px; + color: var(--secondary); + } + } + } + + h1 { + font-size: 18px; + font-weight: 600; + } + + .actions { + display: flex; + gap: 8px; + } + + .btn { + padding: 5px 10px; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + border: none; + display: flex; + align-items: center; + gap: 5px; + font-size: 12px; + + &_outline { + background-color: transparent; + border: 1px solid var(--secondary); + color: var(--secondary); + + &:hover { + background-color: rgba(52, 152, 219, 0.1); + } + } + + &_success { + background-color: var(--success); + color: white; + + &:hover { + background-color: #27ae60; + } + } + } + + .tools_container { + display: flex; + gap: 6px; + margin-left: 10px; + + .tool_btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + background-color: #f1f5f9; + cursor: pointer; + border: 1px solid #e0e4e8; + transition: all 0.2s ease; + font-size: 12px; + + &:hover, &.active { + background-color: #e3e9f1; + border-color: var(--secondary); + } + + &.active { + background-color: var(--secondary); + color: white; + } + + &.batch_btn { + background-color: #ff9800; + color: white; + + &:hover { + background-color: #e68a00; + } + } + } + } + + .top_status_bar { + display: flex; + align-items: center; + gap: 15px; + margin-left: auto; + font-size: 12px; + + .status_item { + display: flex; + align-items: center; + gap: 4px; + color: #555; + } + } + } + + .main_content { + display: flex; + flex: 1; + overflow: hidden; + position: relative; + + .canvas_container { + flex: 1; + background-color: #f9fafb; + position: relative; + overflow: hidden; + background-image: + linear-gradient(var(--grid_color) 1px, transparent 1px), + linear-gradient(90deg, var(--grid_color) 1px, transparent 1px); + + .warehouse_canvas { + position: absolute; + top: 0; + left: 0; + background-color: rgba(240, 248, 255, 0.5); + will-change: transform; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.05); + border-radius: 4px; + } + } + + .shelf { + position: absolute; + background-color: white; + border: 2px solid; + border-radius: 4px; + cursor: grab; + z-index: 1; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + transition: all 0.2s ease; + overflow: hidden; + + &:hover { + border-width: 3px; + } + + &.selected { + z-index: 10; + box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.3); + } + + &.dragging { + cursor: grabbing; + z-index: 100; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + } + + .shelf_name { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 14px; + font-weight: bold; + text-align: center; + background-color: transparent; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + z-index: 2; + } + + .shelf_utilization { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + text-align: center; + padding: 2px; + font-size: 10px; + color: white; + font-weight: bold; + } + } + + .right_panel { + width: 300px; + background-color: white; + border-left: 1px solid #e0e4e8; + padding: 10px; + display: flex; + flex-direction: column; + overflow: auto; + + .panel_section { + margin-bottom: 12px; + + h2 { + font-size: 16px; + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; + color: var(--primary); + } + + .warehouse_overview { + background-color: #f8f9fa; + border-radius: 8px; + padding: 12px; + margin-top: 8px; + + .overview_item { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 12px; + + &:last-child { + margin-bottom: 0; + } + + .label { + color: #6c757d; + } + + .value { + font-weight: 600; + text-align: right; + } + } + + .progress_bar { + height: 8px; + background-color: #e9ecef; + border-radius: 4px; + overflow: hidden; + margin-top: 8px; + + .progress_fill { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + color: white; + font-weight: bold; + transition: width 0.3s ease; + } + } + } + + .analysis_section { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + + .material_stats { + margin-top: 8px; + + .material_category { + display: flex; + align-items: center; + margin-bottom: 5px; + + .color_indicator { + width: 10px; + height: 10px; + border-radius: 2px; + margin-right: 8px; + } + + .category_name { + flex: 1; + font-size: 12px; + } + + .category_value { + font-size: 12px; + font-weight: bold; + } + } + } + } + + .property_group { + margin-bottom: 12px; + // 货架信息组头部样式 + .group_header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + h3 { + margin: 0; + } + } + + // 编辑按钮样式 + .edit_button { + background-color: #1890ff; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + display: flex; + align-items: center; + + &:hover { + background-color: #40a9ff; + } + } + + // 编辑操作按钮容器 + .edit_actions { + display: flex; + gap: 8px; + } + + // 保存按钮样式 + .save_button { + background-color: #52c41a; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + display: flex; + align-items: center; + + &:hover { + background-color: #73d13d; + } + } + + // 取消按钮样式 + .cancel_button { + background-color: #f5222d; + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + + &:hover { + background-color: #ff4d4f; + } + } + + // 属性值样式 + .property_value { + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; + min-height: 32px; + display: flex; + align-items: center; + } + h3 { + font-size: 14px; + margin-bottom: 8px; + color: var(--dark); + } + + .property_row { + display: flex; + flex-direction: column; + gap: 5px; + margin-bottom: 8px; + + label { + font-size: 12px; + font-weight: 500; + color: #555; + } + + .input_group { + display: flex; + gap: 8px; + + input { + width: 100px; + padding: 6px 10px; + border-radius: 4px; + border: 1px solid #d1d8e0; + font-size: 13px; + flex: 1; + + &:focus { + outline: none; + border-color: var(--secondary); + } + } + } + } + + .inventory_stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; + margin-top: 10px; + + .stat_card { + background-color: #f8f9fa; + border-radius: 6px; + padding: 8px; + text-align: center; + + .stat_value { + font-size: 16px; + font-weight: bold; + color: var(--primary); + } + + .stat_label { + font-size: 10px; + color: #6c757d; + margin-top: 3px; + } + } + } + + .slot_grid_container { + border: 1px solid #eee; + border-radius: 6px; + padding: 10px; + background-color: #f8f9fa; + margin-top: 8px; + overflow: auto; + max-height: 300px; + + .slot_grid { + display: flex; + flex-direction: column; + gap: 2px; + + .slot_row { + display: flex; + gap: 2px; + + .slot_cell { + flex: 1; + aspect-ratio: 1; + min-width: 20px; + min-height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + font-size: 9px; + position: relative; + transition: all 0.2s; + cursor: pointer; + + &:hover:not(.empty) { + transform: scale(1.1); + z-index: 1; + box-shadow: 0 0 3px rgba(0,0,0,0.3); + } + + &.empty { + background-color: transparent !important; + border: 1px dashed #ddd; + } + + .slot_info { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + + .slot_position { + font-weight: bold; + font-size: 8px; + } + + .slot_utilization { + font-size: 8px; + font-weight: 500; + } + } + } + } + } + + .slot_legend { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; + justify-content: center; + + .legend_item { + display: flex; + align-items: center; + gap: 4px; + font-size: 10px; + + .color_box { + width: 12px; + height: 12px; + border-radius: 2px; + } + } + } + } + } + + .shelf_header { + display: flex; + align-items: center; + margin-bottom: 15px; + flex-wrap: wrap; + gap: 8px; + + .shelf_code { + background-color: #e6f7ff; + color: #1890ff; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + } + + .editable_name { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + transition: all 0.3s; + padding: 4px 8px; + border-radius: 4px; + position: relative; + + &:hover { + background-color: #f5f5f5; + } + + h2 { + margin: 0; + font-size: 18px; + } + + .edit_icon { + color: #666; + font-size: 12px; + opacity: 0.6; + transition: opacity 0.3s; + } + + &:hover .edit_icon { + opacity: 1; + } + } + + .name_edit_container { + display: flex; + align-items: center; + gap: 8px; + + .name_input { + padding: 6px 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-size: 16px; + width: 180px; + transition: all 0.3s; + + &:focus { + outline: none; + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } + + .edit_buttons { + display: flex; + gap: 4px; + + .btn_icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + border: none; + cursor: pointer; + transition: all 0.2s; + + &.save_btn { + background-color: #52c41a; + color: white; + + &:hover { + background-color: #73d13d; + } + } + + &.cancel_btn { + background-color: #ff4d4f; + color: white; + + &:hover { + background-color: #ff7875; + } + } + } + } + } + } + } + } + } + + .modal_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + + .modal_content { + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + width: 400px; + max-width: 90%; + max-height: 90vh; + overflow-y: auto; + + h3 { + margin-top: 0; + margin-bottom: 20px; + color: #2c3e50; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + } + + .form_group { + margin-bottom: 15px; + + label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #555; + } + + input { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + + &:focus { + outline: none; + border-color: #3498db; + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); + } + } + } + + .modal_actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + + .btn { + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + + &_cancel { + background-color: #f1f5f9; + color: #555; + + &:hover { + background-color: #e2e8f0; + } + } + + &_save { + background-color: #3498db; + color: white; + + &:hover { + background-color: #2980b9; + } + } + } + } + } + } +} + +//// 响应式调整 +//@media (max-width: 768px) { +// .main_content { +// flex-direction: column; +// +// .right_panel { +// width: 100% !important; +// max-height: 40vh; +// } +// } +// +// .header { +// flex-direction: column; +// align-items: flex-start; +// +// .header_left { +// width: 100%; +// justify-content: space-between; +// } +// +// .top_status_bar { +// margin-left: 0 !important; +// margin-top: 10px; +// width: 100%; +// justify-content: space-between; +// } +// } +//} diff --git a/src/components/Space/search/InventoryForm.tsx b/src/components/Space/search/InventoryForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8356c2bfad0dbf71baf77661a3b42e6d8ffc549e --- /dev/null +++ b/src/components/Space/search/InventoryForm.tsx @@ -0,0 +1,195 @@ +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; +import React, { useEffect, useState } from 'react'; +import { XThing } from '@/ts/base/schema'; +import { Cascader, Col, Modal, Result, Row, Space } from 'antd'; +import { logger } from '@/ts/base/common'; +import { CheckCard } from '@ant-design/pro-components'; +import styles from '@/components/Space/search/index.module.less'; +import SearchInput from '@/components/SearchInput'; +import { AiOutlineSmile } from 'react-icons/ai'; + +interface IProps { + formType: string; + current: IWareHousing; + finished: () => void; +} +const InventoryFrom: React.FC = (props: IProps) => { + const [select, setSelect] = useState([]); + const tableProps: IProps = props; + const [checked, setChecked] = useState([]); + const [searchKey, setSearchKey] = useState(); + const [dataSource, setDataSource] = useState([]); + const [searchPlace, setSearchPlace] = useState(); + const [slot, setSlot] = useState(undefined); + useEffect(() => { + setSearchPlace('请输入物资代码'); + }, [props]); + + useEffect(() => { + if (searchKey) { + searchList(searchKey); + } + }, [searchKey]); + useEffect(() => { + if (tableProps.formType === 'removeFromInventory') { + loadThings().then(() => {}); + } + }, [slot]); + let modalTitle = ''; + switch (tableProps.formType) { + case 'addToInventory': + modalTitle = '上架'; + break; + case 'removeFromInventory': + modalTitle = '下架'; + break; + default: + return <>; + } + const searchList = async (searchCode: string) => { + if (searchCode) { + let res: XThing[] = []; + if (slot) { + res = await tableProps.current.loadThing(slot, searchCode); + } + setDataSource(res); + } + }; + const loadThings = async () => { + if (!slot) return; + try { + const things = await tableProps.current.loadThing(slot); + setDataSource(things); + } catch (error) { + setDataSource([]); + } + }; + // 单位卡片渲染 + const infoList = () => { + return ( + { + setChecked(value); + let checkObjs: XThing[] = []; + for (const thing of dataSource) { + if (value.includes(thing.id)) { + checkObjs.push(thing); + } + } + setSelect(checkObjs); + }}> + + {dataSource.map((thing) => ( + + + {thing.id} + {/*物资名称:{thing.id}*/} + + } + value={thing.id} + key={thing.id} + // description={ + // + // + // {thing.code} + // + // + // } + /> + + ))} + + + ); + }; + return ( + { + if (slot) { + let result: boolean = false; + switch (tableProps.formType) { + case 'addToInventory': + result = await tableProps.current.AddToInventory( + slot, + select.map((item) => item.id), + ); + break; + case 'removeFromInventory': + modalTitle = '下架'; + result = await tableProps.current.RemoveFromInventory( + slot, + select.map((item) => item.id), + ); + break; + default: + return; + } + if (result) { + logger.info(`${modalTitle}成功`); + if (tableProps.formType === 'removeFromInventory') { + await loadThings(); + } + } else { + //logger.error(`${modalTitle}失败`); + } + } + }} + onCancel={props.finished} + okButtonProps={{ disabled: select?.length < 1 }} + width={670}> +
+ { + if (!value || !Array.isArray(value) || !value[1]) { + setSlot(undefined); + return; + } + try { + const slot = tableProps.current.shelvesSlots.find( + (item) => item.id === value[1], + ); + if (!slot) { + setSlot(undefined); + return; + } + setSlot(slot.id); + } catch (error) { + setSlot(undefined); + } + }} + /> + {modalTitle === '上架' ? ( + { + setSearchKey(event.target.value); + }} + /> + ) : ( + <> + )} + {dataSource.length > 0 && infoList()} + {searchKey && dataSource.length == 0 && ( + } title={`抱歉,没有查询到相关的结果`} /> + )} +
+
+ ); +}; + +export default InventoryFrom; diff --git a/src/components/Space/search/index.module.less b/src/components/Space/search/index.module.less new file mode 100644 index 0000000000000000000000000000000000000000..d9d41b3e7464099e815f0f1009a2f955d7c87577 --- /dev/null +++ b/src/components/Space/search/index.module.less @@ -0,0 +1,47 @@ +@import (reference) '~antd/es/style/themes/variable'; + +:global { + .ogo-avatar-lg { + width: 60px; + height: 60px; + line-height: 60px; + border-radius: 50%; + } +} + +.search-card { + min-height: 300px; + // background-color: @item-hover-bg; + padding: 24px; + .card { + width: 306px; + margin-top: 24px; + .description { + margin-top: @margin-xs; + } + } + + .company-select-type { + border: 1px solid #5ba0e7; + } + + .company-no-select-type { + border: 1px solid #000; + } +} + +.tree-title-wrapper { + display: flex; + + .tree-title-icon { + margin-top: 2px; + margin-right: 5px; + } +} +.dept-tree{ + :global{ + .ogo-tree-node-content-wrapper.ogo-tree-node-selected{ + background-color: #bdd7ff; + } + } +} diff --git a/src/components/Space/tool.ts b/src/components/Space/tool.ts new file mode 100644 index 0000000000000000000000000000000000000000..23ab26495820b529a22b355ce376d5a58e3b4efa --- /dev/null +++ b/src/components/Space/tool.ts @@ -0,0 +1,5 @@ +export type Tool = { + id: string; + name: string; + icon: JSX.Element; +}; diff --git a/src/executor/action.tsx b/src/executor/action.tsx index 170b3d3b1746b422fe0b6e77c468745a545eabeb..3b5227eb80b8fc98cf8356c6679ce9ba748a09f7 100644 --- a/src/executor/action.tsx +++ b/src/executor/action.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { + IAbstractSpace, IApplication, IDirectory, IEntity, @@ -33,8 +34,10 @@ import MemberBox from '@/components/DataStandard/WorkForm/Viewer/customItem/memb import { IVersion } from '@/ts/core/thing/standard/version'; import { $confirm } from '@/utils/react/antd'; import { IReception } from '@/ts/core/work/assign/reception'; +import { ISpace } from '@/ts/core/abstractSpace/abstractSpaceInfo'; /** 执行非页面命令 */ export const executeCmd = (cmd: string, entity: any) => { + console.log(cmd); switch (cmd) { case 'qrcode': return entityQrCode(entity); @@ -129,7 +132,10 @@ export const executeCmd = (cmd: string, entity: any) => { }; /** 刷新目录 */ -const directoryRefresh = (dir: IDirectory | IApplication, reload: boolean) => { +const directoryRefresh = ( + dir: IDirectory | IApplication | IAbstractSpace, + reload: boolean, +) => { dir.loadContent(reload).then(() => { orgCtrl.changCallback(); }); @@ -413,7 +419,7 @@ const openChat = (entity: IMemeber | ITarget | ISession) => { }; /** 恢复实体 */ -const restoreEntity = (entity: IFile) => { +const restoreEntity = (entity: IFile | ISpace) => { entity.restore().then((success: boolean) => { if (success) { orgCtrl.changCallback(); @@ -422,7 +428,7 @@ const restoreEntity = (entity: IFile) => { }; /** 删除实体 */ -const deleteEntity = (entity: IFile, hardDelete: boolean) => { +const deleteEntity = (entity: IFile | ISpace, hardDelete: boolean) => { Modal.confirm({ okText: '确认', cancelText: '取消', diff --git a/src/executor/operate/entityForm/index.tsx b/src/executor/operate/entityForm/index.tsx index 85a475496a83760923ad437856e19d00fdbc4abc..7ed26b5eb2f4814f39a382f179bbdc66219599a2 100644 --- a/src/executor/operate/entityForm/index.tsx +++ b/src/executor/operate/entityForm/index.tsx @@ -22,6 +22,10 @@ import DocumentTemplateForm from './DocumentTemplateForm'; import ReportForm from './reportForm'; import AssignTaskModelForm from '@/executor/operate/entityForm/assignTaskModelForm'; import ReportTaskForm from './reportTaskForm'; +import SpaceForm from '@/executor/operate/entityForm/spaceForm'; +import WareHousingForm from '@/executor/operate/entityForm/warehousingSpaceForm'; +import InventoryForm from '@/components/Space/search/InventoryForm'; + interface IProps { cmd: string; entity: IEntity; @@ -45,6 +49,19 @@ const EntityForm: React.FC = ({ cmd, entity, finished }) => { ); } + case 'newSpace': + case 'updateSpace': + return ; + case 'newWareHousing': + case 'updateWareHousing': + return ( + + ); + case 'addToInventory': + case 'removeFromInventory': + return ( + + ); case 'newApp': case 'newModule': case 'updateApp': diff --git a/src/executor/operate/entityForm/propertyForm.tsx b/src/executor/operate/entityForm/propertyForm.tsx index cf05d1987e740fd4bbe703e83a4b8af0f2b7dad7..4dee832bdc62f4f24afa62ebafc4897994e74abf 100644 --- a/src/executor/operate/entityForm/propertyForm.tsx +++ b/src/executor/operate/entityForm/propertyForm.tsx @@ -5,6 +5,7 @@ import { IDirectory, valueTypes, IProperty } from '@/ts/core'; import { EntityColumns } from './entityColumns'; import { schema } from '@/ts/base'; import OpenFileDialog from '@/components/OpenFileDialog'; +import OpenSpaceDialog from '@/components/OpenSpaceDialog'; import { Input } from 'antd'; interface Iprops { @@ -127,6 +128,29 @@ const PropertyForm = (props: Iprops) => { }, }); } + if (['空间型'].includes(selectType || '')) { + const typeName = '空间'; + columns.push({ + title: `选择${typeName}`, + dataIndex: 'speciesId', + valueType: 'select', + formItemProps: { rules: [{ required: true, message: `${typeName}为必填项` }] }, + renderFormItem() { + if (readonly) { + return
{species?.name ?? ''}
; + } + return ( + setNeedType(typeName)} + /> + ); + }, + }); + } if (selectType === '引用型') { columns.push({ title: `选择表单`, @@ -225,13 +249,14 @@ const PropertyForm = (props: Iprops) => { props.finished(); }} /> - {needType !== '' && ( + {needType !== '' && needType != '空间' && ( setNeedType('')} onOk={(files) => { + console.log(needType) if (['字典', '分类'].includes(needType)) { if (files.length > 0) { setSpecies(files[0].metadata); @@ -249,6 +274,22 @@ const PropertyForm = (props: Iprops) => { }} /> )} + {needType !== '' && needType === '空间' && ( + setNeedType('')} + onOk={(files) => { + if (files.length > 0) { + setSpecies(files[0].metadata); + } else { + setSpecies(undefined); + } + setNeedType(''); + }} + /> + )} ); }; diff --git a/src/executor/operate/entityForm/spaceForm.tsx b/src/executor/operate/entityForm/spaceForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e04afc71f586f66cf1b26024dc83014766cecd90 --- /dev/null +++ b/src/executor/operate/entityForm/spaceForm.tsx @@ -0,0 +1,128 @@ +import React, { useRef } from 'react'; +import { ProFormColumnsType, ProFormInstance } from '@ant-design/pro-components'; +import SchemaForm from '@/components/SchemaForm'; +import { IAbstractSpace } from '@/ts/core'; +import { EntityColumns } from './entityColumns'; +import { schema } from '@/ts/base'; +import { generateCodeByInitials } from '@/utils/tools'; + +interface Iprops { + formType: string; + current: IAbstractSpace; + finished: () => void; +} +/* + 编辑 +*/ +const SpaceForm = (props: Iprops) => { + let title = ''; + const readonly = props.formType === 'remarkSpace'; + let initialValue: any = props.current.metadata; + const formRef = useRef(); + switch (props.formType) { + case 'newSpace': + title = '新建空间'; + initialValue = { shareId: props.current.target.id }; + break; + case 'updateSpace': + title = '更新空间'; + break; + case 'remarkSpace': + title = '查看空间'; + break; + default: + return <>; + } + const columns: ProFormColumnsType[] = [ + { + title: '名称', + dataIndex: 'name', + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '名称为必填项' }], + }, + }, + { + title: '代码', + dataIndex: 'code', + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '代码为必填项' }], + }, + }, + { + title: '地址', + dataIndex: 'address', + colProps: { span: 24 }, + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '地址信息为必填项' }], + }, + }, + { + title: '制定组织', + dataIndex: 'shareId', + valueType: 'select', + hideInForm: true, + readonly: readonly, + formItemProps: { rules: [{ required: true, message: '组织为必填项' }] }, + fieldProps: { + options: [ + { + value: props.current.target.id, + label: props.current.target.name, + }, + ], + }, + }, + ]; + if (readonly) { + columns.push(...EntityColumns(props.current.metadata)); + } + columns.push({ + title: '备注信息', + dataIndex: 'remark', + valueType: 'textarea', + colProps: { span: 24 }, + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '备注信息为必填项' }], + }, + }); + return ( + + formRef={formRef} + open + title={title} + width={640} + columns={columns} + initialValues={initialValue} + rowProps={{ + gutter: [24, 0], + }} + layoutType="ModalForm" + onOpenChange={(open: boolean) => { + if (!open) { + props.finished(); + } + }} + onValuesChange={async (values: any) => { + if (Object.keys(values)[0] === 'name') { + formRef.current?.setFieldValue('code', generateCodeByInitials(values['name'])); + } + }} + onFinish={async (values) => { + switch (props.formType) { + case 'updateSpace': + await props.current.update(values); + break; + case 'newSpace': + await props.current.create(values); + break; + } + props.finished(); + }}> + ); +}; + +export default SpaceForm; diff --git a/src/executor/operate/entityForm/warehousingSpaceForm.tsx b/src/executor/operate/entityForm/warehousingSpaceForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..326a46563062932897ba505fa8ecd899127f433c --- /dev/null +++ b/src/executor/operate/entityForm/warehousingSpaceForm.tsx @@ -0,0 +1,144 @@ +import React, { useRef } from 'react'; +import { ProFormColumnsType, ProFormInstance } from '@ant-design/pro-components'; +import SchemaForm from '@/components/SchemaForm'; +import { IAbstractSpace } from '@/ts/core'; +import { EntityColumns } from './entityColumns'; +import { schema } from '@/ts/base'; +import { generateCodeByInitials } from '@/utils/tools'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; + +interface Iprops { + formType: string; + current: IAbstractSpace | IWareHousing; + finished: () => void; +} +/* + 编辑 +*/ +const WareHousingForm = (props: Iprops) => { + let title = ''; + let abstractSpace: IAbstractSpace; + let wareHousing: IWareHousing | undefined; + const readonly = props.formType === 'remarkWareHousing'; + let initialValue: any = props.current.metadata; + const formRef = useRef(); + switch (props.formType) { + case 'newWareHousing': + title = '新建仓储空间'; + abstractSpace = props.current as IAbstractSpace; + initialValue = {}; + break; + case 'updateWareHousing': + wareHousing = props.current as IWareHousing; + abstractSpace = wareHousing.abstractSpace; + title = '更新' + wareHousing.typeName; + break; + case 'remarkWareHousing': + wareHousing = props.current as IWareHousing; + abstractSpace = wareHousing.abstractSpace; + title = '查看' + wareHousing.typeName; + break; + default: + return <>; + } + const columns: ProFormColumnsType[] = [ + { + title: '名称', + dataIndex: 'name', + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '名称为必填项' }], + }, + }, + { + title: '代码', + dataIndex: 'code', + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '代码为必填项' }], + }, + }, + { + title: '长(m)', + dataIndex: 'length', + readonly: readonly, + valueType: 'digit', + fieldProps: { + min: 0, + }, + formItemProps: { + rules: [{ required: true, message: '长(m)为必填项' }], + }, + }, + { + title: '宽(m)', + dataIndex: 'width', + readonly: readonly, + valueType: 'digit', + fieldProps: { + min: 0, + }, + formItemProps: { + rules: [{ required: true, message: '宽(m)为必填项' }], + }, + }, + { + title: '地址', + dataIndex: 'address', + readonly: readonly, + colProps: { span: 24 }, + formItemProps: { + rules: [{ required: true, message: '地址信息为必填项' }], + }, + }, + ]; + if (readonly) { + columns.push(...EntityColumns(props.current.metadata)); + } + columns.push({ + title: '备注信息', + dataIndex: 'remark', + valueType: 'textarea', + colProps: { span: 24 }, + readonly: readonly, + formItemProps: { + rules: [{ required: true, message: '备注信息为必填项' }], + }, + }); + return ( + + formRef={formRef} + open + title={title} + width={640} + columns={columns} + initialValues={initialValue} + rowProps={{ + gutter: [24, 0], + }} + layoutType="ModalForm" + onOpenChange={(open: boolean) => { + if (!open) { + props.finished(); + } + }} + onValuesChange={async (values: any) => { + if (Object.keys(values)[0] === 'name') { + formRef.current?.setFieldValue('code', generateCodeByInitials(values['name'])); + } + }} + onFinish={async (values) => { + switch (props.formType) { + case 'updateWareHousing': + await wareHousing!.update(values); + break; + case 'newWareHousing': + await abstractSpace.standard.createWareHousing(values); + break; + } + props.finished(); + }}> + ); +}; + +export default WareHousingForm; diff --git a/src/executor/operate/index.tsx b/src/executor/operate/index.tsx index 44d7c91926971d49265815ec26da67ffe1bed823..516a47ec7d5a894abfb3d3d7646c4b319ff997c4 100644 --- a/src/executor/operate/index.tsx +++ b/src/executor/operate/index.tsx @@ -76,17 +76,31 @@ const ConfigExecutor: React.FC = ({ cmd, args, finished }) => { if ( entity.groupTags && entity.groupTags.some((item) => - ['视图', '表单', '报表', '表格'].includes(item), + ['视图', '表单', '报表', '表格', '空间', '仓储空间'].includes(item), ) ) { - if (entity.typeName === '表格') { - return ( - - ); - } else { - return ( - - ); + console.log(entity.typeName); + switch (entity.typeName) { + case '表格': + return ( + + ); + case '空间': + return ( + + ); + case '仓储空间': + return ( + + ); + default: + return ( + + ); } } if (Object.keys(entityMap).includes(args[0].typeName)) { diff --git a/src/executor/tools/uploadItem.tsx b/src/executor/tools/uploadItem.tsx index 939567a92e32d4dd70dda155fbfe444324f5c4e9..d11218f93ee977fb88927f60f83a8d0f86fe28d7 100644 --- a/src/executor/tools/uploadItem.tsx +++ b/src/executor/tools/uploadItem.tsx @@ -1,13 +1,14 @@ import React, { useState } from 'react'; import { model, parseAvatar } from '@/ts/base'; import { message, Upload, UploadProps, Image, Button, Space, Avatar } from 'antd'; -import { IDirectory } from '@/ts/core'; +import { IDirectory, IAbstractSpace } from '@/ts/core'; import TypeIcon from '@/components/Common/GlobalComps/typeIcon'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; interface IProps { icon?: string; typeName: string; - directory: IDirectory; + directory: IDirectory | IAbstractSpace | IWareHousing; readonly?: boolean; avatarSize?: number; iconSize?: number; @@ -42,7 +43,7 @@ const UploadItem: React.FC = ({ async customRequest(options) { const file = options.file as File; if (file) { - const result = await directory.createFile(file.name, file); + const result = await directory?.createFile(file.name, file); if (result) { setAvatar(result.shareInfo()); onChanged(JSON.stringify(result.shareInfo())); diff --git a/src/pages/Home/components/SpaceBrowser/index.tsx b/src/pages/Home/components/SpaceBrowser/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..207bd78f43dbf1f9391abef2e38ab2ab23aa5cb9 --- /dev/null +++ b/src/pages/Home/components/SpaceBrowser/index.tsx @@ -0,0 +1,105 @@ +import React, { useEffect, useState } from 'react'; +import { command } from '@/ts/base'; +import DirectoryViewer from '@/components/Directory/views'; +import { loadFileMenus } from '@/executor/fileOperate'; +import { Spin } from 'antd'; +import { cleanMenus } from '@/utils/tools'; +import { ISpace, IStandardSpaceInfo } from '@/ts/core/abstractSpace/abstractSpaceInfo'; +import AppLayout from '@/components/MainLayout/appLayout'; +import useTimeoutHanlder from '@/hooks/useTimeoutHanlder'; + +interface IProps { + root: IStandardSpaceInfo; +} + +/** + * @description: 默认目录 + * @return {*} + */ +const SpaceBrowser: React.FC = ({ root }) => { + const [currentTag, setCurrentTag] = useState('全部'); + const [preDirectory, setPreDirectory] = useState(); + const [directory, setDirectory] = useState(root); + const [content, setContent] = useState(directory.content()); + const [loaded, setLoaded] = useState(false); + const [focusFile, setFocusFile] = useState(); + const [submitHanlder] = useTimeoutHanlder(); + useEffect(() => { + if (loaded) { + command.emitter('preview', 'space', focusFile); + } + }, [focusFile, loaded]); + useEffect(() => { + setDirectory(root); + }, [root]); + useEffect(() => { + if (content.length > 0) { + setFocusFile(content[0]); + } + }, [content]); + useEffect(() => { + setCurrentTag('全部'); + const id = directory.subscribe(() => { + loadContent(directory, directory, false); + }); + if (directory != root) { + setPreDirectory(directory.superior); + } else { + setPreDirectory(undefined); + } + return () => { + directory.unsubscribe(id); + }; + }, [directory]); + /** 加载目录内容 */ + const loadContent = (file: ISpace, directory: ISpace, reload: boolean) => { + setLoaded(false); + file.loadContent(reload).then(async () => { + const data = directory.content(); + if (file.key === directory.key) { + setLoaded(true); + setContent(data); + } + }); + }; + const focusHanlder = (file: ISpace | undefined) => { + if (file && file.key !== focusFile?.key) { + setFocusFile(file); + } + }; + const contextMenu = (file?: ISpace) => { + const entity = file ?? directory; + return { + items: cleanMenus(loadFileMenus(entity)) || [], + onClick: ({ key }: { key: string }) => { + command.emitter('executor', key, entity); + }, + }; + }; + + return ( + + + setCurrentTag(t)} + fileOpen={(file) => { + if (file && 'isContainer' in file && file.isContainer) { + setDirectory(file as IStandardSpaceInfo); + } else { + submitHanlder(() => focusHanlder(file as ISpace), 300); + } + }} + preDirectory={preDirectory} + contextMenu={(entity) => contextMenu(entity as ISpace)} + /> + + + ); +}; +export default SpaceBrowser; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index a9906a049fcf37e55abd0fddd27b254f2a6b32c2..9be12b3e4472b82ff5865722ae50f2b294b97e04 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -19,6 +19,7 @@ import ChatViewer from './components/ChatViewer'; import RelationBrowser from './components/RelationBrowser'; import TargetsActivity from './components/TargetsActivity'; import TaskViewer from './components/TaskViewer'; +import SpaceBrowser from '@/pages/Home/components/SpaceBrowser'; const defaultActions = [ { text: '首页', icon: 'homebar/home', count: 0, bodyType: 'home' }, @@ -29,6 +30,7 @@ const defaultActions = [ { text: '文档', icon: 'homebar/store', count: 0, bodyType: 'file' }, { text: '应用', icon: 'homebar/store', count: 0, bodyType: 'data' }, { text: '关系', icon: 'homebar/relation', count: 0, bodyType: 'relation' }, + { text: '空间', icon: 'homebar/relation', count: 0, bodyType: 'abstractSpace' }, ]; const Home: React.FC = () => { const [bodyType, setBodyType] = useState('bench'); @@ -224,6 +226,9 @@ const Home: React.FC = () => { return ; case 'activity': return ; + case 'abstractSpace': + console.log(orgCtrl.home.current.abstractSpace); + return ; default: if (typeof bodyType === 'string') { return ( diff --git a/src/ts/base/schema.ts b/src/ts/base/schema.ts index 240923a2be9c71748da8368f5ded56cf2bccd798..8bcb0cadcb65fb5fc92ee10c9eb78d464112fb4e 100644 --- a/src/ts/base/schema.ts +++ b/src/ts/base/schema.ts @@ -1844,3 +1844,71 @@ export interface viewEntity { avatar?: FileItemShare; }; } +export type XAbstractSpaceStandard = { + //空间id + abstractSpaceId: string; + //是否删除 + isDeleted: boolean; + //二维平面长宽 + length: number; + width: number; + height?: number; + address?: string; + //位置 + position?: [number, number]; +} & XEntity; + +//抽象空间 +export type XAbstractSpace = { + //权限ids + applyAuths: string[]; +} & XAbstractSpaceStandard; +//仓储空间 +export type XWareHousingSpace = { + [key: string]: any; + //货架信息 + goodsShelves: XGoodsShelves[]; +} & XAbstractSpaceStandard; +//货架 +export type XGoodsShelves = { + //货架id + id: string; + //货架名称 + name: string; + //货架编号 + code: string; + //二维平面长宽 + length: number; + width: number; + height?: number; + //货架行数列数 + rows: number; + cols: number; + //货架坐标位置 + position: [number, number]; +}; + +//库位 +export type XGoodsShelvesSlot = { + [key: string]: any; + wareHousingId: string; + //归属货架 + goodsShelveId: string; + //容量 + capacity: number; + //库存 + inventory: string[]; +} & XAbstractSpaceStandard; + +// 变更历史 +export interface XShelvesSlotRecord extends XEntity { + // 操作 + operate: string; + wareHousingId: string; + //归属货架 + goodsShelveId: string; + //库位 + goodsShelvesSlotId: string; + //物资 + goodsId: string; +} diff --git a/src/ts/core/abstractSpace/abstractSpace.ts b/src/ts/core/abstractSpace/abstractSpace.ts new file mode 100644 index 0000000000000000000000000000000000000000..1dd31d61c594a455a864e1f1ad34e6ae27968f72 --- /dev/null +++ b/src/ts/core/abstractSpace/abstractSpace.ts @@ -0,0 +1,168 @@ +import { model, schema } from '../../base'; +import { ITarget } from '@/ts/core'; +import { DataResource } from '@/ts/core/thing/resource'; +import { StandardAbstractSpaces } from '@/ts/core/abstractSpace/standard'; +import { spaceOperates } from '@/ts/core/public'; +import { ISpace, IStandardSpaceInfo, StandardSpaceInfo } from './abstractSpaceInfo'; +export interface IAbstractSpace extends IStandardSpaceInfo { + /** 真实的空间Id */ + abstractSpaceId: string; + standard: StandardAbstractSpaces; + /** 当前的用户 */ + target: ITarget; + /** 资源类 */ + resource: DataResource; + /** 上级 */ + parent: IAbstractSpace | undefined; + /** 下级 */ + children: IAbstractSpace[]; + /** 是否有权限 */ + isAuth: boolean; + /** 空间下的内容 */ + content(store?: boolean): ISpace[]; + /** 创建空间 */ + create(data: schema.XAbstractSpace): Promise; + + loadSpaceResource(reload?: boolean): Promise; +} +export class AbstractSpace + extends StandardSpaceInfo + implements IAbstractSpace +{ + constructor( + _metadata: schema.XAbstractSpace, + _target: ITarget, + _parent?: IAbstractSpace, + _spaces?: schema.XAbstractSpace[], + ) { + super( + { + ..._metadata, + typeName: _metadata.typeName || '空间', + }, + _target.resource.abstractSpaceColl, + ); + this.parent = _parent; + this.target = _target; + this.standard = new StandardAbstractSpaces(this); + } + target: ITarget; + standard: StandardAbstractSpaces; + parent: IAbstractSpace | undefined; + get children(): IAbstractSpace[] { + return this.standard.abstractSpaces; + } + get isContainer(): boolean { + return true; + } + get isShortcut(): boolean { + return false; + } + get groupTags(): string[] { + let tags: string[] = []; + if (this.parent) { + tags = [...super.groupTags]; + } else { + tags = [this.target.typeName]; + } + return tags; + } + get resource(): DataResource { + return this.target.resource; + } + get abstractSpaceId(): string { + return this.id; + } + get isAuth(): boolean { + if (!this._metadata.applyAuths?.length || this._metadata.applyAuths[0] === '0') + return true; + return this.target.hasAuthoritys(this._metadata.applyAuths); + } + get cacheFlag(): string { + return 'abstractSpaces'; + } + get superior(): ISpace { + return this.parent ?? this.target.abstractSpace; + } + get id(): string { + if (!this.parent) { + return this.target.id; + } + return super.id; + } + + content(_store: boolean = false): ISpace[] { + const cnt: ISpace[] = [...this.children]; + cnt.push(...this.standard.wareHousings); + return cnt.sort((a, b) => (a.metadata.updateTime < b.metadata.updateTime ? 1 : -1)); + } + async loadContent(reload: boolean = false): Promise { + if (reload) { + await this.loadSpaceResource(reload); + } + await this.standard.loadStandardSpaces(reload); + return true; + } + public async loadSpaceResource(reload: boolean = false) { + if (this.parent === undefined || reload) { + await this.resource.preSpaceLoad(reload); + } + await this.standard.loadSpaces(reload); + await this.standard.loadWareHousings(reload); + } + async create(data: schema.XAbstractSpace): Promise { + const result = await this.resource.abstractSpaceColl.insert({ + ...data, + abstractSpaceId: this.id, + }); + if (result) { + await this.notify('insert', result); + return result; + } + } + + override async delete(): Promise { + if (this.parent) { + await this.resource.abstractSpaceColl.delete(this.metadata); + await this.notify('delete', this.metadata); + } + return false; + } + override async hardDelete(): Promise { + if (this.parent) { + await this.resource.abstractSpaceColl.remove(this.metadata); + await this.recursionDelete(this); + await this.notify('reload', this.metadata); + } + return false; + } + private async recursionDelete(_space: IAbstractSpace) { + if (!this.isShortcut) { + for (const child of _space.children) { + await this.recursionDelete(child); + } + await _space.standard.delete(); + } + } + // 右键操作 + override operates(): model.OperateModel[] { + const operates: model.OperateModel[] = []; + operates.push(spaceOperates.Refesh); + if (this.target.hasRelationAuth()) { + operates.push(spaceOperates.NewSpace); + } + if (this.parent) { + operates.push(spaceOperates.NewWareHousingSpace); + operates.push(...super.operates()); + } + return operates; + } + + override receive(operate: string, data: schema.XAbstractSpaceStandard): boolean { + console.log(operate, data); + this.coll.removeCache((i) => i.id != data.id); + super.receive(operate, data); + this.coll.cache.push(this._metadata); + return true; + } +} diff --git a/src/ts/core/abstractSpace/abstractSpaceInfo.ts b/src/ts/core/abstractSpace/abstractSpaceInfo.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef39b5d6fb0cf5f86356d12f4a1c7a5d004420e1 --- /dev/null +++ b/src/ts/core/abstractSpace/abstractSpaceInfo.ts @@ -0,0 +1,142 @@ +import { sleep } from '@/ts/base/common'; +import { model, schema } from '../../base'; +import { Entity, entityOperates, IEntity } from '../public'; +import { XCollection } from '@/ts/core'; + +/** 默认空间接口 */ +export interface ISpace extends ISpaceInfo {} +/** 空间类接口 */ +export interface ISpaceInfo extends IEntity { + /** 上级 */ + superior: ISpace; + /** 删除文件系统项 */ + delete(notity?: boolean): Promise; + /** 彻底删除文件系统项 */ + hardDelete(notity?: boolean): Promise; + /** + * 重命名 + * @param {string} name 新名称 + */ + rename(name: string): Promise; + /** 加载文件内容 */ + loadContent(reload: boolean): Promise; + + /** 目录下的内容 */ + content(args?: boolean): ISpace[]; +} +/** 文件类抽象实现 */ +export abstract class SpaceInfo + extends Entity + implements ISpaceInfo +{ + constructor(_metadata: T) { + super(_metadata, [_metadata.typeName]); + } + override get metadata(): T { + return this._metadata; + } + get superior(): ISpace { + return this; + } + abstract delete(): Promise; + abstract hardDelete(): Promise; + async rename(_: string): Promise { + await sleep(0); + return true; + } + async restore(): Promise { + await sleep(0); + return true; + } + + async loadContent(reload: boolean = false): Promise { + return await sleep(reload ? 10 : 0); + } + content(): ISpace[] { + return []; + } +} + +export interface IStandardSpaceInfo< + T extends schema.XAbstractSpaceStandard = schema.XAbstractSpaceStandard, +> extends ISpaceInfo { + /** 设置当前元数据 */ + setMetadata(_metadata: schema.XAbstractSpaceStandard): void; + /** 变更通知 */ + notify(operate: string, data: T): Promise; + /** 更新 */ + update(data: T): Promise; + /** 接收通知 */ + receive(operate: string, data: schema.XAbstractSpaceStandard): boolean; +} +export interface IStandard extends IStandardSpaceInfo {} +export abstract class StandardSpaceInfo + extends SpaceInfo + implements IStandardSpaceInfo +{ + coll: XCollection; + constructor(_metadata: T, _coll: XCollection) { + super(_metadata); + this.coll = _coll; + } + override get metadata(): T { + return this._metadata; + } + async update(data: T): Promise { + const result = await this.coll.replace({ + ...this.metadata, + ...data, + abstractSpaceId: this.metadata.abstractSpaceId, + typeName: this.metadata.typeName, + }); + if (result) { + this.notify('replace', result); + return true; + } + return false; + } + async delete(notify: boolean = true): Promise { + const data = await this.coll.delete(this.metadata); + if (data && notify) { + await this.notify('delete', this.metadata); + } + return false; + } + async hardDelete(notify: boolean = true): Promise { + const data = await this.coll.remove(this.metadata); + if (data && notify) { + await this.notify('remove', this.metadata); + } + return false; + } + async restore(): Promise { + return this.update({ ...this.metadata, isDeleted: false }); + } + async rename(name: string): Promise { + return await this.update({ ...this.metadata, name }); + } + override operates(): model.OperateModel[] { + const operates: model.OperateModel[] = []; + operates.unshift(entityOperates.Update, entityOperates.HardDelete); + return operates; + } + async notify(operate: string, data: T): Promise { + return await this.coll.notity({ data, operate }); + } + receive(operate: string, data: schema.XAbstractSpaceStandard): boolean { + switch (operate) { + case 'delete': + case 'replace': + if (data) { + if (operate === 'delete') { + data = { ...data, isDeleted: true } as unknown as T; + this.setMetadata(data as T); + } else { + this.setMetadata(data as T); + this.loadContent(true); + } + } + } + return true; + } +} diff --git a/src/ts/core/abstractSpace/standard/goodsShelvesSlot.ts b/src/ts/core/abstractSpace/standard/goodsShelvesSlot.ts new file mode 100644 index 0000000000000000000000000000000000000000..de636f6ec4c917ccfbcbffefdf9b1b5ec8a8c676 --- /dev/null +++ b/src/ts/core/abstractSpace/standard/goodsShelvesSlot.ts @@ -0,0 +1,134 @@ +import { schema } from '../../../base'; +import { + IStandardSpaceInfo, + StandardSpaceInfo, +} from '@/ts/core/abstractSpace/abstractSpaceInfo'; +import { IAbstractSpace, ITarget } from '@/ts/core'; +import { DataResource } from '@/ts/core/thing/resource'; +import { IWareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; +import { logger } from '@/ts/base/common'; +//库位 +export interface IGoodsShelvesSlot extends IStandardSpaceInfo { + /** 当前的用户 */ + target: ITarget; + /** 资源集合 */ + resource: DataResource; + abstractSpace: IAbstractSpace; + wareHousing: IWareHousing; + + /** 上架 */ + AddToInventory(thingId: string[]): Promise; + /** 下架 */ + RemoveFromInventory(thingId: string[]): Promise; + /** 加载物品 */ + loadThing(code?: string): Promise; +} + +export class GoodsShelvesSlot + extends StandardSpaceInfo + implements IGoodsShelvesSlot +{ + constructor( + _metadata: schema.XGoodsShelvesSlot, + _space: IAbstractSpace, + _wareHousing: IWareHousing, + ) { + super(_metadata, _space.resource.goodsShelvesSlotColl); + this.setEntity(); + this.abstractSpace = _space; + this.wareHousing = _wareHousing; + this.target = _space.target; + this.resource = _space.resource; + //this.currentStock = this._metadata.inventory.length ?? 0; + } + abstractSpace: IAbstractSpace; + resource: DataResource; + target: ITarget; + wareHousing: IWareHousing; + //currentStock: number; + get id(): string { + return this._metadata.id.replace('_', ''); + } + get superior(): IWareHousing { + return this.wareHousing; + } + get cacheFlag(): string { + return 'shelvesSlots'; + } + /** 上架 */ + async AddToInventory(thingId: string[]): Promise { + if ( + this._metadata.capacity < + (this._metadata.inventory?.length ?? 0) + thingId.length + ) { + logger.warn(`库位${this._metadata.name}已满。`); + return false; + } + if (!this._metadata.inventory) { + this._metadata.inventory = []; + } + this._metadata.inventory.push(...thingId); + const result = await this.coll.update(this.id, { + _set_: this._metadata, + }); + if (result) { + await this.notify('replace', result); + return true; + } + // const result = await this.coll.replace({ + // ...this.metadata, + // }); + // if (result) { + // await this.notify('remove', this.metadata); + // await this.abstractSpace.resource.goodsShelvesSlotColl.notity({ + // data: result, + // operate: 'insert', + // }); + // return true; + // } + return false; + } + /** 下架 */ + async RemoveFromInventory(thingId: string[]): Promise { + this._metadata.inventory = this._metadata.inventory.filter( + (id) => !thingId.includes(id), + ); + console.log(this._metadata.inventory); + const result = await this.coll.update(this.id, { + _set_: this._metadata, + }); + if (result) { + this.notify('replace', result); + return true; + } + return false; + } + async loadThing(code?: string): Promise { + if (code) { + const res = await this.resource.thingColl.loadResult({ + options: { + match: { + id: code, + isDeleted: false, + }, + project: { + archives: 0, + }, + }, + }); + return res.data ?? []; + } + const res = await this.resource.thingColl.loadResult({ + options: { + match: { + id: { _in_: this._metadata.inventory }, + isDeleted: false, + }, + project: { + archives: 0, + }, + }, + }); + return res.data ?? []; + } +} diff --git a/src/ts/core/abstractSpace/standard/index.ts b/src/ts/core/abstractSpace/standard/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7f3abffa1d19ebd1efad4288ebbe61433020e43 --- /dev/null +++ b/src/ts/core/abstractSpace/standard/index.ts @@ -0,0 +1,229 @@ +import { schema } from '../../../base'; +import { DataResource } from '@/ts/core/thing/resource'; +import { AbstractSpace, IAbstractSpace } from '@/ts/core/abstractSpace/abstractSpace'; +import { IStandard } from '@/ts/core/abstractSpace/abstractSpaceInfo'; +import { IWareHousing, WareHousing } from '@/ts/core/abstractSpace/standard/warehousing'; +import { IGoodsShelvesSlot } from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; + +export class StandardAbstractSpaces { + /** 空间对象 */ + abstractSpace: IAbstractSpace; + /** 空间 */ + abstractSpaces: IAbstractSpace[] = []; + wareHousings: IWareHousing[] = []; + goodsShelvesSlots: IGoodsShelvesSlot[] = []; + /** 加载完成标志 */ + spaceLoaded: boolean = false; + wareHousingLoaded: boolean = false; + goodsShelvesSlotLoaded: boolean = false; + constructor(_abstractSpace: IAbstractSpace) { + this.abstractSpace = _abstractSpace; + if (this.abstractSpace.parent === undefined) { + subscribeNotity(this.abstractSpace); + } + } + get id(): string { + return this.abstractSpace.abstractSpaceId; + } + get resource(): DataResource { + return this.abstractSpace.resource; + } + + get standardSpaces(): IStandard[] { + return [...this.wareHousings, ...this.abstractSpaces]; + } + + async loadStandardSpaces(reload: boolean = false): Promise { + await Promise.all([this.loadWareHousings(reload)]); + return this.standardSpaces as IStandard[]; + } + async loadSpaces(reload: boolean = false): Promise { + if (!this.spaceLoaded || reload) { + this.spaceLoaded = true; + var spaces = this.resource.abstractSpaceColl.cache.filter( + (i) => i.abstractSpaceId === this.id, + ); + this.abstractSpaces = spaces.map( + (a) => new AbstractSpace(a, this.abstractSpace.target, this.abstractSpace), + ); + for (const space of this.abstractSpaces) { + await space.standard.loadSpaces(); + } + } + return this.abstractSpaces; + } + + // async loadGoodsShelvesSlot(reload: boolean = false): Promise { + // if (!this.goodsShelvesSlotLoaded || reload) { + // this.goodsShelvesSlotLoaded = true; + // this.goodsShelvesSlots = this.resource.goodsShelvesSlotColl.cache + // .filter((i) => i.abstractSpaceId === this.id) + // .map((i) => { + // return new GoodsShelvesSlot(i, this.abstractSpace); + // }); + // // this.wareHousings = this.xWareHousings + // // .filter((a) => !(a.parentId && a.parentId.length > 5)) + // // .map((i) => { + // // return new WareHousing(i, this.abstractSpace); + // // }); + // } + // return this.wareHousings; + // } + + async loadWareHousings(reload: boolean = false): Promise { + if (!this.wareHousingLoaded || reload) { + this.wareHousingLoaded = true; + this.wareHousings = this.resource.wareHousingSpaceColl.cache + .filter((i) => i.abstractSpaceId === this.id) + .map((i) => { + return new WareHousing(i, this.abstractSpace); + }); + // this.wareHousings = this.xWareHousings + // .filter((a) => !(a.parentId && a.parentId.length > 5)) + // .map((i) => { + // return new WareHousing(i, this.abstractSpace); + // }); + } + return this.wareHousings; + } + async createWareHousing( + data: schema.XWareHousingSpace, + ): Promise { + const result = await this.resource.wareHousingSpaceColl.insert({ + ...data, + typeName: '仓储空间', + abstractSpaceId: this.id, + }); + if (result) { + await this.resource.wareHousingSpaceColl.notity({ + data: result, + operate: 'insert', + }); + return result; + } + } + + async delete() { + await Promise.all(this.standardSpaces.map((item) => item.hardDelete())); + } +} +/** 订阅变更通知 */ +const subscribeNotity = (space: IAbstractSpace) => { + space.resource.abstractSpaceColl.subscribe([space.key], (data) => { + subscribeCallback(space, '空间', data); + }); + space.resource.wareHousingSpaceColl.subscribe([space.key], (data) => { + subscribeCallback(space, '仓储空间', data); + }); + space.resource.goodsShelvesSlotColl.subscribe([space.key], (data) => { + subscribeCallback(space, '库位', data); + }); +}; + +/** 订阅回调方法 */ +function subscribeCallback( + space: IAbstractSpace, + typeName: string, + data?: { operate?: string; data?: T }, +): boolean { + if (data && data.operate && data.data) { + const entity = data.data; + const operate = data.operate; + if (space.id === entity.abstractSpaceId) { + if ( + ['库位'].includes(entity.typeName) || + ('wareHousingId' in entity && (entity as any).wareHousingId) + ) { + for (const app of space.standard.wareHousings) { + if (app.receive(operate, entity)) { + return true; + } + } + return false; + } + switch (operate) { + case 'insert': + case 'remove': + standardSpaceChanged(space, typeName, operate, entity); + break; + case 'reload': + space.loadContent(true).then(() => { + space.changCallback(); + }); + return true; + default: + space.standard.standardSpaces + .find((i) => i.id === entity.id) + ?.receive(operate, entity); + break; + } + space.changCallback(); + return true; + } + for (const subspace of space.standard.abstractSpaces) { + if (subscribeCallback(subspace, typeName, data)) { + return true; + } + } + } + return false; +} + +/** 空间的变更 */ +function standardSpaceChanged( + space: IAbstractSpace, + typeName: string, + operate: string, + data: any, +): void { + console.log(typeName); + switch (typeName) { + case '空间': + space.standard.abstractSpaces = ArrayChanged( + space.standard.abstractSpaces, + operate, + data, + () => new AbstractSpace(data, space.target, space), + ); + if (operate === 'insert') { + space.resource.abstractSpaceColl.cache.push(data); + } else { + space.resource.abstractSpaceColl.removeCache((i) => i.id != data.id); + } + break; + case '仓储空间': + space.standard.wareHousings = ArrayChanged( + space.standard.wareHousings, + operate, + data, + () => new WareHousing(data, space), + ); + if (operate === 'insert') { + space.resource.wareHousingSpaceColl.cache.push(data); + } else { + space.resource.wareHousingSpaceColl.removeCache((i) => i.id != data.id); + } + break; + } +} + +/** 数组元素操作 */ +function ArrayChanged( + arr: T[], + operate: string, + data: schema.XAbstractSpaceStandard, + create: () => T, +): T[] { + if (operate === 'remove') { + return arr.filter((i) => i.id != data.id); + } + if (operate === 'insert') { + const index = arr.findIndex((i) => i.id === data.id); + if (index > -1) { + arr[index].setMetadata(data); + } else { + arr.push(create()); + } + } + return arr; +} diff --git a/src/ts/core/abstractSpace/standard/warehousing.ts b/src/ts/core/abstractSpace/standard/warehousing.ts new file mode 100644 index 0000000000000000000000000000000000000000..695fbcaafa7be346601c219a50469e70fe83f437 --- /dev/null +++ b/src/ts/core/abstractSpace/standard/warehousing.ts @@ -0,0 +1,354 @@ +import { model, schema } from '../../../base'; +import { + ISpace, + IStandardSpaceInfo, + StandardSpaceInfo, +} from '@/ts/core/abstractSpace/abstractSpaceInfo'; +import { IAbstractSpace, ITarget } from '@/ts/core'; +import { DataResource } from '@/ts/core/thing/resource'; +import { spaceOperates, wareHousingOperates } from '@/ts/core/public'; +import { + GoodsShelvesSlot, + IGoodsShelvesSlot, +} from '@/ts/core/abstractSpace/standard/goodsShelvesSlot'; +import { XGoodsShelves, XShelvesSlotRecord } from '@/ts/base/schema'; +interface Option { + value: string; + label: string; + children?: Option[]; +} +//仓库 +export interface IWareHousing extends IStandardSpaceInfo { + /** 当前的用户 */ + target: ITarget; + /** 资源集合 */ + resource: DataResource; + abstractSpace: IAbstractSpace; + shelves: XGoodsShelves[]; + /** 库位信息 */ + shelvesSlots: IGoodsShelvesSlot[]; + /** 新增货架信息 */ + addGoodsShelves(data: schema.XGoodsShelves): Promise; + /** 更新货架信息 */ + updateGoodsShelves(data: schema.XGoodsShelves): Promise; + /** 删除货架信息 */ + deleteGoodsShelves(data: schema.XGoodsShelves): Promise; + /** 新增货架库位信息 */ + addGoodsShelvesSlot(data: schema.XGoodsShelvesSlot[]): Promise; + loadThing(slotId: string, code?: string): Promise; + /** 上架 */ + AddToInventory(slotId: string, thingId: string[]): Promise; + /** 下架 */ + RemoveFromInventory(slotId: string, thingId: string[]): Promise; + ///** 加载货架库位信息 */ + //loadGoodsShelvesSlots(reload?: boolean): Promise; + //记录 + //record(formid, thingid) +} + +export class WareHousing + extends StandardSpaceInfo + implements IWareHousing +{ + constructor(_metadata: schema.XWareHousingSpace, _space: IAbstractSpace) { + super(_metadata, _space.resource.wareHousingSpaceColl); + this.setEntity(); + this.abstractSpace = _space; + this.target = _space.target; + this.resource = _space.resource; + } + abstractSpace: IAbstractSpace; + resource: DataResource; + target: ITarget; + shelves: XGoodsShelves[] = []; + shelvesSlots: IGoodsShelvesSlot[] = []; + private _slotsLoaded: boolean = false; + get selectOpt(): Option[] { + return this.shelves.map((shelf) => ({ + value: shelf.id, + label: shelf.name, + children: + this.shelvesSlots + .filter((item: IGoodsShelvesSlot) => item.metadata?.goodsShelveId === shelf.id) + ?.map((slot) => ({ + value: slot.id, + label: slot.name, + })) || [], // 如果没有库位则返回空数组 + })); + } + async addGoodsShelves(data: schema.XGoodsShelves): Promise { + if (!this._metadata.goodsShelves) { + this._metadata.goodsShelves = []; + } + this._metadata.goodsShelves.push(data); + const result = await this.coll.replace({ + ...this.metadata, + }); + if (result) { + this.notify('replace', result); + return true; + } + return false; + } + + async updateGoodsShelves(data: schema.XGoodsShelves): Promise { + const index = this._metadata.goodsShelves.findIndex((shelf) => shelf.id === data.id); + if (index !== -1) { + this._metadata.goodsShelves[index] = data; + } + const result = await this.coll.replace({ + ...this.metadata, + }); + if (result) { + this.notify('replace', result); + return true; + } + return false; + } + + async deleteGoodsShelves(data: schema.XGoodsShelves): Promise { + this._metadata.goodsShelves = this._metadata.goodsShelves.filter( + (shelf) => shelf.id !== data.id, + ); + const result = await this.coll.replace({ + ...this.metadata, + }); + if (result) { + await this.deleteSolts(data.id); + this.notify('replace', result); + return true; + } + return false; + } + + async deleteSolts(shelfId: string) { + this.shelvesSlots + .filter((slot) => slot.metadata.goodsShelveId === shelfId) + .map((slot) => { + slot.delete(); + }); + } + + async addGoodsShelvesSlot(data: schema.XGoodsShelvesSlot[]): Promise { + const extendedData = data.map((data: schema.XGoodsShelvesSlot) => ({ + ...data, + typeName: '库位', + abstractSpaceId: this.abstractSpace.id, + wareHousingId: this.id, + })); + const result = await this.resource.goodsShelvesSlotColl.insertMany(extendedData); + if (result) { + await this.resource.goodsShelvesSlotColl.notity({ + data: result, + operate: 'insert', + }); + return true; + } + return false; + } + + async AddToInventory(slotId: string, thingId: string[]): Promise { + const slot = this.shelvesSlots.find((slot) => slot.id === slotId); + if (slot) { + const result = await slot.AddToInventory(thingId); + if (result) { + console.log(await this.record(slot, 'add', thingId)); + } + return result; + } + return false; + } + + async RemoveFromInventory(slotId: string, thingId: string[]): Promise { + const slot = this.shelvesSlots.find((slot) => slot.id === slotId); + if (slot) { + const result = await slot.RemoveFromInventory(thingId); + if (result) { + console.log(await this.record(slot, 'remove', thingId)); + // await this.record(slot, 'remove', thingId); + } + return result; + } + return false; + } + + async loadThing(slotId: string, code?: string): Promise { + const slot = this.shelvesSlots.find((slot) => slot.id === slotId); + if (code) { + const res = await this.resource.thingColl.loadResult({ + options: { + match: { + id: code, + isDeleted: false, + }, + project: { + archives: 0, + }, + }, + }); + return res.data ?? []; + } + const res = await this.resource.thingColl.loadResult({ + options: { + match: { + id: { _in_: slot?.metadata?.inventory }, + isDeleted: false, + }, + project: { + archives: 0, + }, + }, + }); + return res.data ?? []; + } + + async record( + slot: IGoodsShelvesSlot, + operate: string, + thingId: string[], + ): Promise { + return await this.resource.shelvesSlotRecordColl.insertMany( + thingId.map( + (p) => + ({ + operate: operate, + wareHousingId: slot.metadata.wareHousingId, + //归属货架 + goodsShelveId: slot.metadata.goodsShelveId, + //库位 + goodsShelvesSlotId: slot.metadata.id, + //物资 + goodsId: p, + } as XShelvesSlotRecord), + ), + ); + } + + get id(): string { + return this._metadata.id; + } + + get superior(): ISpace { + return this.abstractSpace; + } + + get cacheFlag(): string { + return 'wareHousings'; + } + + get isContainer(): boolean { + return false; + } + + /** 库位总容量 */ + get totalSlotsCapacity(): number { + return this.shelvesSlots.reduce((acc, slot) => acc + slot.metadata.capacity, 0); + } + /** 库位使用容量 */ + get usedSlotsCapacity(): number { + return this.shelvesSlots.reduce( + (acc, slot) => acc + (slot.metadata?.inventory?.length ?? 0), + 0, + ); + } + /** 库位利用率 */ + get utilization(): number { + return this.totalSlotsCapacity > 0 + ? Number(((this.usedSlotsCapacity / this.totalSlotsCapacity) * 100).toFixed(2)) + : 0; + } + + content(): ISpace[] { + const cnt = [...this.shelvesSlots]; + return cnt.sort((a, b) => (a.metadata.updateTime < b.metadata.updateTime ? 1 : -1)); + } + + override operates(): model.OperateModel[] { + const operates: model.OperateModel[] = []; + operates.push( + wareHousingOperates.InBound, + wareHousingOperates.Outbound, + wareHousingOperates.Inventory, + ); + operates.push(spaceOperates.Refesh, ...super.operates()); + return operates; + } + + async loadContent(reload: boolean = false): Promise { + this.shelves = this.metadata.goodsShelves; + await this.loadGoodsShelvesSlots(reload); + return true; + } + + private async loadGoodsShelvesSlots(reload?: boolean): Promise { + if (!this._slotsLoaded || reload) { + const res = await this.resource.goodsShelvesSlotColl.loadResult({ + options: { + match: { + abstractSpaceId: this.abstractSpace.id, + wareHousingId: this.id, + isDeleted: false, + }, + project: { resource: 0 }, + }, + }); + this._slotsLoaded = true; + if (res.success) { + this.shelvesSlots = (res.data || []).map( + (a) => new GoodsShelvesSlot(a, this.abstractSpace, this), + ); + } + } + return this.shelvesSlots; + } + + slotReceive(operate: string, data: schema.XGoodsShelvesSlot): boolean { + switch (operate) { + case 'insert': + { + this.resource.goodsShelvesSlotColl.cache.push(data); + this.shelvesSlots.push(new GoodsShelvesSlot(data, this.abstractSpace, this)); + } + break; + case 'remove': + this.resource.goodsShelvesSlotColl.removeCache((i) => i.id !== data.id); + this.shelvesSlots = this.shelvesSlots.filter((a) => a.id !== data.id); + break; + case 'delete': + case 'replace': + var form = this.shelvesSlots.find((a) => a.id === data.id); + if (data && form) { + if (operate === 'delete') { + data = { ...data, isDeleted: true } as unknown as schema.XGoodsShelvesSlot; + } + form.setMetadata(data); + } + break; + default: + break; + } + return true; + } + + override receive(operate: string, data: schema.XWareHousingSpace): boolean { + if (data.shareId === this.target.id) { + if (data.id === this.id) { + this.coll.removeCache((i) => i.id != data.id); + super.receive(operate, data); + this.coll.cache.push(this._metadata); + return true; + } else if ('wareHousingId' in data && data.wareHousingId === this.id) { + switch (data.typeName) { + case '库位': + this.slotReceive(operate, data as unknown as schema.XGoodsShelvesSlot); + break; + default: + break; + } + this.changCallback(); + return true; + } + } + return false; + } +} diff --git a/src/ts/core/index.ts b/src/ts/core/index.ts index 03b5555c934a3961c7527b420473dd054fa48ca3..51c5cecc451ddfdaf0c1a01649377a81b90d00e8 100644 --- a/src/ts/core/index.ts +++ b/src/ts/core/index.ts @@ -50,3 +50,4 @@ export type { IPeriod } from './work/financial/period'; export type { IWorkTask, TaskTypeName } from './work/task'; export type { IOrder } from './mall/order/order'; export type { IAgent } from './target/team/agent'; +export type { IAbstractSpace } from './abstractSpace/abstractSpace'; diff --git a/src/ts/core/public/consts.ts b/src/ts/core/public/consts.ts index 2d2e40a7196a553234f287c0f9c8a5512323f9cb..8a28260d64b00047387880adc1cfab372b3510a6 100644 --- a/src/ts/core/public/consts.ts +++ b/src/ts/core/public/consts.ts @@ -41,6 +41,7 @@ export const valueTypes = [ ValueType.Map, ValueType.Object, ValueType.TimeArray, + ValueType.Space, ]; /** 表单弹框支持的类型 */ export const formModalType = { diff --git a/src/ts/core/public/enums.ts b/src/ts/core/public/enums.ts index 78b612786e42ae87fb87a69517644b126c6ff53d..28cce8d9bf78b8cb1620d869e6990b1c42f7e0ec 100644 --- a/src/ts/core/public/enums.ts +++ b/src/ts/core/public/enums.ts @@ -107,6 +107,7 @@ export enum ValueType { 'Object' = '对象型', 'Currency' = '货币型', 'TimeArray' = '时间段型', + 'Space' = '空间型', } /** 规则触发时机 */ @@ -151,4 +152,4 @@ export enum MallTemplateMode { sharing = '共享', // 交易 trading = '交易', -} \ No newline at end of file +} diff --git a/src/ts/core/public/index.ts b/src/ts/core/public/index.ts index 3cf8ea4021f722b43f7d08e3c2293315210fdfb0..073616f8807d05364ecb27544f1406b5eecfc46d 100644 --- a/src/ts/core/public/index.ts +++ b/src/ts/core/public/index.ts @@ -16,8 +16,9 @@ export { entityOperates, fileOperates, memberOperates, - newWarehouse, personJoins, + spaceOperates, + wareHousingOperates, targetOperates, teamOperates, } from './operates'; diff --git a/src/ts/core/public/operates.ts b/src/ts/core/public/operates.ts index 10302867c47753af011edc3f6ff1fb6a51e949c3..0371ea08dcd8194065692765c565dc3328fb8987 100644 --- a/src/ts/core/public/operates.ts +++ b/src/ts/core/public/operates.ts @@ -462,20 +462,54 @@ export const applicationNew = { ], }; -/** 新建仓库 */ -export const newWarehouse = { +export const wareHousingOperates = { + InBound: { + sort: 4, + cmd: 'addToInventory', + label: '入库', + iconType: '', + }, + Outbound: { + sort: 5, + cmd: 'removeFromInventory', + label: '出库', + iconType: '', + }, + Inventory: { + sort: 6, + cmd: 'inventory', + label: '盘点', + iconType: '', + }, +}; + +export const spaceOperates = { + Refesh: { + sort: 2, + cmd: 'reload', + label: '刷新空间', + iconType: 'refresh', + }, + NewSpace: { + sort: 3, + cmd: 'newSpace', + label: '新建空间', + iconType: 'newDir', + }, + NewWareHousingSpace: { + sort: 4, + cmd: 'newWareHousing', + label: '新建仓储空间', + iconType: 'newDir', + }, +}; + +export const areaNew = { sort: 0, - cmd: 'newWarehouses', - label: '仓库管理', - iconType: 'newWarehouses', - menus: [ - { - sort: -1, - cmd: 'newWarehouse', - label: '新建仓库', - iconType: 'newWarehouse', - }, - ], + cmd: 'new', + label: '新建更多', + iconType: 'new', + menus: [spaceOperates.NewSpace, spaceOperates.NewWareHousingSpace], }; /** 团队的操作 */ diff --git a/src/ts/core/target/base/target.ts b/src/ts/core/target/base/target.ts index 023823d02d0aaa6205c4d2e271de795f08aa58ea..9cbc3da3212d49a84236d2e124ae219c09d79f8f 100644 --- a/src/ts/core/target/base/target.ts +++ b/src/ts/core/target/base/target.ts @@ -18,7 +18,7 @@ import { XObject } from '../../public/object'; import { WebSiteProvider } from '../../provider/website'; import { DomainProvider } from '../../provider/domain'; import { RelationType } from '@/ts/base/enum'; - +import { AbstractSpace, IAbstractSpace } from '@/ts/core/abstractSpace/abstractSpace'; /** 用户抽象接口类 */ export interface ITarget extends ITeam, IFileInfo { /** 会话 */ @@ -37,6 +37,8 @@ export interface ITarget extends ITeam, IFileInfo { chats: ISession[]; /** 当前目录 */ directory: IDirectory; + /** 当前空间 */ + abstractSpace: IAbstractSpace; /** 数据核 */ storeId: string; /** 接受的文件 */ @@ -94,6 +96,15 @@ export abstract class Target extends Team implements ITarget { } as unknown as schema.XDirectory, this, ); + this.abstractSpace = new AbstractSpace( + { + ..._metadata, + shareId: _metadata.id, + id: _metadata.id + '_', + typeName: '空间', + } as unknown as schema.XAbstractSpace, + this, + ); this.memberDirectory = new MemberDirectory(this); this.session = new Session(this.id, this, _metadata); this.recorder = new Recorder(this); @@ -118,6 +129,7 @@ export abstract class Target extends Team implements ITarget { space: IBelong; session: ISession; directory: IDirectory; + abstractSpace: IAbstractSpace; resource: DataResource; cache: schema.XCache; identitys: IIdentity[] = []; diff --git a/src/ts/core/target/person.ts b/src/ts/core/target/person.ts index 2621d05e557a37e48532389349d4dc3577328762..9b820b6a89cc4aabd2cf3f0d7c42f668765293a5 100644 --- a/src/ts/core/target/person.ts +++ b/src/ts/core/target/person.ts @@ -334,6 +334,7 @@ export class Person extends Belong implements IPerson { this._loadCommons(), this.loadCompanys(reload), this.directory.loadDirectoryResource(reload), + this.abstractSpace.loadSpaceResource(reload), ]); setTimeout(async () => { await Promise.all([ diff --git a/src/ts/core/target/team/company.ts b/src/ts/core/target/team/company.ts index a44de3a677ada2f37550bcf0e0e2ef43e66f6451..491be3d2b473086f5a49bc30df1c1fb86532e9aa 100644 --- a/src/ts/core/target/team/company.ts +++ b/src/ts/core/target/team/company.ts @@ -305,6 +305,7 @@ export class Company extends Belong implements ICompany { this.loadDepartments(reload), ]); await this.directory.loadDirectoryResource(reload); + await this.abstractSpace.loadSpaceResource(reload); setTimeout(async () => { await Promise.all( this.departments.map((department) => department.deepLoad(reload)), diff --git a/src/ts/core/thing/resource.ts b/src/ts/core/thing/resource.ts index 4720edf69887d8b5d0af66578c2aae2e61992d09..bbf3d8615c5aefac87de115b3a4d8803387213e8 100644 --- a/src/ts/core/thing/resource.ts +++ b/src/ts/core/thing/resource.ts @@ -33,6 +33,9 @@ import { XAssignTaskTree, XAssignTaskTreeNode, XOrder, XAttribute, + XAbstractSpace, + XWareHousingSpace, + XGoodsShelvesSlot, XShelvesSlotRecord, } from '../../base/schema'; import { BucketOpreates, ChatMessageType, Transfer } from '@/ts/base/model'; import { kernel, model } from '@/ts/base'; @@ -44,6 +47,7 @@ export class DataResource { private target: XTarget; private relations: string[]; private _proLoaded: boolean = false; + private _preSpaceLoaded: boolean = false; constructor(target: XTarget, relations: string[], keys: string[]) { this._keys = keys; this.target = target; @@ -93,6 +97,16 @@ export class DataResource { this.assignTaskTreePublicColl = this.genTargetColl('-assign-task-tree'); this.attributeColl = this.genTargetColl('standard-form-attribute'); + this.abstractSpaceColl = this.genTargetColl('resource-space'); + this.wareHousingSpaceColl = this.genTargetColl( + 'standard-space-warehousing', + ); + this.goodsShelvesSlotColl = this.genTargetColl( + 'standard-space-shelves-slot', + ); + this.shelvesSlotRecordColl = this.genTargetColl( + 'standard-inventory-record', + ); } /** 表单集合 */ formColl: XCollection; @@ -170,6 +184,13 @@ export class DataResource { assignTaskTreePublicColl: XCollection; /** 属性集合 */ attributeColl: XCollection; + /** 抽象空间集合 */ + abstractSpaceColl: XCollection; + /** 仓储空间集合 */ + wareHousingSpaceColl: XCollection; + /** 货架库位集合 */ + goodsShelvesSlotColl: XCollection; + shelvesSlotRecordColl: XCollection; /** 资源对应的用户信息 */ get targetMetadata() { return this.target; @@ -184,6 +205,15 @@ export class DataResource { ]); } } + async preSpaceLoad(reload: boolean = false): Promise { + if (!this._preSpaceLoaded || reload) { + this._preSpaceLoaded = true; + await Promise.all([ + this.abstractSpaceColl.all(reload), + this.wareHousingSpaceColl.all(reload), + ]); + } + } /** 生成集合 */ genColl(collName: string, relations?: string[]): XCollection { return new XCollection(