feat(mes):迁移工序定义(pro_process)、工艺路线(pro_route)

pull/349/head
YunaiV 2026-05-25 09:09:14 +08:00
parent 272757995e
commit 01a1d3e001
36 changed files with 5141 additions and 0 deletions

View File

@ -0,0 +1,49 @@
import { requestClient } from '#/api/request';
export namespace MesProProcessContentApi {
/** MES 生产工序内容(操作步骤) */
export interface ProcessContent {
id?: number;
processId?: number;
sort?: number;
content?: string;
device?: string;
material?: string;
docUrl?: string;
remark?: string;
createTime?: Date;
}
}
/** 按工序编号查询工序内容列表 */
export function getProcessContentListByProcessId(processId: number) {
return requestClient.get<MesProProcessContentApi.ProcessContent[]>(
`/mes/pro/process-content/list-by-process?processId=${processId}`,
);
}
/** 查询工序内容详情 */
export function getProcessContent(id: number) {
return requestClient.get<MesProProcessContentApi.ProcessContent>(
`/mes/pro/process-content/get?id=${id}`,
);
}
/** 新增工序内容 */
export function createProcessContent(
data: MesProProcessContentApi.ProcessContent,
) {
return requestClient.post('/mes/pro/process-content/create', data);
}
/** 修改工序内容 */
export function updateProcessContent(
data: MesProProcessContentApi.ProcessContent,
) {
return requestClient.put('/mes/pro/process-content/update', data);
}
/** 删除工序内容 */
export function deleteProcessContent(id: number) {
return requestClient.delete(`/mes/pro/process-content/delete?id=${id}`);
}

View File

@ -0,0 +1,65 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProRouteApi {
/** MES 工艺路线 */
export interface Route {
id?: number;
code?: string;
name?: string;
description?: string;
status?: number;
remark?: string;
createTime?: Date;
}
}
/** 查询工艺路线分页 */
export function getRoutePage(params: PageParam) {
return requestClient.get<PageResult<MesProRouteApi.Route>>(
'/mes/pro/route/page',
{ params },
);
}
/** 查询工艺路线精简列表 */
export function getRouteSimpleList() {
return requestClient.get<MesProRouteApi.Route[]>(
'/mes/pro/route/simple-list',
);
}
/** 查询工艺路线详情 */
export function getRoute(id: number) {
return requestClient.get<MesProRouteApi.Route>(
`/mes/pro/route/get?id=${id}`,
);
}
/** 新增工艺路线 */
export function createRoute(data: MesProRouteApi.Route) {
return requestClient.post<number>('/mes/pro/route/create', data);
}
/** 修改工艺路线 */
export function updateRoute(data: MesProRouteApi.Route) {
return requestClient.put('/mes/pro/route/update', data);
}
/** 修改工艺路线状态 */
export function updateRouteStatus(id: number, status: number) {
return requestClient.put(
`/mes/pro/route/update-status?id=${id}&status=${status}`,
);
}
/** 删除工艺路线 */
export function deleteRoute(id: number) {
return requestClient.delete(`/mes/pro/route/delete?id=${id}`);
}
/** 导出工艺路线 Excel */
export function exportRoute(params: any) {
return requestClient.download('/mes/pro/route/export-excel', { params });
}

View File

@ -0,0 +1,70 @@
import { requestClient } from '#/api/request';
export namespace MesProRouteProcessApi {
/** MES 工艺路线工序 */
export interface RouteProcess {
id?: number;
routeId?: number;
processId?: number;
processCode?: string;
processName?: string;
sort?: number;
nextProcessId?: number;
nextProcessName?: string;
linkType?: number;
prepareTime?: number;
waitTime?: number;
colorCode?: string;
keyFlag?: boolean;
checkFlag?: boolean;
remark?: string;
createTime?: Date;
}
}
/** 按工艺路线查询工序列表 */
export function getRouteProcessListByRoute(routeId: number) {
return requestClient.get<MesProRouteProcessApi.RouteProcess[]>(
`/mes/pro/route-process/list-by-route?routeId=${routeId}`,
);
}
/** 按产品查询工序列表(自动查找关联的工艺路线) */
export function getRouteProcessListByProduct(productId: number) {
return requestClient.get<MesProRouteProcessApi.RouteProcess[]>(
`/mes/pro/route-process/list-by-product?productId=${productId}`,
);
}
/** 查询工艺路线工序详情 */
export function getRouteProcess(id: number) {
return requestClient.get<MesProRouteProcessApi.RouteProcess>(
`/mes/pro/route-process/get?id=${id}`,
);
}
/** 按工艺路线 + 工序精确查询工序配置 */
export function getRouteProcessByRouteAndProcess(
routeId: number,
processId: number,
) {
return requestClient.get<MesProRouteProcessApi.RouteProcess>(
'/mes/pro/route-process/get-by-route-and-process',
{ params: { processId, routeId } },
);
}
/** 新增工艺路线工序 */
export function createRouteProcess(data: MesProRouteProcessApi.RouteProcess) {
return requestClient.post('/mes/pro/route-process/create', data);
}
/** 修改工艺路线工序 */
export function updateRouteProcess(data: MesProRouteProcessApi.RouteProcess) {
return requestClient.put('/mes/pro/route-process/update', data);
}
/** 删除工艺路线工序 */
export function deleteRouteProcess(id: number) {
return requestClient.delete(`/mes/pro/route-process/delete?id=${id}`);
}

View File

@ -0,0 +1,48 @@
import { requestClient } from '#/api/request';
export namespace MesProRouteProductApi {
/** MES 工艺路线产品 */
export interface RouteProduct {
id?: number;
routeId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
specification?: string;
unitName?: string;
quantity?: number;
productionTime?: number;
timeUnitType?: string;
remark?: string;
createTime?: Date;
}
}
/** 按工艺路线查询产品列表 */
export function getRouteProductListByRoute(routeId: number) {
return requestClient.get<MesProRouteProductApi.RouteProduct[]>(
`/mes/pro/route-product/list-by-route?routeId=${routeId}`,
);
}
/** 查询工艺路线产品详情 */
export function getRouteProduct(id: number) {
return requestClient.get<MesProRouteProductApi.RouteProduct>(
`/mes/pro/route-product/get?id=${id}`,
);
}
/** 新增工艺路线产品 */
export function createRouteProduct(data: MesProRouteProductApi.RouteProduct) {
return requestClient.post<number>('/mes/pro/route-product/create', data);
}
/** 修改工艺路线产品 */
export function updateRouteProduct(data: MesProRouteProductApi.RouteProduct) {
return requestClient.put('/mes/pro/route-product/update', data);
}
/** 删除工艺路线产品 */
export function deleteRouteProduct(id: number) {
return requestClient.delete(`/mes/pro/route-product/delete?id=${id}`);
}

View File

@ -0,0 +1,57 @@
import { requestClient } from '#/api/request';
export namespace MesProRouteProductBomApi {
/** MES 工艺路线产品 BOM */
export interface RouteProductBom {
id?: number;
routeId?: number;
processId?: number;
productId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
specification?: string;
unitName?: string;
quantity?: number;
remark?: string;
createTime?: Date;
}
}
/** 查询工艺路线产品 BOM 列表 */
export function getRouteProductBomList(params: {
processId?: number;
productId?: number;
routeId: number;
}) {
return requestClient.get<MesProRouteProductBomApi.RouteProductBom[]>(
'/mes/pro/route-product-bom/list',
{ params },
);
}
/** 查询工艺路线产品 BOM 详情 */
export function getRouteProductBom(id: number) {
return requestClient.get<MesProRouteProductBomApi.RouteProductBom>(
`/mes/pro/route-product-bom/get?id=${id}`,
);
}
/** 新增工艺路线产品 BOM */
export function createRouteProductBom(
data: MesProRouteProductBomApi.RouteProductBom,
) {
return requestClient.post('/mes/pro/route-product-bom/create', data);
}
/** 修改工艺路线产品 BOM */
export function updateRouteProductBom(
data: MesProRouteProductBomApi.RouteProductBom,
) {
return requestClient.put('/mes/pro/route-product-bom/update', data);
}
/** 删除工艺路线产品 BOM */
export function deleteRouteProductBom(id: number) {
return requestClient.delete(`/mes/pro/route-product-bom/delete?id=${id}`);
}

View File

