{
+ 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 ? (
+
+ }
+ onClick={saveShelfInfo}
+ className={style.save_button}>
+ 保存
+
+
+
+ ) : (
+
}
+ onClick={() => setIsEditing(true)}
+ className={style.edit_button}>
+ 编辑
+
+ )}
+
+
+
+
+ {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 (
+
+
+
+
+
+
+ );
+};
+
+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(