Files
qhmes/jeecgboot-vue3/src/views/xslmes/mesXslWarehouse/MesXslWarehouseList.vue

663 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="mes-xsl-warehouse-page">
<div class="mes-xsl-warehouse-layout">
<div class="mes-xsl-warehouse-sider-col">
<aside
:class="['mes-xsl-warehouse-sider', { 'is-collapsed': siderCollapsed, 'is-dragging': siderDragging }]"
:style="siderAsideStyle"
>
<Card class="mes-xsl-warehouse-sider-card" size="small" title="仓库分类" :bordered="true">
<template #extra>
<a-space v-show="!siderCollapsed" size="small" class="mes-xsl-warehouse-sider-extra">
<a-button type="link" size="small" v-auth="'xslmes:mes_xsl_warehouse_category:add'" @click="handleAddCategory">新增</a-button>
<a-button
type="link"
size="small"
v-auth="'xslmes:mes_xsl_warehouse_category:edit'"
:disabled="!categorySelectedId"
@click="handleEditCategory"
>
编辑
</a-button>
<a-button
type="link"
size="small"
danger
v-auth="'xslmes:mes_xsl_warehouse_category:delete'"
:disabled="!categorySelectedId"
@click="handleDeleteCategory"
>
删除
</a-button>
</a-space>
</template>
<Spin :spinning="treeLoading">
<BasicTree
:treeData="categoryTreeData"
:selectedKeys="selectedKeys"
defaultExpandLevel="2"
@update:selectedKeys="onTreeSelect"
/>
</Spin>
</Card>
</aside>
<!-- 分隔条贴在侧栏右缘三角把手 + 拖拽改宽度与单位管理一致 -->
<div
class="mes-xsl-warehouse-resizer"
:class="{ 'is-dragging': siderDragging }"
role="separator"
aria-orientation="vertical"
:aria-valuenow="siderCollapsed ? 0 : siderWidth"
:aria-valuemin="SIDER_MIN"
:aria-valuemax="SIDER_MAX"
tabindex="0"
@pointerdown="onSiderResizerPointerDown"
@keydown.left.prevent="nudgeSiderWidth(-16)"
@keydown.right.prevent="nudgeSiderWidth(16)"
>
<a-tooltip :title="siderCollapsed ? '展开(可向右拖拽)' : '收起(点击)或左右拖拽调整宽度'">
<span class="mes-xsl-warehouse-resizer-knob" aria-hidden="true">
<span class="mes-xsl-warehouse-tri" :class="{ 'mes-xsl-warehouse-tri--collapsed': siderCollapsed }" />
</span>
</a-tooltip>
</div>
</div>
<div class="mes-xsl-warehouse-main">
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" v-auth="'xslmes:mes_xsl_warehouse:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'xslmes:mes_xsl_warehouse:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_warehouse:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined" />
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'xslmes:mes_xsl_warehouse:deleteBatch'">
批量操作
<Icon icon="mdi:chevron-down" />
</a-button>
</a-dropdown>
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
</div>
</div>
<MesXslWarehouseModal @register="registerModal" @success="handleSuccess" />
<MesXslWarehouseSysCategoryModal @register="registerCategoryModal" @success="onCategorySuccess" />
<MesXslWarehouseAreaBatchAddModal @register="registerBatchAreaModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" name="xslmes-mesXslWarehouse" setup>
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
import { Card, Spin } from 'ant-design-vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { BasicTree } from '/@/components/Tree';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { defHttp } from '/@/utils/http/axios';
import MesXslWarehouseModal from './components/MesXslWarehouseModal.vue';
import MesXslWarehouseSysCategoryModal from './components/MesXslWarehouseSysCategoryModal.vue';
import MesXslWarehouseAreaBatchAddModal from './components/MesXslWarehouseAreaBatchAddModal.vue';
import { columns, searchFormSchema, superQuerySchema, WH_CATEGORY_CUSTOMER_CODE, WH_CATEGORY_SUPPLIER_CODE } from './MesXslWarehouse.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, updateStatus } from './MesXslWarehouse.api';
import { loadTreeData as loadCategoryTreeRoot } from '/@/views/system/category/category.api';
import {
fetchWarehouseCategoryRoot,
deleteWarehouseSysCategory,
WAREHOUSE_CATEGORY_ROOT_CODE,
} from './MesXslWarehouseSysCategory.api';
import Icon from '/@/components/Icon';
import type { KeyType } from '/@/components/Tree/src/types/tree';
import type { Recordable } from '/@/types/global';
const { createMessage, createConfirm } = useMessage();
const TREE_ALL = 'ALL';
/** 侧栏宽度限制px与单位管理一致 */
const SIDER_MIN = 200;
const SIDER_MAX = 560;
const SIDER_DEFAULT = 260;
const SIDER_DRAG_THRESHOLD = 5;
const SIDER_EXPAND_DRAG = 12;
const siderCollapsed = ref(false);
const siderWidth = ref(SIDER_DEFAULT);
const siderDragging = ref(false);
const treeLoading = ref(false);
const siderAsideStyle = computed(() => ({
width: siderCollapsed.value ? '0px' : `${siderWidth.value}px`,
}));
let siderDragStartX = 0;
let siderDragStartW = 0;
let siderDragMaxDelta = 0;
let siderDragCollapsedStart = false;
let siderDragPointerId: number | null = null;
let siderDragEl: HTMLElement | null = null;
function toggleSider() {
siderCollapsed.value = !siderCollapsed.value;
}
function clampSiderW(w: number) {
return Math.min(SIDER_MAX, Math.max(SIDER_MIN, w));
}
function nudgeSiderWidth(delta: number) {
if (siderCollapsed.value) {
if (delta > 0) {
siderCollapsed.value = false;
} else {
return;
}
}
siderWidth.value = clampSiderW(siderWidth.value + delta);
}
function onSiderResizerPointerMove(e: PointerEvent) {
if (siderDragPointerId == null || e.pointerId !== siderDragPointerId) return;
const dx = e.clientX - siderDragStartX;
siderDragMaxDelta = Math.max(siderDragMaxDelta, Math.abs(dx));
if (siderDragCollapsedStart) {
if (dx >= SIDER_EXPAND_DRAG) {
siderCollapsed.value = false;
siderWidth.value = clampSiderW(dx);
siderDragStartX = e.clientX;
siderDragStartW = siderWidth.value;
siderDragCollapsedStart = false;
}
} else {
siderWidth.value = clampSiderW(siderDragStartW + dx);
}
}
function endSiderDrag(e: PointerEvent) {
if (siderDragPointerId == null || e.pointerId !== siderDragPointerId) return;
if (siderDragEl) {
try {
siderDragEl.releasePointerCapture(siderDragPointerId);
} catch {
/* ignore */
}
}
document.removeEventListener('pointermove', onSiderResizerPointerMove);
document.removeEventListener('pointerup', endSiderDrag);
document.removeEventListener('pointercancel', endSiderDrag);
document.body.style.cursor = '';
document.body.style.userSelect = '';
siderDragging.value = false;
siderDragPointerId = null;
siderDragEl = null;
if (siderDragMaxDelta < SIDER_DRAG_THRESHOLD) {
toggleSider();
}
}
function onSiderResizerPointerDown(e: PointerEvent) {
if (e.button !== 0) return;
e.preventDefault();
siderDragEl = e.currentTarget as HTMLElement;
siderDragPointerId = e.pointerId;
siderDragStartX = e.clientX;
siderDragStartW = siderCollapsed.value ? 0 : siderWidth.value;
siderDragMaxDelta = 0;
siderDragCollapsedStart = siderCollapsed.value;
siderDragging.value = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
try {
siderDragEl.setPointerCapture(e.pointerId);
} catch {
/* ignore */
}
document.addEventListener('pointermove', onSiderResizerPointerMove);
document.addEventListener('pointerup', endSiderDrag);
document.addEventListener('pointercancel', endSiderDrag);
}
onUnmounted(() => {
document.removeEventListener('pointermove', onSiderResizerPointerMove);
document.removeEventListener('pointerup', endSiderDrag);
document.removeEventListener('pointercancel', endSiderDrag);
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
/** 分类字典异步树根pcode=XSLMES_WH 下的子树:一楼库、二楼仓库…) */
const rawWarehouseCategoryTree = ref<Recordable[]>([]);
/** 根节点 id编码 XSLMES_WH新增「顶级子节点」时用 */
const warehouseCategoryRootId = ref('');
const queryParam = reactive<any>({});
const selectedKeys = ref<KeyType[]>([TREE_ALL]);
const categorySelectedId = computed(() => {
const k = selectedKeys.value[0];
if (!k || k === TREE_ALL) return '';
return String(k);
});
function mapCategoryTreeNodes(nodes: Recordable[]): Recordable[] {
return (nodes || []).map((n) => ({
key: n.key,
title: n.title,
children: n.children?.length ? mapCategoryTreeNodes(n.children as Recordable[]) : undefined,
}));
}
const categoryTreeData = computed(() => [
{
key: TREE_ALL,
title: '全部分类',
children: mapCategoryTreeNodes(rawWarehouseCategoryTree.value || []),
},
]);
function collectLeafKeysUnder(node: Recordable): string[] {
const children = node.children as Recordable[] | undefined;
if (!children?.length) {
return [String(node.key)];
}
return children.flatMap((ch) => collectLeafKeysUnder(ch));
}
function findNodeByKey(nodes: Recordable[], key: string): Recordable | null {
for (const n of nodes) {
if (String(n.key) === key) {
return n;
}
const nested = n.children as Recordable[] | undefined;
if (nested?.length) {
const found = findNodeByKey(nested, key);
if (found) {
return found;
}
}
}
return null;
}
async function loadCategoryTree() {
treeLoading.value = true;
try {
const [root, res] = await Promise.all([fetchWarehouseCategoryRoot(), loadCategoryTreeRoot({ async: false, pcode: 'XSLMES_WH' })]);
warehouseCategoryRootId.value = root?.id != null ? String(root.id) : '';
rawWarehouseCategoryTree.value = Array.isArray(res) ? res : [];
if (!warehouseCategoryRootId.value || !rawWarehouseCategoryTree.value.length) {
createMessage.warning('未加载到仓库分类树,请确认已执行库脚本并已在「分类字典」中维护根节点 XSLMES_WH。');
}
} catch {
warehouseCategoryRootId.value = '';
rawWarehouseCategoryTree.value = [];
createMessage.warning('加载分类字典失败,请检查分类根编码 XSLMES_WH 是否存在。');
} finally {
treeLoading.value = false;
}
}
const [registerModal, { openModal }] = useModal();
const [registerCategoryModal, { openModal: openCategoryModal }] = useModal();
const [registerBatchAreaModal, { openModal: openBatchAreaModal }] = useModal();
function handleAddCategory() {
const rootId = warehouseCategoryRootId.value;
if (!rootId) {
createMessage.warning('未找到仓库分类根节点请确认分类字典中存在编码「XSLMES_WH」');
return;
}
const sel = categorySelectedId.value;
const parentId = sel || rootId;
openCategoryModal(true, { isUpdate: false, parentId });
}
function handleEditCategory() {
const id = categorySelectedId.value;
if (!id) {
createMessage.warning('请先选择左侧分类');
return;
}
openCategoryModal(true, { isUpdate: true, record: { id } });
}
async function handleDeleteCategory() {
const id = categorySelectedId.value;
if (!id) {
createMessage.warning('请先选择要删除的分类');
return;
}
if (id === warehouseCategoryRootId.value) {
createMessage.warning('根分类不可删除');
return;
}
let code = '';
try {
const cat = await defHttp.get<Recordable>({ url: '/sys/category/queryById', params: { id } });
code = cat?.code != null ? String(cat.code) : '';
} catch {
createMessage.error('无法读取分类信息');
return;
}
if (code === WAREHOUSE_CATEGORY_ROOT_CODE) {
createMessage.warning('根分类不可删除');
return;
}
if (code === WH_CATEGORY_CUSTOMER_CODE || code === WH_CATEGORY_SUPPLIER_CODE) {
createMessage.warning('「客户库」「供应商库」为业务保留分类,不建议删除');
return;
}
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '将删除该节点及其下级分类;已选用这些分类的仓库数据不会自动变更,请确认无业务影响。',
onOk: async () => {
await deleteWarehouseSysCategory(id);
await loadCategoryTree();
selectedKeys.value = [TREE_ALL];
delete queryParam.warehouseCategory;
delete queryParam.warehouseCategory_MultiString;
reload();
},
});
}
function onCategorySuccess() {
loadCategoryTree();
reload();
}
const { tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '仓库管理',
api: list,
columns,
// 避免:表格默认 immediate 请求一次 + onMounted 末尾 reload 再请求一次(进入页列表闪两次)
immediate: false,
canResize: true,
formConfig: {
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
},
actionColumn: {
width: 300,
fixed: 'right',
},
beforeFetch: (params) => Object.assign(params, queryParam),
},
exportConfig: {
name: '仓库管理',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
const superQueryConfig = reactive(superQuerySchema);
onMounted(async () => {
await loadCategoryTree();
reload();
});
function onTreeSelect(keys: KeyType[]) {
selectedKeys.value = keys;
const k = keys[0];
delete queryParam.warehouseCategory;
delete queryParam.warehouseCategory_MultiString;
if (!k || k === TREE_ALL) {
reload();
return;
}
const node = findNodeByKey(rawWarehouseCategoryTree.value, String(k));
if (!node) {
reload();
return;
}
const leafKeys = collectLeafKeysUnder(node);
if (leafKeys.length === 1) {
queryParam.warehouseCategory = leafKeys[0];
} else if (leafKeys.length > 1) {
queryParam.warehouseCategory_MultiString = leafKeys.join(',');
}
reload();
}
function handleSuperQuery(params) {
Object.keys(params).forEach((k) => {
queryParam[k] = params[k];
});
reload();
}
function handleAdd() {
openModal(true, { isUpdate: false, showFooter: true, record: {} });
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true, showFooter: true });
}
function handleDetail(record: Recordable) {
openModal(true, { record, isUpdate: true, showFooter: false });
}
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
function isRecordEnabled(record: Recordable) {
return record.status === '0' || record.status === 0;
}
async function handleToggleStatus(record: Recordable, status: string) {
await updateStatus({ id: record.id, status }, handleSuccess);
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
function handleSuccess() {
selectedRowKeys.value = [];
reload();
}
function getTableAction(record) {
const enabled = isRecordEnabled(record);
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'xslmes:mes_xsl_warehouse:edit',
},
{
label: '启用',
ifShow: !enabled,
onClick: handleToggleStatus.bind(null, record, '0'),
auth: 'xslmes:mes_xsl_warehouse:updateStatus',
},
{
label: '停用',
ifShow: enabled,
onClick: handleToggleStatus.bind(null, record, '1'),
auth: 'xslmes:mes_xsl_warehouse:updateStatus',
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
auth: 'xslmes:mes_xsl_warehouse:delete',
},
];
}
function handleBatchAddArea(record: Recordable) {
openBatchAreaModal(true, { record });
}
function getDropDownAction(record) {
return [
{ label: '详情', onClick: handleDetail.bind(null, record) },
{ label: '批量添加库区', onClick: handleBatchAddArea.bind(null, record) },
];
}
</script>
<style lang="less" scoped>
.mes-xsl-warehouse-page {
display: flex;
flex-direction: column;
min-height: calc(100vh - 160px);
:deep(.ant-picker-range) {
width: 100%;
}
}
.mes-xsl-warehouse-layout {
display: flex;
flex: 1;
align-items: stretch;
gap: 0;
min-height: 0;
}
.mes-xsl-warehouse-sider-col {
display: flex;
flex-direction: row;
align-items: stretch;
flex: 0 0 auto;
min-height: 0;
}
.mes-xsl-warehouse-sider {
flex: 0 0 auto;
min-width: 0;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
transition: width 0.2s ease;
.mes-xsl-warehouse-sider-extra {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0;
max-width: 200px;
}
&.is-dragging {
transition: none;
}
&.is-collapsed {
pointer-events: none;
}
}
.mes-xsl-warehouse-sider-card {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:deep(.ant-card-head) {
min-height: 40px;
padding: 0 12px;
}
:deep(.ant-card-body) {
flex: 1;
overflow: auto;
min-height: 0;
padding: 8px 12px;
}
}
.mes-xsl-warehouse-resizer {
flex: 0 0 8px;
width: 8px;
position: relative;
z-index: 2;
cursor: col-resize;
touch-action: none;
user-select: none;
background: #fafafa;
margin-right: 2px;
&:hover,
&:focus-visible {
background: #f0f0f0;
}
&.is-dragging {
background: #e6f4ff;
}
}
.mes-xsl-warehouse-resizer-knob {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
width: 10px;
height: 44px;
pointer-events: none;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.mes-xsl-warehouse-tri {
display: block;
width: 0;
height: 0;
border-style: solid;
border-width: 5px 6px 5px 0;
border-color: transparent #595959 transparent transparent;
margin-left: -1px;
}
.mes-xsl-warehouse-tri--collapsed {
border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #595959;
margin-left: 0;
margin-right: -1px;
}
.mes-xsl-warehouse-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
min-height: 0;
}
</style>