@ -0,0 +1,272 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessApi } from '#/api/mes/pro/process';
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
import { h } from 'vue';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 新增/修改生产工序的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '工序编码',
component: 'Input',
componentProps: {
maxLength: 64,
placeholder: '请输入工序编码',
},
rules: z.string().min(1, '工序编码不能为空').max(64),
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
try {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_PROCESS_CODE,
);
await formApi?.setFieldValue('code', code);
} catch (error) {
console.error(error);
}
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '工序名称',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入工序名称',
},
rules: z.string().min(1, '工序名称不能为空').max(100),
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
optionType: 'button',
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
{
fieldName: 'attention',
label: '工序说明',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
maxLength: 500,
placeholder: '请输入工序说明',
rows: 3,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工序编码',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入工序编码',
},
},
{
fieldName: 'name',
label: '工序名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入工序名称',
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
placeholder: '请选择状态',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProProcessApi.Process>['columns'] {
return [
{
field: 'code',
title: '工序编码',
minWidth: 150,
slots: {
default: 'code',
},
},
{ field: 'name', title: '工序名称', minWidth: 180 },
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{ field: 'remark', title: '备注', minWidth: 180 },
{
field: 'createTime',
title: '创建时间',
width: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 150,
fixed: 'right',
slots: {
default: 'actions',
},
},
];
}
/** 工序内容(操作步骤)表单 */
export function useContentFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'processId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'sort',
label: '序号',
component: 'InputNumber',
componentProps: {
class: '!w-full',
max: 999,
min: 1,
precision: 0,
},
rules: z.number().default(1),
},
{
fieldName: 'content',
label: '步骤说明',
component: 'Textarea',
componentProps: {
maxLength: 500,
placeholder: '请输入步骤说明',
rows: 3,
},
},
{
fieldName: 'device',
label: '辅助设备',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入辅助设备',
},
},
{
fieldName: 'material',
label: '辅助材料',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入辅助材料',
},
},
{
fieldName: 'docUrl',
label: '材料文档 URL',
component: 'Input',
componentProps: {
maxLength: 250,
placeholder: '请输入材料文档 URL',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 工序内容列表的字段 */
export function useContentGridColumns(): VxeTableGridOptions<MesProProcessContentApi.ProcessContent>['columns'] {
return [
{ field: 'sort', title: '序号', width: 80, align: 'center' },
{ field: 'content', title: '步骤说明', minWidth: 220 },
{ field: 'device', title: '辅助设备', width: 150 },
{ field: 'material', title: '辅助材料', width: 150 },
{ field: 'docUrl', title: '材料文档', minWidth: 180 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,158 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessApi } from '#/api/mes/pro/process';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProcess,
exportProcess,
getProcessPage,
} from '#/api/mes/pro/process';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建生产工序 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 编辑生产工序 */
function handleEdit(row: MesProProcessApi.Process) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
/** 查看生产工序详情 */
function handleDetail(row: MesProProcessApi.Process) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
/** 删除生产工序 */
async function handleDelete(row: MesProProcessApi.Process) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteProcess(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 导出生产工序 */
async function handleExport() {
const data = await exportProcess(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '生产工序.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProcessPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
isHover: true,
keyField: 'id',
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesProProcessApi.Process>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】工序设置、工艺流程"
url="https://doc.iocoder.cn/mes/pro/process-route/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工序']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-process:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-process:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:pro-process:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:pro-process:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProcessContent,
getProcessContent,
updateProcessContent,
} from '#/api/mes/pro/process/content';
import { $t } from '#/locales';
import { useContentFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProProcessContentApi.ProcessContent>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['工序步骤'])
: $t('ui.actionTitle.create', ['工序步骤']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useContentFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as MesProProcessContentApi.ProcessContent;
try {
await (formData.value?.id
? updateProcessContent(data)
: createProcessContent(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
await formApi.resetForm();
//
const data = modalApi.getData<{
id?: number;
maxSort?: number;
processId: number;
}>();
if (!data?.id) {
// = maxSort + 1
await formApi.setValues({
processId: data?.processId,
sort: (data?.maxSort || 0) + 1,
});
return;
}
modalApi.lock();
try {
formData.value = await getProcessContent(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,132 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProcessContent,
getProcessContentListByProcessId,
} from '#/api/mes/pro/process/content';
import { $t } from '#/locales';
import { useContentGridColumns } from '../data';
import ContentForm from './content-form.vue';
const props = defineProps<{
processId: number;
readonly?: boolean;
}>();
const list = ref<MesProProcessContentApi.ProcessContent[]>([]);
const [ContentFormModal, contentFormModalApi] = useVbenModal({
connectedComponent: ContentForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useContentGridColumns(),
data: list.value,
minHeight: 240,
pagerConfig: { enabled: false },
rowConfig: {
isHover: true,
keyField: 'id',
},
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProProcessContentApi.ProcessContent>,
});
/** 加载工序内容列表 */
async function getList() {
gridApi.setLoading(true);
try {
list.value = await getProcessContentListByProcessId(props.processId);
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
/** 新增工序步骤 */
function handleCreate() {
const maxSort =
list.value.length > 0
? Math.max(...list.value.map((item) => item.sort || 0))
: 0;
contentFormModalApi
.setData({ maxSort, processId: props.processId })
.open();
}
/** 编辑工序步骤 */
function handleEdit(row: MesProProcessContentApi.ProcessContent) {
contentFormModalApi.setData({ id: row.id, processId: props.processId }).open();
}
/** 删除工序步骤 */
async function handleDelete(row: MesProProcessContentApi.ProcessContent) {
await deleteProcessContent(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', ['工序步骤']));
await getList();
}
watch(
() => props.processId,
(value) => {
if (value) {
getList();
}
},
{ immediate: true },
);
</script>
<template>
<ContentFormModal @success="getList" />
<div v-if="!readonly" class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['步骤']),
type: 'primary',
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
ifShow: () => !readonly,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
ifShow: () => !readonly,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['工序步骤']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@ -0,0 +1,117 @@
<script lang="ts" setup>
import type { MesProProcessApi } from '#/api/mes/pro/process';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Divider, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProcess,
getProcess,
updateProcess,
} from '#/api/mes/pro/process';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import ContentList from './content-list.vue';
type FormMode = 'create' | 'detail' | 'update';
const emit = defineEmits(['success']);
const formMode = ref<FormMode>('create'); //
const formData = ref<MesProProcessApi.Process>(); // /
const isDetail = computed(() => formMode.value === 'detail'); //
const getTitle = computed(() => {
if (formMode.value === 'detail') {
return $t('ui.actionTitle.detail', ['生产工序']);
}
return formMode.value === 'update'
? $t('ui.actionTitle.edit', ['生产工序'])
: $t('ui.actionTitle.create', ['生产工序']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-3',
labelWidth: 100,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 表单 schema 需要 formApi 引用(生成编码按钮),所以通过 setState 设置 schema */
formApi.setState({ schema: useFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (isDetail.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as MesProProcessApi.Process;
try {
await (formData.value?.id ? updateProcess(data) : createProcess(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
await formApi.resetForm();
//
const data = modalApi.getData<{ id?: number; type?: FormMode }>();
formMode.value = data?.type ?? 'create';
formApi.setDisabled(formMode.value === 'detail');
modalApi.setState({ showConfirmButton: formMode.value !== 'detail' });
if (!data?.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProcess(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
/** 用于工序内容子表的工序编号 */
const processId = computed(() => formData.value?.id);
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 编辑/详情模式下展示工序操作步骤子表新增模式下隐藏 -->
<template v-if="processId">
<Divider class="!my-3" orientation="left">操作步骤</Divider>
<div class="mx-4">
<ContentList :process-id="processId" :readonly="isDetail" />
</div>
</template>
</Modal>
</template>

View File

@ -0,0 +1,335 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 工艺路线表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'code',
label: '路线编码',
component: 'Input',
componentProps: {
maxLength: 64,
placeholder: '请输入工艺路线编码',
},
rules: z.string().min(1, '路线编码不能为空').max(64),
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
try {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_ROUTE_CODE,
);
await formApi?.setFieldValue('code', code);
} catch (error) {
console.error(error);
}
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '路线名称',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入工艺路线名称',
},
rules: z.string().min(1, '路线名称不能为空').max(100),
},
{
fieldName: 'description',
label: '路线说明',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
maxLength: 500,
placeholder: '请输入工艺路线说明',
rows: 3,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 列表搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '路线编码',
component: 'Input',
componentProps: { allowClear: true, placeholder: '请输入路线编码' },
},
{
fieldName: 'name',
label: '路线名称',
component: 'Input',
componentProps: { allowClear: true, placeholder: '请输入路线名称' },
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
placeholder: '请选择状态',
},
},
];
}
/** 列表字段 */
export function useGridColumns(): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
return [
{
field: 'code',
title: '路线编码',
minWidth: 160,
slots: { default: 'code' },
},
{ field: 'name', title: '路线名称', minWidth: 180 },
{ field: 'description', title: '路线说明', minWidth: 200 },
{
field: 'status',
title: '状态',
width: 110,
slots: { default: 'status' },
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
field: 'createTime',
title: '创建时间',
width: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工艺路线工序明细表单 */
export function useRouteProcessFormSchema(
processOptions: Array<{ label: string; value: number }>,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'routeId',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'sort',
label: '序号',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 1, precision: 0 },
rules: z.number().default(1),
},
{
fieldName: 'processId',
label: '工序',
component: 'Select',
componentProps: {
allowClear: true,
options: processOptions,
placeholder: '请选择工序',
showSearch: true,
},
rules: 'selectRequired',
},
{
fieldName: 'linkType',
label: '与下道工序关系',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'),
placeholder: '请选择',
},
rules: z.number().default(3),
},
{
fieldName: 'colorCode',
label: '甘特图颜色',
component: 'Input',
componentProps: {
maxLength: 16,
placeholder: '请输入颜色 hex例如 #00AEF3',
},
},
{
fieldName: 'keyFlag',
label: '是否关键工序',
component: 'Switch',
componentProps: { checkedChildren: '是', unCheckedChildren: '否' },
rules: z.boolean().default(false),
},
{
fieldName: 'checkFlag',
label: '是否质检确认',
component: 'Switch',
componentProps: { checkedChildren: '是', unCheckedChildren: '否' },
rules: z.boolean().default(false),
},
{
fieldName: 'prepareTime',
label: '准备时间(分)',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 0 },
rules: z.number().default(0),
},
{
fieldName: 'waitTime',
label: '等待时间(分)',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 0 },
rules: z.number().default(0),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: { maxLength: 250, placeholder: '请输入备注', rows: 2 },
},
];
}
/** 工艺路线工序列表字段 */
export function useRouteProcessGridColumns(): VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>['columns'] {
return [
{ field: 'sort', title: '序号', width: 70, align: 'center', fixed: 'left' },
{ field: 'processCode', title: '工序编码', width: 140, fixed: 'left' },
{ field: 'processName', title: '工序名称', width: 140, fixed: 'left' },
{ field: 'nextProcessName', title: '下一道工序', width: 140 },
{
field: 'linkType',
title: '与下一道工序关系',
width: 160,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_LINK_TYPE },
},
},
{
field: 'keyFlag',
title: '关键工序',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'checkFlag',
title: '质检确认',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{ field: 'prepareTime', title: '准备时间(分)', width: 110 },
{ field: 'waitTime', title: '等待时间(分)', width: 110 },
{
field: 'colorCode',
title: '甘特图颜色',
width: 130,
slots: { default: 'colorCode' },
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工艺路线产品列表字段 */
export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] {
return [
{ field: 'itemCode', title: '产品物料编码', width: 150 },
{ field: 'itemName', title: '产品物料名称', width: 150 },
{ field: 'specification', title: '规格型号', width: 150 },
{ field: 'unitName', title: '单位', width: 80 },
{ field: 'quantity', title: '生产数量', width: 100 },
{
field: 'productionTime',
title: '生产用时',
width: 130,
slots: { default: 'productionTime' },
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工艺路线产品 BOM 列表字段 */
export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] {
return [
{ field: 'itemCode', title: 'BOM 物料编码', width: 150 },
{ field: 'itemName', title: 'BOM 物料名称', width: 150 },
{ field: 'specification', title: '规格型号', width: 150 },
{ field: 'unitName', title: '单位', width: 80 },
{ field: 'quantity', title: '用料比例', width: 100 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,198 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Modal, Switch, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteRoute,
exportRoute,
getRoutePage,
updateRouteStatus,
} from '#/api/mes/pro/route';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建工艺路线 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 编辑工艺路线(仅停用状态可编辑) */
function handleEdit(row: MesProRouteApi.Route) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
/** 详情查看 */
function handleDetail(row: MesProRouteApi.Route) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
/** 切换状态 */
async function handleStatusChange(row: MesProRouteApi.Route, value: number) {
const text = value === CommonStatusEnum.ENABLE ? '启用' : '停用';
const previousStatus = row.status;
Modal.confirm({
title: `确认要"${text}""${row.name}"工艺路线吗?`,
onOk: async () => {
await updateRouteStatus(row.id!, value);
message.success($t('ui.actionMessage.operationSuccess'));
handleRefresh();
},
onCancel: () => {
//
row.status = previousStatus;
},
});
}
/** 删除(仅停用状态可删除) */
async function handleDelete(row: MesProRouteApi.Route) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteRoute(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 导出 */
async function handleExport() {
const data = await exportRoute(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '工艺路线.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getRoutePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
isHover: true,
keyField: 'id',
},
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<MesProRouteApi.Route>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】工序设置、工艺流程"
url="https://doc.iocoder.cn/mes/pro/process-route/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="线">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工艺路线']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-route:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-route:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
</template>
<template #status="{ row }">
<Switch
:checked="row.status === CommonStatusEnum.ENABLE"
checked-children="启用"
un-checked-children="停用"
@change="
(checked: boolean | number | string) =>
handleStatusChange(
row,
checked ? CommonStatusEnum.ENABLE : CommonStatusEnum.DISABLE,
)
"
/>
</template>
<template #actions="{ row }">
<Tooltip
:open="row.status === CommonStatusEnum.DISABLE ? false : undefined"
title="仅停用状态,才可以操作"
>
<span class="inline-block">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:pro-route:update'],
disabled: row.status !== CommonStatusEnum.DISABLE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:pro-route:delete'],
disabled: row.status !== CommonStatusEnum.DISABLE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</span>
</Tooltip>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,122 @@
<script lang="ts" setup>
import type { MesProRouteApi } from '#/api/mes/pro/route';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, TabPane, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createRoute,
getRoute,
updateRoute,
} from '#/api/mes/pro/route';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import ProcessList from './process-list.vue';
import ProductList from './product-list.vue';
type FormMode = 'create' | 'detail' | 'update';
const emit = defineEmits(['success']);
const formMode = ref<FormMode>('create');
const subTab = ref('process');
const formData = ref<MesProRouteApi.Route>();
const isDetail = computed(() => formMode.value === 'detail');
const getTitle = computed(() => {
if (formMode.value === 'detail') {
return $t('ui.actionTitle.detail', ['工艺路线']);
}
return formMode.value === 'update'
? $t('ui.actionTitle.edit', ['工艺路线'])
: $t('ui.actionTitle.create', ['工艺路线']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-1',
labelWidth: 100,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
formApi.setState({ schema: useFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (isDetail.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
const data = (await formApi.getValues()) as MesProRouteApi.Route;
try {
if (formMode.value === 'create') {
const id = await createRoute(data);
formData.value = { ...data, id };
await formApi.setFieldValue('id', id);
formMode.value = 'update';
} else {
await updateRoute(data);
formData.value = { ...formData.value, ...data };
}
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
await formApi.resetForm();
subTab.value = 'process';
const data = modalApi.getData<{ id?: number; type?: FormMode }>();
formMode.value = data?.type ?? 'create';
formApi.setDisabled(formMode.value === 'detail');
modalApi.setState({ showConfirmButton: formMode.value !== 'detail' });
if (!data?.id) {
return;
}
modalApi.lock();
try {
formData.value = await getRoute(data.id);
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-4/5">
<Form class="mx-4" />
<Tabs
v-if="formMode !== 'create' && formData?.id"
v-model:active-key="subTab"
class="mx-4 mt-4"
>
<TabPane key="process" tab="组成工序">
<ProcessList :form-mode="formMode" :route-id="formData.id" />
</TabPane>
<TabPane key="product" tab="关联产品">
<ProductList :form-mode="formMode" :route-id="formData.id" />
</TabPane>
</Tabs>
</Modal>
</template>

View File

@ -0,0 +1,113 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getProcessSimpleList } from '#/api/mes/pro/process';
import {
createRouteProcess,
updateRouteProcess,
} from '#/api/mes/pro/route/process';
import { $t } from '#/locales';
import { useRouteProcessFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProRouteProcessApi.RouteProcess>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['工艺路线工序'])
: $t('ui.actionTitle.create', ['工艺路线工序']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-1',
labelWidth: 130,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
/** 加载工序选项后再生成 schema避免下拉空选项 */
async function loadSchema(): Promise<VbenFormSchema[]> {
const list = await getProcessSimpleList();
const options = (list || []).map((item) => ({
label: item.name ?? '',
value: item.id!,
}));
return useRouteProcessFormSchema(options);
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
const data =
(await formApi.getValues()) as MesProRouteProcessApi.RouteProcess;
try {
await (formData.value?.id
? updateRouteProcess(data)
: createRouteProcess(data));
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
modalApi.lock();
try {
const schema = await loadSchema();
formApi.setState({ schema });
await formApi.resetForm();
} finally {
modalApi.unlock();
}
const data = modalApi.getData<{
id?: number;
maxSort?: number;
routeId: number;
row?: MesProRouteProcessApi.RouteProcess;
}>();
if (!data) {
return;
}
if (!data.id) {
await formApi.setValues({
colorCode: '#00AEF3',
routeId: data.routeId,
sort: (data.maxSort || 0) + 1,
});
return;
}
if (data.row) {
formData.value = data.row;
await formApi.setValues(data.row);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteRouteProcess,
getRouteProcessListByRoute,
} from '#/api/mes/pro/route/process';
import { $t } from '#/locales';
import { useRouteProcessGridColumns } from '../data';
import ProcessForm from './process-form.vue';
const props = defineProps<{
formMode: 'create' | 'detail' | 'update';
routeId: number;
}>();
const isEditable = ref(props.formMode !== 'detail');
const list = ref<MesProRouteProcessApi.RouteProcess[]>([]);
const [ProcessFormModal, processFormModalApi] = useVbenModal({
connectedComponent: ProcessForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useRouteProcessGridColumns(),
data: list.value,
minHeight: 240,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>,
});
async function getList() {
gridApi.setLoading(true);
try {
list.value = await getRouteProcessListByRoute(props.routeId);
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
function handleCreate() {
const maxSort =
list.value.length > 0
? Math.max(...list.value.map((item) => item.sort || 0))
: 0;
processFormModalApi.setData({ maxSort, routeId: props.routeId }).open();
}
function handleEdit(row: MesProRouteProcessApi.RouteProcess) {
processFormModalApi.setData({ id: row.id, routeId: props.routeId, row }).open();
}
async function handleDelete(row: MesProRouteProcessApi.RouteProcess) {
await deleteRouteProcess(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', ['工艺路线工序']));
await getList();
}
watch(
() => props.routeId,
(value) => {
if (value) {
getList();
}
},
{ immediate: true },
);
</script>
<template>
<ProcessFormModal @success="getList" />
<div v-if="isEditable" class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工序']),
type: 'primary',
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #colorCode="{ row }">
<div v-if="row.colorCode" class="flex items-center justify-center gap-1">
<div
class="h-4 w-4 rounded"
:style="{ backgroundColor: row.colorCode }"
></div>
<span>{{ row.colorCode }}</span>
</div>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
ifShow: () => isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
ifShow: () => isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['工艺路线工序']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@ -0,0 +1,265 @@
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { computed, reactive, ref, watch } from 'vue';
import {
Form as AForm,
FormItem,
InputNumber,
message,
Modal,
TabPane,
Tabs,
Textarea,
} from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process';
import {
createRouteProductBom,
deleteRouteProductBom,
getRouteProductBomList,
updateRouteProductBom,
} from '#/api/mes/pro/route/productbom';
import { $t } from '#/locales';
import { MdProductBomSelect } from '#/views/mes/md/item/components';
import { useRouteProductBomGridColumns } from '../data';
const props = defineProps<{
productId: number;
productName?: string;
routeId: number;
}>();
const processOptions = ref<
Array<{ processId: number; processName?: string }>
>([]);
const activeProcessId = ref<string>('');
const list = ref<MesProRouteProductBomApi.RouteProductBom[]>([]);
const formVisible = ref(false);
const formRef = ref<FormInstance>();
const isUpdate = ref(false);
const formData = reactive<MesProRouteProductBomApi.RouteProductBom>({
quantity: 1,
});
const formRules = {
itemId: [{ message: 'BOM 物料不能为空', required: true }],
quantity: [{ message: '用料比例不能为空', required: true }],
};
const formTitle = computed(() =>
isUpdate.value
? $t('ui.actionTitle.edit', ['BOM 物料'])
: $t('ui.actionTitle.create', ['BOM 物料']),
);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useRouteProductBomGridColumns(),
data: list.value,
minHeight: 200,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>,
});
async function loadProcessList() {
const data = await getRouteProcessListByRoute(props.routeId);
processOptions.value = (data || []).map((item) => ({
processId: item.processId!,
processName: item.processName,
}));
if (processOptions.value.length > 0) {
activeProcessId.value = String(processOptions.value[0]!.processId);
await getList();
} else {
activeProcessId.value = '';
list.value = [];
gridApi.setGridOptions({ data: list.value });
}
}
async function getList() {
if (!activeProcessId.value) {
return;
}
gridApi.setLoading(true);
try {
list.value = await getRouteProductBomList({
processId: Number(activeProcessId.value),
productId: props.productId,
routeId: props.routeId,
});
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
function resetForm() {
Object.assign(formData, {
id: undefined,
itemCode: undefined,
itemId: undefined,
itemName: undefined,
processId: Number(activeProcessId.value),
productId: props.productId,
quantity: 1,
remark: undefined,
routeId: props.routeId,
specification: undefined,
unitName: undefined,
});
formRef.value?.clearValidate();
}
function handleCreate() {
if (!activeProcessId.value) {
message.warning('请先选择工序');
return;
}
resetForm();
isUpdate.value = false;
formVisible.value = true;
}
function handleEdit(row: MesProRouteProductBomApi.RouteProductBom) {
Object.assign(formData, row);
isUpdate.value = true;
formVisible.value = true;
}
async function handleDelete(row: MesProRouteProductBomApi.RouteProductBom) {
await deleteRouteProductBom(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', ['BOM 物料']));
await getList();
}
async function submitForm() {
try {
await formRef.value?.validate();
} catch {
return;
}
await (isUpdate.value
? updateRouteProductBom(formData)
: createRouteProductBom(formData));
message.success($t('ui.actionMessage.operationSuccess'));
formVisible.value = false;
await getList();
}
/** BOM 选中后回填用量比例 */
function handleBomChange(bom?: any) {
if (bom) {
formData.quantity = bom.quantity ?? 1;
formData.itemCode = bom.bomItemCode;
formData.itemName = bom.bomItemName;
formData.specification = bom.specification;
formData.unitName = bom.unitName;
}
}
watch(
() => [props.routeId, props.productId],
() => {
if (props.routeId && props.productId) {
loadProcessList();
}
},
{ immediate: true },
);
</script>
<template>
<Tabs v-model:active-key="activeProcessId" @change="getList">
<TabPane
v-for="item in processOptions"
:key="String(item.processId)"
:tab="item.processName"
/>
</Tabs>
<div class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['BOM 物料']),
type: 'primary',
disabled: !activeProcessId,
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title=" BOM">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['BOM 物料']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
<Modal
v-model:open="formVisible"
:title="formTitle"
width="500px"
@ok="submitForm"
>
<AForm
ref="formRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
:model="formData"
:rules="formRules"
>
<FormItem label="BOM 物料" name="itemId">
<MdProductBomSelect
v-model="formData.itemId"
:item-id="productId"
placeholder="请选择 BOM 物料"
@change="handleBomChange"
/>
</FormItem>
<FormItem label="用料比例" name="quantity">
<InputNumber
v-model:value="formData.quantity"
class="!w-full"
:min="0"
:precision="2"
/>
</FormItem>
<FormItem label="备注" name="remark">
<Textarea
v-model:value="formData.remark"
:max-length="250"
placeholder="请输入备注"
:rows="2"
/>
</FormItem>
</AForm>
</Modal>
</template>

View File

@ -0,0 +1,193 @@
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import { computed, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
Form as AForm,
Divider,
FormItem,
InputNumber,
message,
Select,
Textarea,
} from 'ant-design-vue';
import {
createRouteProduct,
updateRouteProduct,
} from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
import { MdItemSelect } from '#/views/mes/md/item/components';
import ProductBomList from './product-bom-list.vue';
const emit = defineEmits(['success']);
const isUpdate = ref(false);
const formRef = ref<FormInstance>();
const formData = reactive<MesProRouteProductApi.RouteProduct>({
productionTime: 1,
quantity: 1,
timeUnitType: 'MINUTE',
});
const formRules = {
itemId: [{ message: '产品不能为空', required: true }],
quantity: [{ message: '生产数量不能为空', required: true }],
};
const timeUnitOptions = computed(() =>
getDictOptions(DICT_TYPE.MES_TIME_UNIT_TYPE).map((item) => ({
label: item.label,
value: item.value as string,
})),
);
const getTitle = computed(() =>
isUpdate.value
? $t('ui.actionTitle.edit', ['工艺路线产品'])
: $t('ui.actionTitle.create', ['工艺路线产品']),
);
/** 重置表单数据 */
function resetForm(routeId?: number) {
Object.assign(formData, {
id: undefined,
itemCode: undefined,
itemId: undefined,
itemName: undefined,
productionTime: 1,
quantity: 1,
remark: undefined,
routeId: routeId ?? formData.routeId,
specification: undefined,
timeUnitType: 'MINUTE',
unitName: undefined,
});
formRef.value?.clearValidate();
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await formRef.value?.validate();
} catch {
return;
}
modalApi.lock();
try {
if (isUpdate.value) {
await updateRouteProduct(formData);
} else {
const id = await createRouteProduct(formData);
formData.id = id;
isUpdate.value = true;
}
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetForm();
isUpdate.value = false;
return;
}
const data = modalApi.getData<{
id?: number;
routeId: number;
row?: MesProRouteProductApi.RouteProduct;
}>();
if (!data) {
return;
}
if (data.row) {
Object.assign(formData, data.row);
isUpdate.value = true;
} else {
resetForm(data.routeId);
isUpdate.value = false;
}
},
});
/** 物料编号变化时回填名称等 */
function handleItemChange(item?: any) {
if (!item) {
return;
}
formData.itemCode = item.code;
formData.itemName = item.name;
formData.specification = item.specification;
formData.unitName = item.unitName;
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<AForm
ref="formRef"
class="mx-4"
:label-col="{ flex: '0 0 110px' }"
:wrapper-col="{ flex: 'auto' }"
:model="formData"
:rules="formRules"
>
<div class="grid grid-cols-2 gap-x-4">
<FormItem class="col-span-2" label="产品" name="itemId">
<MdItemSelect
v-model="formData.itemId"
@change="handleItemChange"
/>
</FormItem>
<FormItem label="生产数量" name="quantity">
<InputNumber
v-model:value="formData.quantity"
class="!w-full"
:min="1"
:precision="0"
/>
</FormItem>
<FormItem label="生产用时" name="productionTime">
<InputNumber
v-model:value="formData.productionTime"
class="!w-full"
:min="0"
:precision="2"
/>
</FormItem>
<FormItem label="时间单位" name="timeUnitType">
<Select
v-model:value="formData.timeUnitType"
allow-clear
:options="timeUnitOptions"
placeholder="请选择"
/>
</FormItem>
<FormItem class="col-span-2" label="备注" name="remark">
<Textarea
v-model:value="formData.remark"
:max-length="250"
placeholder="请输入备注"
:rows="2"
/>
</FormItem>
</div>
</AForm>
<template v-if="isUpdate && formData.id && formData.itemId">
<Divider class="!my-3" orientation="left">产品 BOM 配置</Divider>
<div class="mx-4">
<ProductBomList
:route-id="formData.routeId!"
:product-id="formData.itemId"
:product-name="formData.itemName"
/>
</div>
</template>
</Modal>
</template>

View File

@ -0,0 +1,128 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { message } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteRouteProduct,
getRouteProductListByRoute,
} from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
import { useRouteProductGridColumns } from '../data';
import ProductForm from './product-form.vue';
const props = defineProps<{
formMode: 'create' | 'detail' | 'update';
routeId: number;
}>();
const isEditable = ref(props.formMode !== 'detail');
const list = ref<MesProRouteProductApi.RouteProduct[]>([]);
const [ProductFormModal, productFormModalApi] = useVbenModal({
connectedComponent: ProductForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useRouteProductGridColumns(),
data: list.value,
minHeight: 240,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProRouteProductApi.RouteProduct>,
});
async function getList() {
gridApi.setLoading(true);
try {
list.value = await getRouteProductListByRoute(props.routeId);
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
function handleCreate() {
productFormModalApi.setData({ routeId: props.routeId }).open();
}
function handleEdit(row: MesProRouteProductApi.RouteProduct) {
productFormModalApi.setData({ id: row.id, routeId: props.routeId, row }).open();
}
async function handleDelete(row: MesProRouteProductApi.RouteProduct) {
await deleteRouteProduct(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', ['工艺路线产品']));
await getList();
}
watch(
() => props.routeId,
(value) => {
if (value) {
getList();
}
},
{ immediate: true },
);
</script>
<template>
<ProductFormModal @success="getList" />
<div v-if="isEditable" class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['关联产品']),
type: 'primary',
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #productionTime="{ row }">
<span v-if="row.productionTime">
{{ row.productionTime }}
{{ getDictLabel(DICT_TYPE.MES_TIME_UNIT_TYPE, row.timeUnitType) }}
</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
ifShow: () => isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
ifShow: () => isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['工艺路线产品']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@ -0,0 +1,49 @@
import { requestClient } from '#/api/request';
export namespace MesProProcessContentApi {
/** MES 生产工序内容(操作步骤) */
export interface ProcessContent {
id?: number;
processId?: number;
sort?: number;
content?: string;
device?: string;
material?: string;
docUrl?: string;
remark?: string;
createTime?: Date;
}
}
/** 按工序编号查询工序内容列表 */
export function getProcessContentListByProcessId(processId: number) {
return requestClient.get<MesProProcessContentApi.ProcessContent[]>(
`/mes/pro/process-content/list-by-process?processId=${processId}`,
);
}
/** 查询工序内容详情 */
export function getProcessContent(id: number) {
return requestClient.get<MesProProcessContentApi.ProcessContent>(
`/mes/pro/process-content/get?id=${id}`,
);
}
/** 新增工序内容 */
export function createProcessContent(
data: MesProProcessContentApi.ProcessContent,
) {
return requestClient.post('/mes/pro/process-content/create', data);
}
/** 修改工序内容 */
export function updateProcessContent(
data: MesProProcessContentApi.ProcessContent,
) {
return requestClient.put('/mes/pro/process-content/update', data);
}
/** 删除工序内容 */
export function deleteProcessContent(id: number) {
return requestClient.delete(`/mes/pro/process-content/delete?id=${id}`);
}

View File

@ -0,0 +1,65 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProRouteApi {
/** MES 工艺路线 */
export interface Route {
id?: number;
code?: string;
name?: string;
description?: string;
status?: number;
remark?: string;
createTime?: Date;
}
}
/** 查询工艺路线分页 */
export function getRoutePage(params: PageParam) {
return requestClient.get<PageResult<MesProRouteApi.Route>>(
'/mes/pro/route/page',
{ params },
);
}
/** 查询工艺路线精简列表 */
export function getRouteSimpleList() {
return requestClient.get<MesProRouteApi.Route[]>(
'/mes/pro/route/simple-list',
);
}
/** 查询工艺路线详情 */
export function getRoute(id: number) {
return requestClient.get<MesProRouteApi.Route>(
`/mes/pro/route/get?id=${id}`,
);
}
/** 新增工艺路线 */
export function createRoute(data: MesProRouteApi.Route) {
return requestClient.post<number>('/mes/pro/route/create', data);
}
/** 修改工艺路线 */
export function updateRoute(data: MesProRouteApi.Route) {
return requestClient.put('/mes/pro/route/update', data);
}
/** 修改工艺路线状态 */
export function updateRouteStatus(id: number, status: number) {
return requestClient.put(
`/mes/pro/route/update-status?id=${id}&status=${status}`,
);
}
/** 删除工艺路线 */
export function deleteRoute(id: number) {
return requestClient.delete(`/mes/pro/route/delete?id=${id}`);
}
/** 导出工艺路线 Excel */
export function exportRoute(params: any) {
return requestClient.download('/mes/pro/route/export-excel', { params });
}

View File

@ -0,0 +1,70 @@
import { requestClient } from '#/api/request';
export namespace MesProRouteProcessApi {
/** MES 工艺路线工序 */
export interface RouteProcess {
id?: number;
routeId?: number;
processId?: number;
processCode?: string;
processName?: string;
sort?: number;
nextProcessId?: number;
nextProcessName?: string;
linkType?: number;
prepareTime?: number;
waitTime?: number;
colorCode?: string;
keyFlag?: boolean;
checkFlag?: boolean;
remark?: string;
createTime?: Date;
}
}
/** 按工艺路线查询工序列表 */
export function getRouteProcessListByRoute(routeId: number) {
return requestClient.get<MesProRouteProcessApi.RouteProcess[]>(
`/mes/pro/route-process/list-by-route?routeId=${routeId}`,
);
}
/** 按产品查询工序列表(自动查找关联的工艺路线) */
export function getRouteProcessListByProduct(productId: number) {
return requestClient.get<MesProRouteProcessApi.RouteProcess[]>(
`/mes/pro/route-process/list-by-product?productId=${productId}`,
);
}
/** 查询工艺路线工序详情 */
export function getRouteProcess(id: number) {
return requestClient.get<MesProRouteProcessApi.RouteProcess>(
`/mes/pro/route-process/get?id=${id}`,
);
}
/** 按工艺路线 + 工序精确查询工序配置 */
export function getRouteProcessByRouteAndProcess(
routeId: number,
processId: number,
) {
return requestClient.get<MesProRouteProcessApi.RouteProcess>(
'/mes/pro/route-process/get-by-route-and-process',
{ params: { processId, routeId } },
);
}
/** 新增工艺路线工序 */
export function createRouteProcess(data: MesProRouteProcessApi.RouteProcess) {
return requestClient.post('/mes/pro/route-process/create', data);
}
/** 修改工艺路线工序 */
export function updateRouteProcess(data: MesProRouteProcessApi.RouteProcess) {
return requestClient.put('/mes/pro/route-process/update', data);
}
/** 删除工艺路线工序 */
export function deleteRouteProcess(id: number) {
return requestClient.delete(`/mes/pro/route-process/delete?id=${id}`);
}

View File

@ -0,0 +1,48 @@
import { requestClient } from '#/api/request';
export namespace MesProRouteProductApi {
/** MES 工艺路线产品 */
export interface RouteProduct {
id?: number;
routeId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
specification?: string;
unitName?: string;
quantity?: number;
productionTime?: number;
timeUnitType?: string;
remark?: string;
createTime?: Date;
}
}
/** 按工艺路线查询产品列表 */
export function getRouteProductListByRoute(routeId: number) {
return requestClient.get<MesProRouteProductApi.RouteProduct[]>(
`/mes/pro/route-product/list-by-route?routeId=${routeId}`,
);
}
/** 查询工艺路线产品详情 */
export function getRouteProduct(id: number) {
return requestClient.get<MesProRouteProductApi.RouteProduct>(
`/mes/pro/route-product/get?id=${id}`,
);
}
/** 新增工艺路线产品 */
export function createRouteProduct(data: MesProRouteProductApi.RouteProduct) {
return requestClient.post<number>('/mes/pro/route-product/create', data);
}
/** 修改工艺路线产品 */
export function updateRouteProduct(data: MesProRouteProductApi.RouteProduct) {
return requestClient.put('/mes/pro/route-product/update', data);
}
/** 删除工艺路线产品 */
export function deleteRouteProduct(id: number) {
return requestClient.delete(`/mes/pro/route-product/delete?id=${id}`);
}

View File

@ -0,0 +1,57 @@
import { requestClient } from '#/api/request';
export namespace MesProRouteProductBomApi {
/** MES 工艺路线产品 BOM */
export interface RouteProductBom {
id?: number;
routeId?: number;
processId?: number;
productId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
specification?: string;
unitName?: string;
quantity?: number;
remark?: string;
createTime?: Date;
}
}
/** 查询工艺路线产品 BOM 列表 */
export function getRouteProductBomList(params: {
processId?: number;
productId?: number;
routeId: number;
}) {
return requestClient.get<MesProRouteProductBomApi.RouteProductBom[]>(
'/mes/pro/route-product-bom/list',
{ params },
);
}
/** 查询工艺路线产品 BOM 详情 */
export function getRouteProductBom(id: number) {
return requestClient.get<MesProRouteProductBomApi.RouteProductBom>(
`/mes/pro/route-product-bom/get?id=${id}`,
);
}
/** 新增工艺路线产品 BOM */
export function createRouteProductBom(
data: MesProRouteProductBomApi.RouteProductBom,
) {
return requestClient.post('/mes/pro/route-product-bom/create', data);
}
/** 修改工艺路线产品 BOM */
export function updateRouteProductBom(
data: MesProRouteProductBomApi.RouteProductBom,
) {
return requestClient.put('/mes/pro/route-product-bom/update', data);
}
/** 删除工艺路线产品 BOM */
export function deleteRouteProductBom(id: number) {
return requestClient.delete(`/mes/pro/route-product-bom/delete?id=${id}`);
}

View File

@ -0,0 +1,271 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessApi } from '#/api/mes/pro/process';
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
import { h } from 'vue';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElButton } from 'element-plus';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 新增/修改生产工序的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '工序编码',
component: 'Input',
componentProps: {
maxLength: 64,
placeholder: '请输入工序编码',
},
rules: z.string().min(1, '工序编码不能为空').max(64),
suffix: () =>
h(
ElButton,
{
type: 'default',
onClick: async () => {
try {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_PROCESS_CODE,
);
await formApi?.setFieldValue('code', code);
} catch (error) {
console.error(error);
}
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '工序名称',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入工序名称',
},
rules: z.string().min(1, '工序名称不能为空').max(100),
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
{
fieldName: 'attention',
label: '工序说明',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
maxLength: 500,
placeholder: '请输入工序说明',
rows: 3,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工序编码',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入工序编码',
},
},
{
fieldName: 'name',
label: '工序名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入工序名称',
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
placeholder: '请选择状态',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProProcessApi.Process>['columns'] {
return [
{
field: 'code',
title: '工序编码',
minWidth: 150,
slots: {
default: 'code',
},
},
{ field: 'name', title: '工序名称', minWidth: 180 },
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{ field: 'remark', title: '备注', minWidth: 180 },
{
field: 'createTime',
title: '创建时间',
width: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 150,
fixed: 'right',
slots: {
default: 'actions',
},
},
];
}
/** 工序内容(操作步骤)表单 */
export function useContentFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'processId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'sort',
label: '序号',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
max: 999,
min: 1,
precision: 0,
},
rules: z.number().default(1),
},
{
fieldName: 'content',
label: '步骤说明',
component: 'Textarea',
componentProps: {
maxLength: 500,
placeholder: '请输入步骤说明',
rows: 3,
},
},
{
fieldName: 'device',
label: '辅助设备',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入辅助设备',
},
},
{
fieldName: 'material',
label: '辅助材料',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入辅助材料',
},
},
{
fieldName: 'docUrl',
label: '材料文档 URL',
component: 'Input',
componentProps: {
maxLength: 250,
placeholder: '请输入材料文档 URL',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 工序内容列表的字段 */
export function useContentGridColumns(): VxeTableGridOptions<MesProProcessContentApi.ProcessContent>['columns'] {
return [
{ field: 'sort', title: '序号', width: 80, align: 'center' },
{ field: 'content', title: '步骤说明', minWidth: 220 },
{ field: 'device', title: '辅助设备', width: 150 },
{ field: 'material', title: '辅助材料', width: 150 },
{ field: 'docUrl', title: '材料文档', minWidth: 180 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,160 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessApi } from '#/api/mes/pro/process';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProcess,
exportProcess,
getProcessPage,
} from '#/api/mes/pro/process';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建生产工序 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 编辑生产工序 */
function handleEdit(row: MesProProcessApi.Process) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
/** 查看生产工序详情 */
function handleDetail(row: MesProProcessApi.Process) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
/** 删除生产工序 */
async function handleDelete(row: MesProProcessApi.Process) {
const hideLoading = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.name]),
});
try {
await deleteProcess(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading.close();
}
}
/** 导出生产工序 */
async function handleExport() {
const data = await exportProcess(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '生产工序.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProcessPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
isHover: true,
keyField: 'id',
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesProProcessApi.Process>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】工序设置、工艺流程"
url="https://doc.iocoder.cn/mes/pro/process-route/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工序']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-process:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-process:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:pro-process:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:pro-process:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProcessContent,
getProcessContent,
updateProcessContent,
} from '#/api/mes/pro/process/content';
import { $t } from '#/locales';
import { useContentFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProProcessContentApi.ProcessContent>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['工序步骤'])
: $t('ui.actionTitle.create', ['工序步骤']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useContentFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as MesProProcessContentApi.ProcessContent;
try {
await (formData.value?.id
? updateProcessContent(data)
: createProcessContent(data));
//
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
await formApi.resetForm();
//
const data = modalApi.getData<{
id?: number;
maxSort?: number;
processId: number;
}>();
if (!data?.id) {
// = maxSort + 1
await formApi.setValues({
processId: data?.processId,
sort: (data?.maxSort || 0) + 1,
});
return;
}
modalApi.lock();
try {
formData.value = await getProcessContent(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProcessContent,
getProcessContentListByProcessId,
} from '#/api/mes/pro/process/content';
import { $t } from '#/locales';
import { useContentGridColumns } from '../data';
import ContentForm from './content-form.vue';
const props = defineProps<{
processId: number;
readonly?: boolean;
}>();
const list = ref<MesProProcessContentApi.ProcessContent[]>([]);
const [ContentFormModal, contentFormModalApi] = useVbenModal({
connectedComponent: ContentForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useContentGridColumns(),
data: list.value,
minHeight: 240,
pagerConfig: { enabled: false },
rowConfig: {
isHover: true,
keyField: 'id',
},
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProProcessContentApi.ProcessContent>,
});
/** 加载工序内容列表 */
async function getList() {
gridApi.setLoading(true);
try {
list.value = await getProcessContentListByProcessId(props.processId);
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
/** 新增工序步骤 */
function handleCreate() {
const maxSort =
list.value.length > 0
? Math.max(...list.value.map((item) => item.sort || 0))
: 0;
contentFormModalApi
.setData({ maxSort, processId: props.processId })
.open();
}
/** 编辑工序步骤 */
function handleEdit(row: MesProProcessContentApi.ProcessContent) {
contentFormModalApi.setData({ id: row.id, processId: props.processId }).open();
}
/** 删除工序步骤 */
async function handleDelete(row: MesProProcessContentApi.ProcessContent) {
await deleteProcessContent(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', ['工序步骤']));
await getList();
}
watch(
() => props.processId,
(value) => {
if (value) {
getList();
}
},
{ immediate: true },
);
</script>
<template>
<ContentFormModal @success="getList" />
<div v-if="!readonly" class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['步骤']),
type: 'primary',
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
ifShow: () => !readonly,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
ifShow: () => !readonly,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['工序步骤']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@ -0,0 +1,117 @@
<script lang="ts" setup>
import type { MesProProcessApi } from '#/api/mes/pro/process';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElDivider, ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProcess,
getProcess,
updateProcess,
} from '#/api/mes/pro/process';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import ContentList from './content-list.vue';
type FormMode = 'create' | 'detail' | 'update';
const emit = defineEmits(['success']);
const formMode = ref<FormMode>('create'); //
const formData = ref<MesProProcessApi.Process>(); // /
const isDetail = computed(() => formMode.value === 'detail'); //
const getTitle = computed(() => {
if (formMode.value === 'detail') {
return $t('ui.actionTitle.detail', ['生产工序']);
}
return formMode.value === 'update'
? $t('ui.actionTitle.edit', ['生产工序'])
: $t('ui.actionTitle.create', ['生产工序']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-3',
labelWidth: 100,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 表单 schema 需要 formApi 引用(生成编码按钮),所以通过 setState 设置 schema */
formApi.setState({ schema: useFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (isDetail.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as MesProProcessApi.Process;
try {
await (formData.value?.id ? updateProcess(data) : createProcess(data));
//
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
await formApi.resetForm();
//
const data = modalApi.getData<{ id?: number; type?: FormMode }>();
formMode.value = data?.type ?? 'create';
formApi.setDisabled(formMode.value === 'detail');
modalApi.setState({ showConfirmButton: formMode.value !== 'detail' });
if (!data?.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProcess(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
/** 用于工序内容子表的工序编号 */
const processId = computed(() => formData.value?.id);
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 编辑/详情模式下展示工序操作步骤子表新增模式下隐藏 -->
<template v-if="processId">
<ElDivider class="!my-3" content-position="left">操作步骤</ElDivider>
<div class="mx-4">
<ContentList :process-id="processId" :readonly="isDetail" />
</div>
</template>
</Modal>
</template>

View File

@ -0,0 +1,350 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElButton } from 'element-plus';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 工艺路线表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'code',
label: '路线编码',
component: 'Input',
componentProps: {
maxLength: 64,
placeholder: '请输入工艺路线编码',
},
rules: z.string().min(1, '路线编码不能为空').max(64),
suffix: () =>
h(
ElButton,
{
type: 'default',
onClick: async () => {
try {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_ROUTE_CODE,
);
await formApi?.setFieldValue('code', code);
} catch (error) {
console.error(error);
}
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '路线名称',
component: 'Input',
componentProps: {
maxLength: 100,
placeholder: '请输入工艺路线名称',
},
rules: z.string().min(1, '路线名称不能为空').max(100),
},
{
fieldName: 'description',
label: '路线说明',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
maxLength: 500,
placeholder: '请输入工艺路线说明',
rows: 3,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 列表搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '路线编码',
component: 'Input',
componentProps: { clearable: true, placeholder: '请输入路线编码' },
},
{
fieldName: 'name',
label: '路线名称',
component: 'Input',
componentProps: { clearable: true, placeholder: '请输入路线名称' },
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
placeholder: '请选择状态',
},
},
];
}
/** 列表字段 */
export function useGridColumns(): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
return [
{
field: 'code',
title: '路线编码',
minWidth: 160,
slots: { default: 'code' },
},
{ field: 'name', title: '路线名称', minWidth: 180 },
{ field: 'description', title: '路线说明', minWidth: 200 },
{
field: 'status',
title: '状态',
width: 110,
slots: { default: 'status' },
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
field: 'createTime',
title: '创建时间',
width: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工艺路线工序明细表单 */
export function useRouteProcessFormSchema(
processOptions: Array<{ label: string; value: number }>,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'routeId',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'sort',
label: '序号',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 1,
precision: 0,
},
rules: z.number().default(1),
},
{
fieldName: 'processId',
label: '工序',
component: 'Select',
componentProps: {
clearable: true,
filterable: true,
options: processOptions,
placeholder: '请选择工序',
},
rules: 'selectRequired',
},
{
fieldName: 'linkType',
label: '与下道工序关系',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'),
placeholder: '请选择',
},
rules: z.number().default(3),
},
{
fieldName: 'colorCode',
label: '甘特图颜色',
component: 'Input',
componentProps: {
maxLength: 16,
placeholder: '请输入颜色 hex例如 #00AEF3',
},
},
{
fieldName: 'keyFlag',
label: '是否关键工序',
component: 'Switch',
componentProps: { activeText: '是', inactiveText: '否' },
rules: z.boolean().default(false),
},
{
fieldName: 'checkFlag',
label: '是否质检确认',
component: 'Switch',
componentProps: { activeText: '是', inactiveText: '否' },
rules: z.boolean().default(false),
},
{
fieldName: 'prepareTime',
label: '准备时间(分)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
precision: 0,
},
rules: z.number().default(0),
},
{
fieldName: 'waitTime',
label: '等待时间(分)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
precision: 0,
},
rules: z.number().default(0),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: { maxLength: 250, placeholder: '请输入备注', rows: 2 },
},
];
}
/** 工艺路线工序列表字段 */
export function useRouteProcessGridColumns(): VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>['columns'] {
return [
{ field: 'sort', title: '序号', width: 70, align: 'center', fixed: 'left' },
{ field: 'processCode', title: '工序编码', width: 140, fixed: 'left' },
{ field: 'processName', title: '工序名称', width: 140, fixed: 'left' },
{ field: 'nextProcessName', title: '下一道工序', width: 140 },
{
field: 'linkType',
title: '与下一道工序关系',
width: 160,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_LINK_TYPE },
},
},
{
field: 'keyFlag',
title: '关键工序',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'checkFlag',
title: '质检确认',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{ field: 'prepareTime', title: '准备时间(分)', width: 110 },
{ field: 'waitTime', title: '等待时间(分)', width: 110 },
{
field: 'colorCode',
title: '甘特图颜色',
width: 130,
slots: { default: 'colorCode' },
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工艺路线产品列表字段 */
export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] {
return [
{ field: 'itemCode', title: '产品物料编码', width: 150 },
{ field: 'itemName', title: '产品物料名称', width: 150 },
{ field: 'specification', title: '规格型号', width: 150 },
{ field: 'unitName', title: '单位', width: 80 },
{ field: 'quantity', title: '生产数量', width: 100 },
{
field: 'productionTime',
title: '生产用时',
width: 130,
slots: { default: 'productionTime' },
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工艺路线产品 BOM 列表字段 */
export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] {
return [
{ field: 'itemCode', title: 'BOM 物料编码', width: 150 },
{ field: 'itemName', title: 'BOM 物料名称', width: 150 },
{ field: 'specification', title: '规格型号', width: 150 },
{ field: 'unitName', title: '单位', width: 80 },
{ field: 'quantity', title: '用料比例', width: 100 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,195 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import {
ElButton,
ElLoading,
ElMessage,
ElMessageBox,
ElSwitch,
ElTooltip,
} from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteRoute,
exportRoute,
getRoutePage,
updateRouteStatus,
} from '#/api/mes/pro/route';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
function handleRefresh() {
gridApi.query();
}
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
function handleEdit(row: MesProRouteApi.Route) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
function handleDetail(row: MesProRouteApi.Route) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
async function handleStatusChange(row: MesProRouteApi.Route, value: number) {
const text = value === CommonStatusEnum.ENABLE ? '启用' : '停用';
const previousStatus = row.status;
try {
await ElMessageBox.confirm(
`确认要"${text}""${row.name}"工艺路线吗?`,
'提示',
{ type: 'warning' },
);
await updateRouteStatus(row.id!, value);
ElMessage.success($t('ui.actionMessage.operationSuccess'));
handleRefresh();
} catch {
row.status = previousStatus;
}
}
async function handleDelete(row: MesProRouteApi.Route) {
const hideLoading = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.name]),
});
try {
await deleteRoute(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading.close();
}
}
async function handleExport() {
const data = await exportRoute(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '工艺路线.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { schema: useGridFormSchema() },
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getRoutePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: { isHover: true, keyField: 'id' },
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<MesProRouteApi.Route>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】工序设置、工艺流程"
url="https://doc.iocoder.cn/mes/pro/process-route/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="线">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工艺路线']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-route:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-route:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #status="{ row }">
<ElSwitch
:model-value="row.status === CommonStatusEnum.ENABLE"
active-text="启用"
inactive-text="停用"
@update:model-value="
(value: boolean | number | string) =>
handleStatusChange(
row,
value ? CommonStatusEnum.ENABLE : CommonStatusEnum.DISABLE,
)
"
/>
</template>
<template #actions="{ row }">
<ElTooltip
:disabled="row.status === CommonStatusEnum.DISABLE"
content="仅停用状态,才可以操作"
placement="top"
>
<span class="inline-block">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:pro-route:update'],
disabled: row.status !== CommonStatusEnum.DISABLE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:pro-route:delete'],
disabled: row.status !== CommonStatusEnum.DISABLE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</span>
</ElTooltip>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,118 @@
<script lang="ts" setup>
import type { MesProRouteApi } from '#/api/mes/pro/route';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage, ElTabPane, ElTabs } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import ProcessList from './process-list.vue';
import ProductList from './product-list.vue';
type FormMode = 'create' | 'detail' | 'update';
const emit = defineEmits(['success']);
const formMode = ref<FormMode>('create');
const subTab = ref('process');
const formData = ref<MesProRouteApi.Route>();
const isDetail = computed(() => formMode.value === 'detail');
const getTitle = computed(() => {
if (formMode.value === 'detail') {
return $t('ui.actionTitle.detail', ['工艺路线']);
}
return formMode.value === 'update'
? $t('ui.actionTitle.edit', ['工艺路线'])
: $t('ui.actionTitle.create', ['工艺路线']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-1',
labelWidth: 100,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
formApi.setState({ schema: useFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (isDetail.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
const data = (await formApi.getValues()) as MesProRouteApi.Route;
try {
if (formMode.value === 'create') {
const id = await createRoute(data);
formData.value = { ...data, id };
await formApi.setFieldValue('id', id);
formMode.value = 'update';
} else {
await updateRoute(data);
formData.value = { ...formData.value, ...data };
}
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
await formApi.resetForm();
subTab.value = 'process';
const data = modalApi.getData<{ id?: number; type?: FormMode }>();
formMode.value = data?.type ?? 'create';
formApi.setDisabled(formMode.value === 'detail');
modalApi.setState({ showConfirmButton: formMode.value !== 'detail' });
if (!data?.id) {
return;
}
modalApi.lock();
try {
formData.value = await getRoute(data.id);
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-4/5">
<Form class="mx-4" />
<ElTabs
v-if="formMode !== 'create' && formData?.id"
v-model="subTab"
class="mx-4 mt-4"
>
<ElTabPane label="组成工序" name="process">
<ProcessList :form-mode="formMode" :route-id="formData.id" />
</ElTabPane>
<ElTabPane label="关联产品" name="product">
<ProductList :form-mode="formMode" :route-id="formData.id" />
</ElTabPane>
</ElTabs>
</Modal>
</template>

View File

@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { getProcessSimpleList } from '#/api/mes/pro/process';
import {
createRouteProcess,
updateRouteProcess,
} from '#/api/mes/pro/route/process';
import { $t } from '#/locales';
import { useRouteProcessFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProRouteProcessApi.RouteProcess>();
const getTitle = computed(() =>
formData.value?.id
? $t('ui.actionTitle.edit', ['工艺路线工序'])
: $t('ui.actionTitle.create', ['工艺路线工序']),
);
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-1',
labelWidth: 130,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
async function loadSchema(): Promise<VbenFormSchema[]> {
const list = await getProcessSimpleList();
const options = (list || []).map((item) => ({
label: item.name ?? '',
value: item.id!,
}));
return useRouteProcessFormSchema(options);
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
const data =
(await formApi.getValues()) as MesProRouteProcessApi.RouteProcess;
try {
await (formData.value?.id
? updateRouteProcess(data)
: createRouteProcess(data));
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
modalApi.lock();
try {
const schema = await loadSchema();
formApi.setState({ schema });
await formApi.resetForm();
} finally {
modalApi.unlock();
}
const data = modalApi.getData<{
id?: number;
maxSort?: number;
routeId: number;
row?: MesProRouteProcessApi.RouteProcess;
}>();
if (!data) {
return;
}
if (!data.id) {
await formApi.setValues({
colorCode: '#00AEF3',
routeId: data.routeId,
sort: (data.maxSort || 0) + 1,
});
return;
}
if (data.row) {
formData.value = data.row;
await formApi.setValues(data.row);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,136 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteRouteProcess,
getRouteProcessListByRoute,
} from '#/api/mes/pro/route/process';
import { $t } from '#/locales';
import { useRouteProcessGridColumns } from '../data';
import ProcessForm from './process-form.vue';
const props = defineProps<{
formMode: 'create' | 'detail' | 'update';
routeId: number;
}>();
const isEditable = ref(props.formMode !== 'detail');
const list = ref<MesProRouteProcessApi.RouteProcess[]>([]);
const [ProcessFormModal, processFormModalApi] = useVbenModal({
connectedComponent: ProcessForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useRouteProcessGridColumns(),
data: list.value,
minHeight: 240,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>,
});
async function getList() {
gridApi.setLoading(true);
try {
list.value = await getRouteProcessListByRoute(props.routeId);
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
function handleCreate() {
const maxSort =
list.value.length > 0
? Math.max(...list.value.map((item) => item.sort || 0))
: 0;
processFormModalApi.setData({ maxSort, routeId: props.routeId }).open();
}
function handleEdit(row: MesProRouteProcessApi.RouteProcess) {
processFormModalApi
.setData({ id: row.id, routeId: props.routeId, row })
.open();
}
async function handleDelete(row: MesProRouteProcessApi.RouteProcess) {
await deleteRouteProcess(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', ['工艺路线工序']));
await getList();
}
watch(
() => props.routeId,
(value) => {
if (value) {
getList();
}
},
{ immediate: true },
);
</script>
<template>
<ProcessFormModal @success="getList" />
<div v-if="isEditable" class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['工序']),
type: 'primary',
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #colorCode="{ row }">
<div v-if="row.colorCode" class="flex items-center justify-center gap-1">
<div
class="h-4 w-4 rounded"
:style="{ backgroundColor: row.colorCode }"
></div>
<span>{{ row.colorCode }}</span>
</div>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
ifShow: () => isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
ifShow: () => isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['工艺路线工序']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@ -0,0 +1,278 @@
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { computed, reactive, ref, watch } from 'vue';
import {
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElTabPane,
ElTabs,
} from 'element-plus';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process';
import {
createRouteProductBom,
deleteRouteProductBom,
getRouteProductBomList,
updateRouteProductBom,
} from '#/api/mes/pro/route/productbom';
import { $t } from '#/locales';
import { MdProductBomSelect } from '#/views/mes/md/item/components';
import { useRouteProductBomGridColumns } from '../data';
const props = defineProps<{
productId: number;
productName?: string;
routeId: number;
}>();
const processOptions = ref<
Array<{ processId: number; processName?: string }>
>([]);
const activeProcessId = ref<string>('');
const list = ref<MesProRouteProductBomApi.RouteProductBom[]>([]);
const formVisible = ref(false);
const formRef = ref<FormInstance>();
const isUpdate = ref(false);
const formData = reactive<MesProRouteProductBomApi.RouteProductBom>({
quantity: 1,
});
const formRules: FormRules = {
itemId: [{ message: 'BOM 物料不能为空', required: true }],
quantity: [{ message: '用料比例不能为空', required: true }],
};
const formTitle = computed(() =>
isUpdate.value
? $t('ui.actionTitle.edit', ['BOM 物料'])
: $t('ui.actionTitle.create', ['BOM 物料']),
);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useRouteProductBomGridColumns(),
data: list.value,
minHeight: 200,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>,
});
async function loadProcessList() {
const data = await getRouteProcessListByRoute(props.routeId);
processOptions.value = (data || []).map((item) => ({
processId: item.processId!,
processName: item.processName,
}));
if (processOptions.value.length > 0) {
activeProcessId.value = String(processOptions.value[0]!.processId);
await getList();
} else {
activeProcessId.value = '';
list.value = [];
gridApi.setGridOptions({ data: list.value });
}
}
async function getList() {
if (!activeProcessId.value) {
return;
}
gridApi.setLoading(true);
try {
list.value = await getRouteProductBomList({
processId: Number(activeProcessId.value),
productId: props.productId,
routeId: props.routeId,
});
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
function resetForm() {
Object.assign(formData, {
id: undefined,
itemCode: undefined,
itemId: undefined,
itemName: undefined,
processId: Number(activeProcessId.value),
productId: props.productId,
quantity: 1,
remark: undefined,
routeId: props.routeId,
specification: undefined,
unitName: undefined,
});
formRef.value?.clearValidate();
}
function handleCreate() {
if (!activeProcessId.value) {
ElMessage.warning('请先选择工序');
return;
}
resetForm();
isUpdate.value = false;
formVisible.value = true;
}
function handleEdit(row: MesProRouteProductBomApi.RouteProductBom) {
Object.assign(formData, row);
isUpdate.value = true;
formVisible.value = true;
}
async function handleDelete(row: MesProRouteProductBomApi.RouteProductBom) {
await deleteRouteProductBom(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', ['BOM 物料']));
await getList();
}
async function submitForm() {
try {
await formRef.value?.validate();
} catch {
return;
}
await (isUpdate.value
? updateRouteProductBom(formData)
: createRouteProductBom(formData));
ElMessage.success($t('ui.actionMessage.operationSuccess'));
formVisible.value = false;
await getList();
}
function handleBomChange(bom?: any) {
if (bom) {
formData.quantity = bom.quantity ?? 1;
formData.itemCode = bom.bomItemCode;
formData.itemName = bom.bomItemName;
formData.specification = bom.specification;
formData.unitName = bom.unitName;
}
}
watch(
() => [props.routeId, props.productId],
() => {
if (props.routeId && props.productId) {
loadProcessList();
}
},
{ immediate: true },
);
</script>
<template>
<ElTabs v-model="activeProcessId" @tab-change="getList">
<ElTabPane
v-for="item in processOptions"
:key="String(item.processId)"
:label="item.processName"
:name="String(item.processId)"
/>
</ElTabs>
<div class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['BOM 物料']),
type: 'primary',
disabled: !activeProcessId,
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title=" BOM">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['BOM 物料']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
<ElDialog v-model="formVisible" :title="formTitle" width="500px">
<ElForm
ref="formRef"
label-width="100px"
:model="formData"
:rules="formRules"
>
<ElFormItem label="BOM 物料" prop="itemId">
<MdProductBomSelect
v-model="formData.itemId"
:item-id="productId"
placeholder="请选择 BOM 物料"
@change="handleBomChange"
/>
</ElFormItem>
<ElFormItem label="用料比例" prop="quantity">
<ElInputNumber
v-model="formData.quantity"
class="!w-full"
controls-position="right"
:min="0"
:precision="2"
/>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput
v-model="formData.remark"
:maxlength="250"
placeholder="请输入备注"
:rows="2"
type="textarea"
/>
</ElFormItem>
</ElForm>
<template #footer>
<TableAction
:actions="[
{
label: $t('common.cancel'),
type: 'default',
onClick: () => (formVisible = false),
},
{
label: $t('common.confirm'),
type: 'primary',
onClick: submitForm,
},
]"
/>
</template>
</ElDialog>
</template>

View File

@ -0,0 +1,198 @@
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import { computed, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
ElDivider,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElOption,
ElSelect,
} from 'element-plus';
import {
createRouteProduct,
updateRouteProduct,
} from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
import { MdItemSelect } from '#/views/mes/md/item/components';
import ProductBomList from './product-bom-list.vue';
const emit = defineEmits(['success']);
const isUpdate = ref(false);
const formRef = ref<FormInstance>();
const formData = reactive<MesProRouteProductApi.RouteProduct>({
productionTime: 1,
quantity: 1,
timeUnitType: 'MINUTE',
});
const formRules: FormRules = {
itemId: [{ message: '产品不能为空', required: true }],
quantity: [{ message: '生产数量不能为空', required: true }],
};
const timeUnitOptions = computed(() =>
getDictOptions(DICT_TYPE.MES_TIME_UNIT_TYPE),
);
const getTitle = computed(() =>
isUpdate.value
? $t('ui.actionTitle.edit', ['工艺路线产品'])
: $t('ui.actionTitle.create', ['工艺路线产品']),
);
function resetForm(routeId?: number) {
Object.assign(formData, {
id: undefined,
itemCode: undefined,
itemId: undefined,
itemName: undefined,
productionTime: 1,
quantity: 1,
remark: undefined,
routeId: routeId ?? formData.routeId,
specification: undefined,
timeUnitType: 'MINUTE',
unitName: undefined,
});
formRef.value?.clearValidate();
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await formRef.value?.validate();
} catch {
return;
}
modalApi.lock();
try {
if (isUpdate.value) {
await updateRouteProduct(formData);
} else {
const id = await createRouteProduct(formData);
formData.id = id;
isUpdate.value = true;
}
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetForm();
isUpdate.value = false;
return;
}
const data = modalApi.getData<{
id?: number;
routeId: number;
row?: MesProRouteProductApi.RouteProduct;
}>();
if (!data) {
return;
}
if (data.row) {
Object.assign(formData, data.row);
isUpdate.value = true;
} else {
resetForm(data.routeId);
isUpdate.value = false;
}
},
});
function handleItemChange(item?: any) {
if (!item) {
return;
}
formData.itemCode = item.code;
formData.itemName = item.name;
formData.specification = item.specification;
formData.unitName = item.unitName;
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<ElForm
ref="formRef"
class="mx-4"
label-width="120px"
:model="formData"
:rules="formRules"
>
<div class="grid grid-cols-2 gap-x-4">
<ElFormItem class="col-span-2" label="产品" prop="itemId">
<MdItemSelect
v-model="formData.itemId"
@change="handleItemChange"
/>
</ElFormItem>
<ElFormItem label="生产数量" prop="quantity">
<ElInputNumber
v-model="formData.quantity"
class="!w-full"
controls-position="right"
:min="1"
:precision="0"
/>
</ElFormItem>
<ElFormItem label="生产用时" prop="productionTime">
<ElInputNumber
v-model="formData.productionTime"
class="!w-full"
controls-position="right"
:min="0"
:precision="2"
/>
</ElFormItem>
<ElFormItem label="时间单位" prop="timeUnitType">
<ElSelect
v-model="formData.timeUnitType"
class="!w-full"
clearable
placeholder="请选择"
>
<ElOption
v-for="dict in timeUnitOptions"
:key="dict.value as string"
:label="dict.label"
:value="dict.value as string"
/>
</ElSelect>
</ElFormItem>
<ElFormItem class="col-span-2" label="备注" prop="remark">
<ElInput
v-model="formData.remark"
:maxlength="250"
placeholder="请输入备注"
:rows="2"
type="textarea"
/>
</ElFormItem>
</div>
</ElForm>
<template v-if="isUpdate && formData.id && formData.itemId">
<ElDivider class="!my-3" content-position="left">产品 BOM 配置</ElDivider>
<div class="mx-4">
<ProductBomList
:route-id="formData.routeId!"
:product-id="formData.itemId"
:product-name="formData.itemName"
/>
</div>
</template>
</Modal>
</template>

View File

@ -0,0 +1,131 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { ElMessage } from 'element-plus';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteRouteProduct,
getRouteProductListByRoute,
} from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
import { useRouteProductGridColumns } from '../data';
import ProductForm from './product-form.vue';
const props = defineProps<{
formMode: 'create' | 'detail' | 'update';
routeId: number;
}>();
const isEditable = ref(props.formMode !== 'detail');
const list = ref<MesProRouteProductApi.RouteProduct[]>([]);
const [ProductFormModal, productFormModalApi] = useVbenModal({
connectedComponent: ProductForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useRouteProductGridColumns(),
data: list.value,
minHeight: 240,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProRouteProductApi.RouteProduct>,
});
async function getList() {
gridApi.setLoading(true);
try {
list.value = await getRouteProductListByRoute(props.routeId);
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
function handleCreate() {
productFormModalApi.setData({ routeId: props.routeId }).open();
}
function handleEdit(row: MesProRouteProductApi.RouteProduct) {
productFormModalApi
.setData({ id: row.id, routeId: props.routeId, row })
.open();
}
async function handleDelete(row: MesProRouteProductApi.RouteProduct) {
await deleteRouteProduct(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', ['工艺路线产品']));
await getList();
}
watch(
() => props.routeId,
(value) => {
if (value) {
getList();
}
},
{ immediate: true },
);
</script>
<template>
<ProductFormModal @success="getList" />
<div v-if="isEditable" class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['关联产品']),
type: 'primary',
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #productionTime="{ row }">
<span v-if="row.productionTime">
{{ row.productionTime }}
{{ getDictLabel(DICT_TYPE.MES_TIME_UNIT_TYPE, row.timeUnitType) }}
</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
ifShow: () => isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
ifShow: () => isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['工艺路线产品']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>