feat(mes):新增“安灯(pro_andon)”的迁移

pull/349/head
YunaiV 2026-05-25 22:28:23 +08:00
parent be3d9eaed7
commit 6b6228bc9c
36 changed files with 3005 additions and 117 deletions

View File

@ -0,0 +1,48 @@
import { requestClient } from '#/api/request';
export namespace MesProAndonConfigApi {
/** MES 安灯配置 */
export interface AndonConfig {
id?: number;
reason?: string; // 呼叫原因
level?: number; // 级别
handlerRoleId?: number; // 处置角色编号
handlerUserId?: number; // 处置人编号
handlerUserNickname?: string; // 处置人姓名(详情回显)
remark?: string;
}
}
/** 查询安灯配置分页 */
export function getAndonConfigPage(params: any) {
return requestClient.get('/mes/pro/andon-config/page', { params });
}
/** 查询安灯配置列表 */
export function getAndonConfigList() {
return requestClient.get<MesProAndonConfigApi.AndonConfig[]>(
'/mes/pro/andon-config/list',
);
}
/** 查询安灯配置详情 */
export function getAndonConfig(id: number) {
return requestClient.get<MesProAndonConfigApi.AndonConfig>(
`/mes/pro/andon-config/get?id=${id}`,
);
}
/** 新增安灯配置 */
export function createAndonConfig(data: MesProAndonConfigApi.AndonConfig) {
return requestClient.post('/mes/pro/andon-config/create', data);
}
/** 修改安灯配置 */
export function updateAndonConfig(data: MesProAndonConfigApi.AndonConfig) {
return requestClient.put('/mes/pro/andon-config/update', data);
}
/** 删除安灯配置 */
export function deleteAndonConfig(id: number) {
return requestClient.delete(`/mes/pro/andon-config/delete?id=${id}`);
}

View File

@ -0,0 +1,76 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProAndonRecordApi {
/** MES 安灯记录 */
export interface AndonRecord {
id?: number;
configId?: number; // 安灯配置编号
workstationId?: number; // 工作站编号
workstationCode?: string; // 工作站编码
workstationName?: string; // 工作站名称
workOrderId?: number; // 生产工单编号
workOrderCode?: string; // 工单编码
processId?: number; // 工序编号
processName?: string; // 工序名称
userId?: number; // 发起用户编号
userNickname?: string; // 发起人昵称
reason?: string; // 呼叫原因
level?: number; // 级别
status?: number; // 处置状态
handleTime?: number; // 处置时间(毫秒时间戳)
handlerUserId?: number; // 处置人编号
handlerUserNickname?: string; // 处置人昵称
remark?: string; // 备注
createTime?: number; // 发起时间
}
/** MES 安灯记录分页查询参数 */
export interface PageParams extends PageParam {
workstationId?: number; // 工作站编号
userId?: number; // 发起用户编号
handlerUserId?: number; // 处置人编号
status?: number; // 处置状态
createTime?: string[]; // 发起时间区间
}
}
/** 查询安灯记录分页 */
export function getAndonRecordPage(params: MesProAndonRecordApi.PageParams) {
return requestClient.get<PageResult<MesProAndonRecordApi.AndonRecord>>(
'/mes/pro/andon-record/page',
{ params },
);
}
/** 查询安灯记录详情 */
export function getAndonRecord(id: number) {
return requestClient.get<MesProAndonRecordApi.AndonRecord>(
`/mes/pro/andon-record/get?id=${id}`,
);
}
/** 新增安灯记录 */
export function createAndonRecord(data: MesProAndonRecordApi.AndonRecord) {
return requestClient.post('/mes/pro/andon-record/create', data);
}
/** 删除安灯记录 */
export function deleteAndonRecord(id: number) {
return requestClient.delete(`/mes/pro/andon-record/delete?id=${id}`);
}
/** 更新安灯记录(保存/已处置) */
export function updateAndonRecord(data: MesProAndonRecordApi.AndonRecord) {
return requestClient.put('/mes/pro/andon-record/update', data);
}
/** 导出安灯记录 Excel */
export function exportAndonRecord(
params: Partial<MesProAndonRecordApi.PageParams>,
) {
return requestClient.download('/mes/pro/andon-record/export-excel', {
params,
});
}

View File

@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProWorkOrderApi {
/** MES 生产工单 */
export interface WorkOrder {
id?: number;
code?: string; // 工单编码
name?: string; // 工单名称
type?: number; // 工单类型
status?: number; // 工单状态
sourceType?: number;
productId?: number; // 产品物料编号
productCode?: string;
productName?: string;
productSpecification?: string;
quantity?: number;
unitName?: string;
routeId?: number;
routeName?: string;
clientId?: number;
clientName?: string;
planStartTime?: number | string;
planEndTime?: number | string;
actualStartTime?: number | string;
actualEndTime?: number | string;
remark?: string;
createTime?: number | string;
}
export interface PageParams extends PageParam {
code?: string;
name?: string;
status?: number;
type?: number;
}
}
/** 查询生产工单分页 */
export function getWorkOrderPage(params: MesProWorkOrderApi.PageParams) {
return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>(
'/mes/pro/work-order/page',
{ params },
);
}
/** 查询生产工单详情 */
export function getWorkOrder(id: number) {
return requestClient.get<MesProWorkOrderApi.WorkOrder>(
`/mes/pro/work-order/get?id=${id}`,
);
}

View File

@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import { computed, onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Select } from 'ant-design-vue';
import { getAndonConfigList } from '#/api/mes/pro/andon/config';
import DictTag from '#/components/dict-tag/dict-tag.vue';
/** MES 安灯配置选择器:纯下拉,前端按 reason 过滤 */
defineOptions({ name: 'AndonConfigSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择呼叫原因',
},
);
const emit = defineEmits<{
change: [item: MesProAndonConfigApi.AndonConfig | undefined];
'update:modelValue': [value: number | undefined];
}>();
const allList = ref<MesProAndonConfigApi.AndonConfig[]>([]);
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
/** 前端过滤:按 reason 模糊匹配 */
function handleFilter(input: string, option: any) {
const keyword = input.toLowerCase();
const item = option?.item as MesProAndonConfigApi.AndonConfig | undefined;
return Boolean(item?.reason?.toLowerCase().includes(keyword));
}
/** 选中变化 */
function handleChange(value: any) {
const nextValue = value === undefined ? undefined : Number(value);
const item = allList.value.find((o) => o.id === nextValue);
emit('change', item);
}
onMounted(async () => {
allList.value = (await getAndonConfigList()) || [];
});
</script>
<template>
<Select
v-bind="$attrs"
v-model:value="selectValue"
:allow-clear="allowClear"
:disabled="disabled"
:filter-option="handleFilter"
:placeholder="placeholder"
class="w-full"
show-search
@change="handleChange"
>
<Select.Option
v-for="item in allList"
:key="item.id"
:item="item"
:value="item.id"
>
<div class="flex items-center gap-2">
<span>{{ item.reason }}</span>
<DictTag :type="DICT_TYPE.MES_PRO_ANDON_LEVEL" :value="item.level" />
</div>
</Select.Option>
</Select>
</template>

View File

@ -0,0 +1 @@
export { default as AndonConfigSelect } from './andon-config-select.vue';

View File

@ -0,0 +1,91 @@
<script lang="ts" setup>
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createAndonConfig,
getAndonConfig,
updateAndonConfig,
} from '#/api/mes/pro/andon/config';
import { $t } from '#/locales';
import { useConfigFormSchema } from '../../record/data';
const emit = defineEmits(['success']);
const formData = ref<MesProAndonConfigApi.AndonConfig>();
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-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useConfigFormSchema(),
showDefaultActions: false,
});
// TODO @AI
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
const data =
(await formApi.getValues()) as MesProAndonConfigApi.AndonConfig;
if (!data.handlerRoleId && !data.handlerUserId) {
message.warning('处置角色和处置人至少填一个');
modalApi.unlock();
return;
}
try {
await (formData.value?.id
? updateAndonConfig(data)
: createAndonConfig(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<MesProAndonConfigApi.AndonConfig>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getAndonConfig(data.id);
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,122 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteAndonConfig,
getAndonConfigList,
} from '#/api/mes/pro/andon/config';
import { $t } from '#/locales';
import { useConfigGridColumns } from '../../record/data';
import ConfigForm from './config-form.vue';
const list = ref<MesProAndonConfigApi.AndonConfig[]>([]);
const [ConfigFormModal, configFormModalApi] = useVbenModal({
connectedComponent: ConfigForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useConfigGridColumns(),
data: list.value,
minHeight: 320,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProAndonConfigApi.AndonConfig>,
});
/** 加载安灯配置列表 */
async function getList() {
gridApi.setLoading(true);
try {
list.value = (await getAndonConfigList()) || [];
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
/** 新增配置 */
function handleCreate() {
configFormModalApi.setData({}).open();
}
/** 编辑配置 */
function handleEdit(row: MesProAndonConfigApi.AndonConfig) {
configFormModalApi.setData({ id: row.id }).open();
}
/** 删除配置 */
async function handleDelete(row: MesProAndonConfigApi.AndonConfig) {
await deleteAndonConfig(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', ['安灯配置']));
await getList();
}
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
await getList();
},
});
defineExpose({ open: () => modalApi.open() });
</script>
<template>
<Modal :show-cancel-button="false" :show-confirm-button="false" class="w-3/5" title="安灯设置">
<ConfigFormModal @success="getList" />
<div class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['安灯配置']),
type: 'primary',
auth: ['mes:pro-andon-config:create'],
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
auth: ['mes:pro-andon-config:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
auth: ['mes:pro-andon-config:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['安灯配置']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Modal>
</template>

View File

@ -0,0 +1,368 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import type { MesProAndonRecordApi } from '#/api/mes/pro/andon/record';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getSimpleUserList } from '#/api/system/user';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { MesProWorkOrderStatusEnum } from '#/views/mes/utils/constants';
import { AndonConfigSelect } from '../config/components';
/** 列表搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'workstationId',
label: '工作站',
component: MdWorkstationSelect as any,
componentProps: { allowClear: true, placeholder: '请选择工作站' },
},
{
fieldName: 'userId',
label: '发起人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择发起人',
valueField: 'id',
},
},
{
fieldName: 'handlerUserId',
label: '处置人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择处置人',
valueField: 'id',
},
},
{
fieldName: 'status',
label: '处理状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_STATUS, 'number'),
placeholder: '请选择状态',
},
},
{
fieldName: 'createTime',
label: '发起时间',
component: 'RangePicker',
componentProps: {
allowClear: true,
format: 'YYYY-MM-DD HH:mm:ss',
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
},
];
}
/** 列表字段 */
export function useGridColumns(): VxeTableGridOptions<MesProAndonRecordApi.AndonRecord>['columns'] {
return [
{ field: 'workstationCode', title: '工作站编码', width: 140 },
{ field: 'workstationName', title: '工作站名称', minWidth: 140 },
{ field: 'workOrderCode', title: '工单编码', width: 140 },
{ field: 'processName', title: '工序名称', width: 140 },
{ field: 'userNickname', title: '发起人', width: 110 },
{
field: 'createTime',
title: '发起时间',
width: 180,
formatter: 'formatDateTime',
},
{ field: 'reason', title: '呼叫原因', minWidth: 160 },
{
field: 'level',
title: '级别',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_ANDON_LEVEL },
},
},
{
field: 'handleTime',
title: '处理时间',
width: 180,
formatter: 'formatDateTime',
},
{ field: 'handlerUserNickname', title: '处理人', width: 110 },
{
field: 'status',
title: '处置状态',
width: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_ANDON_STATUS },
},
},
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 安灯记录表单(按表单类型动态切换字段) */
export function useFormSchema(
formType: 'create' | 'detail' | 'update',
onConfigChange?: (config: MesProAndonConfigApi.AndonConfig | undefined) => void,
): VbenFormSchema[] {
const isCreate = formType === 'create';
const isUpdate = formType === 'update';
const isDetail = formType === 'detail';
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
isCreate
? {
fieldName: 'workstationId',
label: '工作站',
component: MdWorkstationSelect as any,
componentProps: { placeholder: '请选择工作站' },
rules: 'selectRequired',
}
: {
fieldName: 'workstationName',
label: '工作站',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'userId',
label: '发起人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择发起人',
valueField: 'id',
},
}
: {
fieldName: 'userNickname',
label: '发起人',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'workOrderId',
label: '生产工单',
component: ProWorkOrderSelect as any,
componentProps: {
placeholder: '请选择工单(可选)',
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
}
: {
fieldName: 'workOrderCode',
label: '生产工单',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'processId',
label: '工序',
component: ProProcessSelect as any,
componentProps: { placeholder: '请选择工序(可选)' },
}
: {
fieldName: 'processName',
label: '工序',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'configId',
label: '呼叫原因',
component: AndonConfigSelect as any,
componentProps: { onChange: onConfigChange },
rules: 'selectRequired',
}
: {
fieldName: 'reason',
label: '呼叫原因',
component: 'Input',
componentProps: { disabled: true },
},
{
fieldName: 'level',
label: '级别',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_LEVEL, 'number'),
placeholder: '由呼叫原因自动带出',
},
},
// 处置信息update / detail 才展示
...(isCreate
? []
: ([
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_STATUS, 'number'),
},
},
{
fieldName: 'handleTime',
label: '处置时间',
component: 'DatePicker',
componentProps: {
disabled: !isUpdate,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: isUpdate ? '请选择处置时间' : undefined,
showTime: true,
valueFormat: 'x',
},
},
isUpdate
? {
fieldName: 'handlerUserId',
label: '处置人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择处置人',
valueField: 'id',
},
}
: {
fieldName: 'handlerUserNickname',
label: '处置人',
component: 'Input',
componentProps: { disabled: true },
},
] as VbenFormSchema[])),
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
disabled: isDetail,
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 安灯配置表格列(弹窗内嵌网格) */
export function useConfigGridColumns(): VxeTableGridOptions<MesProAndonConfigApi.AndonConfig>['columns'] {
return [
{ field: 'reason', title: '呼叫原因', minWidth: 200 },
{
field: 'level',
title: '级别',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_ANDON_LEVEL },
},
},
{ field: 'handlerUserNickname', title: '处置人', width: 140 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 安灯配置表单(弹窗内的新增/编辑表单) */
export function useConfigFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'reason',
label: '呼叫原因',
component: 'Textarea',
componentProps: {
autoSize: { maxRows: 3, minRows: 1 },
maxLength: 200,
placeholder: '请输入呼叫原因',
},
rules: z.string().min(1, '呼叫原因不能为空').max(200),
},
{
fieldName: 'level',
label: '级别',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_LEVEL, 'number'),
placeholder: '请选择级别',
},
rules: 'selectRequired',
},
{
fieldName: 'handlerRoleId',
label: '处置角色',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: () =>
import('#/api/system/role').then((m) => m.getSimpleRoleList()),
labelField: 'name',
placeholder: '请选择角色(与处置人至少填一个)',
valueField: 'id',
},
},
{
fieldName: 'handlerUserId',
label: '处置人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择处置人(与角色至少填一个)',
valueField: 'id',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Input',
componentProps: { maxLength: 100, placeholder: '请输入备注' },
},
];
}

View File

@ -0,0 +1,174 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProAndonRecordApi } from '#/api/mes/pro/andon/record';
import { ref } from 'vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteAndonRecord,
exportAndonRecord,
getAndonRecordPage,
} from '#/api/mes/pro/andon/record';
import { $t } from '#/locales';
import { MesProAndonStatusEnum } from '#/views/mes/utils/constants';
import ConfigModal from '../config/modules/config-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const configModalRef = ref<InstanceType<typeof ConfigModal>>();
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 新增记录 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 处置记录 */
function handleHandle(row: MesProAndonRecordApi.AndonRecord) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
/** 详情 */
function handleDetail(row: MesProAndonRecordApi.AndonRecord) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
/** 删除 */
async function handleDelete(row: MesProAndonRecordApi.AndonRecord) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.workstationName]),
duration: 0,
});
try {
await deleteAndonRecord(row.id!);
message.success(
$t('ui.actionMessage.deleteSuccess', [row.workstationName]),
);
handleRefresh();
} finally {
hideLoading();
}
}
/** 导出 */
async function handleExport() {
const data = await exportAndonRecord(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '安灯呼叫记录.xls', source: data });
}
/** 打开安灯设置弹窗 */
function handleOpenConfig() {
configModalRef.value?.open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { schema: useGridFormSchema() },
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getAndonRecordPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: { isHover: true, keyField: 'id' },
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<MesProAndonRecordApi.AndonRecord>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】安灯配置、安灯呼叫"
url="https://doc.iocoder.cn/mes/pro/andon/"
/>
</template>
<FormModal @success="handleRefresh" />
<ConfigModal ref="configModalRef" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['安灯呼叫']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-andon-record:create'],
onClick: handleCreate,
},
{
label: '安灯设置',
type: 'primary',
auth: ['mes:pro-andon-config:query'],
onClick: handleOpenConfig,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-andon-record:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '处置',
type: 'link',
auth: ['mes:pro-andon-record:update'],
ifShow: () => row.status === MesProAndonStatusEnum.ACTIVE,
onClick: handleHandle.bind(null, row),
},
{
label: '详情',
type: 'link',
auth: ['mes:pro-andon-record:query'],
onClick: handleDetail.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
auth: ['mes:pro-andon-record:delete'],
ifShow: () => row.status === MesProAndonStatusEnum.ACTIVE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.workstationName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,205 @@
<script lang="ts" setup>
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import type { MesProAndonRecordApi } from '#/api/mes/pro/andon/record';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { formatDate } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createAndonRecord,
getAndonRecord,
updateAndonRecord,
} from '#/api/mes/pro/andon/record';
import { $t } from '#/locales';
import { MesProAndonStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<'create' | 'detail' | 'update'>('create'); //
const formData = ref<MesProAndonRecordApi.AndonRecord>({}); //
const userStore = useUserStore();
const dialogTitle = computed(() => {
switch (formType.value) {
case 'create': {
return '新增安灯呼叫';
}
case 'detail': {
return '安灯呼叫详情';
}
case 'update': {
return '处置安灯呼叫';
}
default: {
return '安灯呼叫';
}
}
});
/** 选择呼叫原因后自动填充级别 */
function handleConfigChange(config: MesProAndonConfigApi.AndonConfig | undefined) {
if (!config) {
formApi.setValues({ level: undefined, reason: undefined });
return;
}
formApi.setValues({ level: config.level, reason: config.reason });
}
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema('create', handleConfigChange),
showDefaultActions: false,
});
/** 提交:新增 */
async function handleCreate() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
try {
const data =
(await formApi.getValues()) as MesProAndonRecordApi.AndonRecord;
await createAndonRecord(data);
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
}
/** 处置:保存(保持 ACTIVE */
async function handleSave() {
modalApi.lock();
try {
const values =
(await formApi.getValues()) as MesProAndonRecordApi.AndonRecord;
await updateAndonRecord({
handlerUserId: values.handlerUserId,
handleTime: values.handleTime,
id: formData.value.id,
remark: values.remark,
status: MesProAndonStatusEnum.ACTIVE,
});
await modalApi.close();
emit('success');
message.success('保存成功');
} finally {
modalApi.unlock();
}
}
/** 处置:标记已处置 */
async function handleFinish() {
const values =
(await formApi.getValues()) as MesProAndonRecordApi.AndonRecord;
if (!values.handleTime) {
message.warning('标记已处置时,处置时间不能为空');
return;
}
if (!values.handlerUserId) {
message.warning('标记已处置时,处置人不能为空');
return;
}
modalApi.lock();
try {
await updateAndonRecord({
handlerUserId: values.handlerUserId,
handleTime: values.handleTime,
id: formData.value.id,
remark: values.remark,
status: MesProAndonStatusEnum.HANDLED,
});
await modalApi.close();
emit('success');
message.success('处置成功');
} finally {
modalApi.unlock();
}
}
// TODO @AI
// TODO @AI
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = {};
return;
}
const data = modalApi.getData<{
id?: number;
type: 'create' | 'detail' | 'update';
}>();
if (!data) {
return;
}
formType.value = data.type;
// schema便 type
formApi.setState({ schema: useFormSchema(data.type, handleConfigChange) });
await formApi.resetForm();
if (data.type === 'create') {
//
const currentUserId = userStore.userInfo?.id;
formData.value = { userId: currentUserId };
await formApi.setValues({ userId: currentUserId });
return;
}
// /
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getAndonRecord(data.id);
const initial: MesProAndonRecordApi.AndonRecord = { ...formData.value };
//
if (data.type === 'update') {
if (!initial.handleTime) {
initial.handleTime = formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss');
}
if (!initial.handlerUserId) {
initial.handlerUserId = userStore.userInfo?.id;
}
}
await formApi.setValues(initial);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="dialogTitle" class="w-1/2">
<Form />
<template #footer>
<template v-if="formType === 'create'">
<Button @click="modalApi.close()"></Button>
<Button type="primary" @click="handleCreate"></Button>
</template>
<template v-else-if="formType === 'update'">
<Button @click="modalApi.close()"></Button>
<Button type="primary" @click="handleSave"></Button>
<Button type="primary" danger @click="handleFinish"></Button>
</template>
<template v-else>
<Button @click="modalApi.close()"></Button>
</template>
</template>
</Modal>
</template>

View File

@ -0,0 +1,66 @@
<script lang="ts" setup>
import { computed } from 'vue';
/** 甘特图颜色选择器ant-design-vue 4.x 暂未提供 ColorPicker封装原生 input[type=color] */
defineOptions({ name: 'RouteColorPicker' });
const props = withDefaults(
defineProps<{
disabled?: boolean;
modelValue?: string;
}>(),
{
disabled: false,
modelValue: '',
},
);
const emit = defineEmits<{
change: [value: string];
'update:modelValue': [value: string];
}>();
/** 用于 input[type=color] 的展示值,必须是合法 hex非法时回退到 #000000 但不修改 modelValue */
const swatchValue = computed(() =>
/^#[0-9a-f]{6}$/i.test(props.modelValue ?? '')
? (props.modelValue as string)
: '#000000',
);
/** 颜色变化时同步 modelValue */
function handleColorChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
emit('update:modelValue', value);
emit('change', value);
}
</script>
<template>
<div class="flex items-center gap-2">
<input
class="route-color-picker__swatch"
:disabled="disabled"
type="color"
:value="swatchValue"
@change="handleColorChange"
@input="handleColorChange"
/>
<span v-if="modelValue">{{ modelValue }}</span>
</div>
</template>
<style scoped>
.route-color-picker__swatch {
block-size: 28px;
border: 1px solid var(--ant-color-border, #d9d9d9);
border-radius: 4px;
cursor: pointer;
inline-size: 36px;
padding: 2px;
}
.route-color-picker__swatch:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>

View File

@ -0,0 +1 @@
export { default as RouteColorPicker } from './color-picker.vue';

View File

@ -7,7 +7,7 @@ import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@ -20,6 +20,8 @@ import {
} from '#/views/mes/md/item/components';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
import { RouteColorPicker } from './components';
/** 工艺路线表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
@ -120,7 +122,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表字段 */
export function useGridColumns(): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
export function useGridColumns(
onStatusChange?: (
newStatus: number,
row: MesProRouteApi.Route,
) => PromiseLike<boolean | undefined>,
statusEditable = true,
): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
return [
{
field: 'code',
@ -134,7 +142,16 @@ export function useGridColumns(): VxeTableGridOptions<MesProRouteApi.Route>['col
field: 'status',
title: '状态',
width: 110,
slots: { default: 'status' },
align: 'center',
cellRender: {
attrs: { beforeChange: onStatusChange },
name: 'CellSwitch',
props: {
checkedValue: CommonStatusEnum.ENABLE,
disabled: !statusEditable,
unCheckedValue: CommonStatusEnum.DISABLE,
},
},
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
@ -200,11 +217,7 @@ export function useRouteProcessFormSchema(
{
fieldName: 'colorCode',
label: '甘特图颜色',
component: 'Input',
componentProps: {
maxLength: 16,
placeholder: '请输入颜色 hex例如 #00AEF3',
},
component: RouteColorPicker,
},
{
fieldName: 'keyFlag',

View File

@ -2,11 +2,13 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import { useAccess } from '@vben/access';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Modal, Switch, Tooltip } from 'ant-design-vue';
import { Button, message, Modal, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@ -20,6 +22,9 @@ import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const { hasAccessByCodes } = useAccess();
const statusEditable = hasAccessByCodes(['mes:pro-route:update']); //
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
@ -46,20 +51,22 @@ function handleDetail(row: MesProRouteApi.Route) {
}
/** 切换状态 */
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 handleStatusChange(
newStatus: number,
row: MesProRouteApi.Route,
): Promise<boolean | undefined> {
return new Promise((resolve, reject) => {
Modal.confirm({
content: `确认要将"${row.name}"工艺路线切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}】吗?`,
async onOk() {
await updateRouteStatus(row.id!, newStatus);
message.success($t('ui.actionMessage.operationSuccess'));
resolve(true);
},
onCancel() {
reject(new Error('取消操作'));
},
});
});
}
@ -89,7 +96,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
columns: useGridColumns(handleStatusChange, statusEditable),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -146,20 +153,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<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"

View File

@ -10,6 +10,7 @@ import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createRouteProductBom,
getRouteProductBom,
updateRouteProductBom,
} from '#/api/mes/pro/route/productbom';
import { $t } from '#/locales';
@ -80,27 +81,33 @@ const [Modal, modalApi] = useVbenModal({
await formApi.resetForm();
//
const data = modalApi.getData<{
id?: number;
processId: number;
productId: number;
routeId: number;
row?: MesProRouteProductBomApi.RouteProductBom;
}>();
if (!data) {
return;
}
productId.value = data.productId;
if (data.row) {
formData.value = data.row;
// values
await formApi.setValues(data.row);
if (!data.id) {
// routeId/processId/productId
await formApi.setValues({
processId: data.processId,
productId: data.productId,
quantity: 1,
routeId: data.routeId,
});
return;
}
await formApi.setValues({
processId: data.processId,
productId: data.productId,
quantity: 1,
routeId: data.routeId,
});
modalApi.lock();
try {
formData.value = await getRouteProductBom(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>

View File

@ -0,0 +1 @@
export { default as ProWorkOrderSelect } from './pro-work-order-select.vue';

View File

@ -0,0 +1,144 @@
<script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { computed, onMounted, ref, watch } from 'vue';
import { Select, Tag, Tooltip } from 'ant-design-vue';
import { getWorkOrder, getWorkOrderPage } from '#/api/mes/pro/workorder';
/**
* MES 生产工单选择器轻量版
*
* 当前用于安灯记录等只需要单选工单 ID 的业务页面
* - 默认按 `status` 过滤拉取首页 100 条工单作为下拉
* - 编辑回显走 `getWorkOrder(id)`
* - 后续 `mes/pro/workorder` 完整迁移后可替换为带弹窗的复杂选择器
*/
defineOptions({ name: 'ProWorkOrderSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
pageSize?: number;
placeholder?: string;
status?: number;
type?: number;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
pageSize: 100,
placeholder: '请选择工单',
status: undefined,
type: undefined,
},
);
const emit = defineEmits<{
change: [item: MesProWorkOrderApi.WorkOrder | undefined];
'update:modelValue': [value: number | undefined];
}>();
const allList = ref<MesProWorkOrderApi.WorkOrder[]>([]);
const selectedItem = ref<MesProWorkOrderApi.WorkOrder>();
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
/** 前端过滤:按工单编码或名称模糊匹配 */
function handleFilter(input: string, option: any) {
const keyword = input.toLowerCase();
const item = option?.item as MesProWorkOrderApi.WorkOrder | undefined;
return Boolean(
item?.code?.toLowerCase().includes(keyword) ||
item?.name?.toLowerCase().includes(keyword),
);
}
/** 同步选中工单详情,未在列表内时单独拉取 */
async function syncSelectedItem(value: number | undefined) {
if (value === undefined) {
selectedItem.value = undefined;
return;
}
const found = allList.value.find((item) => item.id === value);
if (found) {
selectedItem.value = found;
return;
}
try {
selectedItem.value = await getWorkOrder(value);
} catch (error) {
console.error('[ProWorkOrderSelect] resolveItemById failed:', error);
}
}
/** 除 v-model 外,额外抛出完整工单对象给业务表单使用 */
function handleChange(value: any) {
const nextValue = value === undefined ? undefined : Number(value);
syncSelectedItem(nextValue);
emit('change', selectedItem.value);
}
watch(
() => props.modelValue,
(value) => {
syncSelectedItem(value);
},
);
onMounted(async () => {
const data = await getWorkOrderPage({
pageNo: 1,
pageSize: props.pageSize,
status: props.status,
type: props.type,
});
allList.value = data.list ?? [];
syncSelectedItem(props.modelValue);
});
</script>
<template>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>编码{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div>产品{{ selectedItem.productName || '-' }}</div>
<div>数量{{ selectedItem.quantity ?? '-' }}</div>
</div>
</template>
<Select
v-bind="$attrs"
v-model:value="selectValue"
:allow-clear="allowClear"
:disabled="disabled"
:filter-option="handleFilter"
:placeholder="placeholder"
class="w-full"
show-search
@change="handleChange"
>
<Select.Option
v-for="item in allList"
:key="item.id"
:item="item"
:value="item.id"
>
<div class="flex items-center gap-2">
<span>{{ item.code }}</span>
<Tag v-if="item.name" color="default">{{ item.name }}</Tag>
</div>
</Select.Option>
</Select>
</Tooltip>
</template>

View File

@ -181,20 +181,17 @@ export const MesProCardStatusEnum = {
CANCELLED: MesOrderStatusConstants.CANCELLED,
} as const;
/** MES 安灯类型枚举 */
export const MesProAndonTypeEnum = {
QUALITY: 1,
EQUIPMENT: 2,
MATERIAL: 3,
PROCESS: 4,
OTHER: 9,
/** MES 安灯处置状态枚举 */
export const MesProAndonStatusEnum = {
ACTIVE: 0, // 未处置
HANDLED: 1, // 已处置
} as const;
/** MES 安灯状态枚举 */
export const MesProAndonStatusEnum = {
TRIGGERED: 1,
HANDLING: 2,
CLOSED: 3,
/** MES 安灯级别枚举 */
export const MesProAndonLevelEnum = {
LEVEL1: 1,
LEVEL2: 2,
LEVEL3: 3,
} as const;
/** MES 编码规则分段类型枚举 */

View File

@ -0,0 +1,48 @@
import { requestClient } from '#/api/request';
export namespace MesProAndonConfigApi {
/** MES 安灯配置 */
export interface AndonConfig {
id?: number;
reason?: string; // 呼叫原因
level?: number; // 级别
handlerRoleId?: number; // 处置角色编号
handlerUserId?: number; // 处置人编号
handlerUserNickname?: string; // 处置人姓名(详情回显)
remark?: string;
}
}
/** 查询安灯配置分页 */
export function getAndonConfigPage(params: any) {
return requestClient.get('/mes/pro/andon-config/page', { params });
}
/** 查询安灯配置列表 */
export function getAndonConfigList() {
return requestClient.get<MesProAndonConfigApi.AndonConfig[]>(
'/mes/pro/andon-config/list',
);
}
/** 查询安灯配置详情 */
export function getAndonConfig(id: number) {
return requestClient.get<MesProAndonConfigApi.AndonConfig>(
`/mes/pro/andon-config/get?id=${id}`,
);
}
/** 新增安灯配置 */
export function createAndonConfig(data: MesProAndonConfigApi.AndonConfig) {
return requestClient.post('/mes/pro/andon-config/create', data);
}
/** 修改安灯配置 */
export function updateAndonConfig(data: MesProAndonConfigApi.AndonConfig) {
return requestClient.put('/mes/pro/andon-config/update', data);
}
/** 删除安灯配置 */
export function deleteAndonConfig(id: number) {
return requestClient.delete(`/mes/pro/andon-config/delete?id=${id}`);
}

View File

@ -0,0 +1,76 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProAndonRecordApi {
/** MES 安灯记录 */
export interface AndonRecord {
id?: number;
configId?: number; // 安灯配置编号
workstationId?: number; // 工作站编号
workstationCode?: string; // 工作站编码
workstationName?: string; // 工作站名称
workOrderId?: number; // 生产工单编号
workOrderCode?: string; // 工单编码
processId?: number; // 工序编号
processName?: string; // 工序名称
userId?: number; // 发起用户编号
userNickname?: string; // 发起人昵称
reason?: string; // 呼叫原因
level?: number; // 级别
status?: number; // 处置状态
handleTime?: number; // 处置时间(毫秒时间戳)
handlerUserId?: number; // 处置人编号
handlerUserNickname?: string; // 处置人昵称
remark?: string; // 备注
createTime?: number; // 发起时间
}
/** MES 安灯记录分页查询参数 */
export interface PageParams extends PageParam {
workstationId?: number; // 工作站编号
userId?: number; // 发起用户编号
handlerUserId?: number; // 处置人编号
status?: number; // 处置状态
createTime?: string[]; // 发起时间区间
}
}
/** 查询安灯记录分页 */
export function getAndonRecordPage(params: MesProAndonRecordApi.PageParams) {
return requestClient.get<PageResult<MesProAndonRecordApi.AndonRecord>>(
'/mes/pro/andon-record/page',
{ params },
);
}
/** 查询安灯记录详情 */
export function getAndonRecord(id: number) {
return requestClient.get<MesProAndonRecordApi.AndonRecord>(
`/mes/pro/andon-record/get?id=${id}`,
);
}
/** 新增安灯记录 */
export function createAndonRecord(data: MesProAndonRecordApi.AndonRecord) {
return requestClient.post('/mes/pro/andon-record/create', data);
}
/** 删除安灯记录 */
export function deleteAndonRecord(id: number) {
return requestClient.delete(`/mes/pro/andon-record/delete?id=${id}`);
}
/** 更新安灯记录(保存/已处置) */
export function updateAndonRecord(data: MesProAndonRecordApi.AndonRecord) {
return requestClient.put('/mes/pro/andon-record/update', data);
}
/** 导出安灯记录 Excel */
export function exportAndonRecord(
params: Partial<MesProAndonRecordApi.PageParams>,
) {
return requestClient.download('/mes/pro/andon-record/export-excel', {
params,
});
}

View File

@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProWorkOrderApi {
/** MES 生产工单 */
export interface WorkOrder {
id?: number;
code?: string; // 工单编码
name?: string; // 工单名称
type?: number; // 工单类型
status?: number; // 工单状态
sourceType?: number;
productId?: number; // 产品物料编号
productCode?: string;
productName?: string;
productSpecification?: string;
quantity?: number;
unitName?: string;
routeId?: number;
routeName?: string;
clientId?: number;
clientName?: string;
planStartTime?: number | string;
planEndTime?: number | string;
actualStartTime?: number | string;
actualEndTime?: number | string;
remark?: string;
createTime?: number | string;
}
export interface PageParams extends PageParam {
code?: string;
name?: string;
status?: number;
type?: number;
}
}
/** 查询生产工单分页 */
export function getWorkOrderPage(params: MesProWorkOrderApi.PageParams) {
return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>(
'/mes/pro/work-order/page',
{ params },
);
}
/** 查询生产工单详情 */
export function getWorkOrder(id: number) {
return requestClient.get<MesProWorkOrderApi.WorkOrder>(
`/mes/pro/work-order/get?id=${id}`,
);
}

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import { computed, onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElOption, ElSelect } from 'element-plus';
import { getAndonConfigList } from '#/api/mes/pro/andon/config';
import DictTag from '#/components/dict-tag/dict-tag.vue';
/** MES 安灯配置选择器:纯下拉,前端按 reason 过滤 */
defineOptions({ name: 'AndonConfigSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择呼叫原因',
},
);
const emit = defineEmits<{
change: [item: MesProAndonConfigApi.AndonConfig | undefined];
'update:modelValue': [value: number | undefined];
}>();
const allList = ref<MesProAndonConfigApi.AndonConfig[]>([]);
const filteredList = ref<MesProAndonConfigApi.AndonConfig[]>([]);
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
/** 前端过滤:按 reason 模糊匹配 */
function handleFilter(query: string) {
if (!query) {
filteredList.value = allList.value;
return;
}
const keyword = query.toLowerCase();
filteredList.value = allList.value.filter((item) =>
item.reason?.toLowerCase().includes(keyword),
);
}
/** 选中变化 */
function handleChange(value: number | undefined) {
const item = allList.value.find((o) => o.id === value);
emit('change', item);
}
onMounted(async () => {
allList.value = (await getAndonConfigList()) || [];
filteredList.value = allList.value;
});
</script>
<template>
<ElSelect
v-bind="$attrs"
v-model="selectValue"
:clearable="clearable"
:disabled="disabled"
:filter-method="handleFilter"
:placeholder="placeholder"
class="w-full"
filterable
@change="handleChange"
>
<ElOption
v-for="item in filteredList"
:key="item.id"
:label="item.reason"
:value="item.id!"
>
<div class="flex items-center gap-2">
<span>{{ item.reason }}</span>
<DictTag :type="DICT_TYPE.MES_PRO_ANDON_LEVEL" :value="item.level" />
</div>
</ElOption>
</ElSelect>
</template>

View File

@ -0,0 +1 @@
export { default as AndonConfigSelect } from './andon-config-select.vue';

View File

@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createAndonConfig,
getAndonConfig,
updateAndonConfig,
} from '#/api/mes/pro/andon/config';
import { $t } from '#/locales';
import { useConfigFormSchema } from '../../record/data';
const emit = defineEmits(['success']);
const formData = ref<MesProAndonConfigApi.AndonConfig>();
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-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useConfigFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
const data =
(await formApi.getValues()) as MesProAndonConfigApi.AndonConfig;
if (!data.handlerRoleId && !data.handlerUserId) {
ElMessage.warning('处置角色和处置人至少填一个');
modalApi.unlock();
return;
}
try {
await (formData.value?.id
? updateAndonConfig(data)
: createAndonConfig(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<MesProAndonConfigApi.AndonConfig>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getAndonConfig(data.id);
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,128 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteAndonConfig,
getAndonConfigList,
} from '#/api/mes/pro/andon/config';
import { $t } from '#/locales';
import { useConfigGridColumns } from '../../record/data';
import ConfigForm from './config-form.vue';
const list = ref<MesProAndonConfigApi.AndonConfig[]>([]);
const [ConfigFormModal, configFormModalApi] = useVbenModal({
connectedComponent: ConfigForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
autoResize: true,
border: true,
columns: useConfigGridColumns(),
data: list.value,
minHeight: 320,
pagerConfig: { enabled: false },
rowConfig: { isHover: true, keyField: 'id' },
showOverflow: true,
toolbarConfig: { enabled: false },
} as VxeTableGridOptions<MesProAndonConfigApi.AndonConfig>,
});
/** 加载安灯配置列表 */
async function getList() {
gridApi.setLoading(true);
try {
list.value = (await getAndonConfigList()) || [];
gridApi.setGridOptions({ data: list.value });
} finally {
gridApi.setLoading(false);
}
}
/** 新增配置 */
function handleCreate() {
configFormModalApi.setData({}).open();
}
/** 编辑配置 */
function handleEdit(row: MesProAndonConfigApi.AndonConfig) {
configFormModalApi.setData({ id: row.id }).open();
}
/** 删除配置 */
async function handleDelete(row: MesProAndonConfigApi.AndonConfig) {
await deleteAndonConfig(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', ['安灯配置']));
await getList();
}
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
await getList();
},
});
defineExpose({ open: () => modalApi.open() });
</script>
<template>
<Modal
:show-cancel-button="false"
:show-confirm-button="false"
class="w-3/5"
title="安灯设置"
>
<ConfigFormModal @success="getList" />
<div class="mb-3 flex items-center justify-start">
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['安灯配置']),
type: 'primary',
auth: ['mes:pro-andon-config:create'],
onClick: handleCreate,
},
]"
/>
</div>
<Grid class="w-full" table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
auth: ['mes:pro-andon-config:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
auth: ['mes:pro-andon-config:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', ['安灯配置']),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Modal>
</template>

View File

@ -0,0 +1,369 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import type { MesProAndonRecordApi } from '#/api/mes/pro/andon/record';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getSimpleUserList } from '#/api/system/user';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { MesProWorkOrderStatusEnum } from '#/views/mes/utils/constants';
import { AndonConfigSelect } from '../config/components';
/** 列表搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'workstationId',
label: '工作站',
component: MdWorkstationSelect as any,
componentProps: { clearable: true, placeholder: '请选择工作站' },
},
{
fieldName: 'userId',
label: '发起人',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
clearable: true,
labelField: 'nickname',
placeholder: '请选择发起人',
valueField: 'id',
},
},
{
fieldName: 'handlerUserId',
label: '处置人',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
clearable: true,
labelField: 'nickname',
placeholder: '请选择处置人',
valueField: 'id',
},
},
{
fieldName: 'status',
label: '处理状态',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_STATUS, 'number'),
placeholder: '请选择状态',
},
},
{
fieldName: 'createTime',
label: '发起时间',
component: 'DatePicker',
componentProps: {
clearable: true,
format: 'YYYY-MM-DD HH:mm:ss',
type: 'datetimerange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
},
];
}
/** 列表字段 */
export function useGridColumns(): VxeTableGridOptions<MesProAndonRecordApi.AndonRecord>['columns'] {
return [
{ field: 'workstationCode', title: '工作站编码', width: 140 },
{ field: 'workstationName', title: '工作站名称', minWidth: 140 },
{ field: 'workOrderCode', title: '工单编码', width: 140 },
{ field: 'processName', title: '工序名称', width: 140 },
{ field: 'userNickname', title: '发起人', width: 110 },
{
field: 'createTime',
title: '发起时间',
width: 180,
formatter: 'formatDateTime',
},
{ field: 'reason', title: '呼叫原因', minWidth: 160 },
{
field: 'level',
title: '级别',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_ANDON_LEVEL },
},
},
{
field: 'handleTime',
title: '处理时间',
width: 180,
formatter: 'formatDateTime',
},
{ field: 'handlerUserNickname', title: '处理人', width: 110 },
{
field: 'status',
title: '处置状态',
width: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_ANDON_STATUS },
},
},
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 安灯记录表单(按表单类型动态切换字段) */
export function useFormSchema(
formType: 'create' | 'detail' | 'update',
onConfigChange?: (config: MesProAndonConfigApi.AndonConfig | undefined) => void,
): VbenFormSchema[] {
const isCreate = formType === 'create';
const isUpdate = formType === 'update';
const isDetail = formType === 'detail';
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
isCreate
? {
fieldName: 'workstationId',
label: '工作站',
component: MdWorkstationSelect as any,
componentProps: { placeholder: '请选择工作站' },
rules: 'selectRequired',
}
: {
fieldName: 'workstationName',
label: '工作站',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'userId',
label: '发起人',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
clearable: true,
labelField: 'nickname',
placeholder: '请选择发起人',
valueField: 'id',
},
}
: {
fieldName: 'userNickname',
label: '发起人',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'workOrderId',
label: '生产工单',
component: ProWorkOrderSelect as any,
componentProps: {
placeholder: '请选择工单(可选)',
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
}
: {
fieldName: 'workOrderCode',
label: '生产工单',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'processId',
label: '工序',
component: ProProcessSelect as any,
componentProps: { placeholder: '请选择工序(可选)' },
}
: {
fieldName: 'processName',
label: '工序',
component: 'Input',
componentProps: { disabled: true },
},
isCreate
? {
fieldName: 'configId',
label: '呼叫原因',
component: AndonConfigSelect as any,
componentProps: { onChange: onConfigChange },
rules: 'selectRequired',
}
: {
fieldName: 'reason',
label: '呼叫原因',
component: 'Input',
componentProps: { disabled: true },
},
{
fieldName: 'level',
label: '级别',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_LEVEL, 'number'),
placeholder: '由呼叫原因自动带出',
},
},
// 处置信息update / detail 才展示
...(isCreate
? []
: ([
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_STATUS, 'number'),
},
},
{
fieldName: 'handleTime',
label: '处置时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
disabled: !isUpdate,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: isUpdate ? '请选择处置时间' : undefined,
type: 'datetime',
valueFormat: 'x',
},
},
isUpdate
? {
fieldName: 'handlerUserId',
label: '处置人',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
clearable: true,
labelField: 'nickname',
placeholder: '请选择处置人',
valueField: 'id',
},
}
: {
fieldName: 'handlerUserNickname',
label: '处置人',
component: 'Input',
componentProps: { disabled: true },
},
] as VbenFormSchema[])),
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
autosize: { maxRows: 3, minRows: 2 },
disabled: isDetail,
maxLength: 250,
placeholder: '请输入备注',
},
},
];
}
/** 安灯配置表格列(弹窗内嵌网格) */
export function useConfigGridColumns(): VxeTableGridOptions<MesProAndonConfigApi.AndonConfig>['columns'] {
return [
{ field: 'reason', title: '呼叫原因', minWidth: 200 },
{
field: 'level',
title: '级别',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_ANDON_LEVEL },
},
},
{ field: 'handlerUserNickname', title: '处置人', width: 140 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 安灯配置表单(弹窗内的新增/编辑表单) */
export function useConfigFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'reason',
label: '呼叫原因',
component: 'Textarea',
componentProps: {
autosize: { maxRows: 3, minRows: 1 },
maxLength: 200,
placeholder: '请输入呼叫原因',
},
rules: z.string().min(1, '呼叫原因不能为空').max(200),
},
{
fieldName: 'level',
label: '级别',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.MES_PRO_ANDON_LEVEL, 'number'),
placeholder: '请选择级别',
},
rules: 'selectRequired',
},
{
fieldName: 'handlerRoleId',
label: '处置角色',
component: 'ApiSelect',
componentProps: {
api: () =>
import('#/api/system/role').then((m) => m.getSimpleRoleList()),
clearable: true,
labelField: 'name',
placeholder: '请选择角色(与处置人至少填一个)',
valueField: 'id',
},
},
{
fieldName: 'handlerUserId',
label: '处置人',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
clearable: true,
labelField: 'nickname',
placeholder: '请选择处置人(与角色至少填一个)',
valueField: 'id',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Input',
componentProps: { maxLength: 100, placeholder: '请输入备注' },
},
];
}

View File

@ -0,0 +1,175 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProAndonRecordApi } from '#/api/mes/pro/andon/record';
import { ref } from 'vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteAndonRecord,
exportAndonRecord,
getAndonRecordPage,
} from '#/api/mes/pro/andon/record';
import { $t } from '#/locales';
import { MesProAndonStatusEnum } from '#/views/mes/utils/constants';
import ConfigModal from '../config/modules/config-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const configModalRef = ref<InstanceType<typeof ConfigModal>>();
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 新增记录 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 处置记录 */
function handleHandle(row: MesProAndonRecordApi.AndonRecord) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
/** 详情 */
function handleDetail(row: MesProAndonRecordApi.AndonRecord) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
/** 删除 */
async function handleDelete(row: MesProAndonRecordApi.AndonRecord) {
const hideLoading = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.workstationName]),
});
try {
await deleteAndonRecord(row.id!);
ElMessage.success(
$t('ui.actionMessage.deleteSuccess', [row.workstationName]),
);
handleRefresh();
} finally {
hideLoading.close();
}
}
/** 导出 */
async function handleExport() {
const data = await exportAndonRecord(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '安灯呼叫记录.xls', source: data });
}
/** 打开安灯设置弹窗 */
function handleOpenConfig() {
configModalRef.value?.open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { schema: useGridFormSchema() },
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getAndonRecordPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: { isHover: true, keyField: 'id' },
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<MesProAndonRecordApi.AndonRecord>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】安灯配置、安灯呼叫"
url="https://doc.iocoder.cn/mes/pro/andon/"
/>
</template>
<FormModal @success="handleRefresh" />
<ConfigModal ref="configModalRef" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['安灯呼叫']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-andon-record:create'],
onClick: handleCreate,
},
{
label: '安灯设置',
type: 'primary',
auth: ['mes:pro-andon-config:query'],
onClick: handleOpenConfig,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-andon-record:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '处置',
type: 'primary',
link: true,
auth: ['mes:pro-andon-record:update'],
ifShow: () => row.status === MesProAndonStatusEnum.ACTIVE,
onClick: handleHandle.bind(null, row),
},
{
label: '详情',
type: 'primary',
link: true,
auth: ['mes:pro-andon-record:query'],
onClick: handleDetail.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
auth: ['mes:pro-andon-record:delete'],
ifShow: () => row.status === MesProAndonStatusEnum.ACTIVE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.workstationName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,199 @@
<script lang="ts" setup>
import type { MesProAndonConfigApi } from '#/api/mes/pro/andon/config';
import type { MesProAndonRecordApi } from '#/api/mes/pro/andon/record';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { formatDate } from '@vben/utils';
import { ElButton, ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createAndonRecord,
getAndonRecord,
updateAndonRecord,
} from '#/api/mes/pro/andon/record';
import { $t } from '#/locales';
import { MesProAndonStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<'create' | 'detail' | 'update'>('create');
const formData = ref<MesProAndonRecordApi.AndonRecord>({});
const userStore = useUserStore();
const dialogTitle = computed(() => {
switch (formType.value) {
case 'create': {
return '新增安灯呼叫';
}
case 'detail': {
return '安灯呼叫详情';
}
case 'update': {
return '处置安灯呼叫';
}
default: {
return '安灯呼叫';
}
}
});
/** 选择呼叫原因后自动填充级别 */
function handleConfigChange(config: MesProAndonConfigApi.AndonConfig | undefined) {
if (!config) {
formApi.setValues({ level: undefined, reason: undefined });
return;
}
formApi.setValues({ level: config.level, reason: config.reason });
}
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema('create', handleConfigChange),
showDefaultActions: false,
});
/** 提交:新增 */
async function handleCreate() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
try {
const data =
(await formApi.getValues()) as MesProAndonRecordApi.AndonRecord;
await createAndonRecord(data);
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
}
/** 处置:保存(保持 ACTIVE */
async function handleSave() {
modalApi.lock();
try {
const values =
(await formApi.getValues()) as MesProAndonRecordApi.AndonRecord;
await updateAndonRecord({
handlerUserId: values.handlerUserId,
handleTime: values.handleTime,
id: formData.value.id,
remark: values.remark,
status: MesProAndonStatusEnum.ACTIVE,
});
await modalApi.close();
emit('success');
ElMessage.success('保存成功');
} finally {
modalApi.unlock();
}
}
/** 处置:标记已处置 */
async function handleFinish() {
const values =
(await formApi.getValues()) as MesProAndonRecordApi.AndonRecord;
if (!values.handleTime) {
ElMessage.warning('标记已处置时,处置时间不能为空');
return;
}
if (!values.handlerUserId) {
ElMessage.warning('标记已处置时,处置人不能为空');
return;
}
modalApi.lock();
try {
await updateAndonRecord({
handlerUserId: values.handlerUserId,
handleTime: values.handleTime,
id: formData.value.id,
remark: values.remark,
status: MesProAndonStatusEnum.HANDLED,
});
await modalApi.close();
emit('success');
ElMessage.success('处置成功');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = {};
return;
}
const data = modalApi.getData<{
id?: number;
type: 'create' | 'detail' | 'update';
}>();
if (!data) {
return;
}
formType.value = data.type;
formApi.setState({ schema: useFormSchema(data.type, handleConfigChange) });
await formApi.resetForm();
if (data.type === 'create') {
const currentUserId = userStore.userInfo?.id;
formData.value = { userId: currentUserId };
await formApi.setValues({ userId: currentUserId });
return;
}
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getAndonRecord(data.id);
const initial: MesProAndonRecordApi.AndonRecord = { ...formData.value };
if (data.type === 'update') {
if (!initial.handleTime) {
initial.handleTime = formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss');
}
if (!initial.handlerUserId) {
initial.handlerUserId = userStore.userInfo?.id;
}
}
await formApi.setValues(initial);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="dialogTitle" class="w-1/2">
<Form />
<template #footer>
<template v-if="formType === 'create'">
<ElButton @click="modalApi.close()"></ElButton>
<ElButton type="primary" @click="handleCreate"></ElButton>
</template>
<template v-else-if="formType === 'update'">
<ElButton @click="modalApi.close()"></ElButton>
<ElButton type="primary" @click="handleSave"></ElButton>
<ElButton type="success" @click="handleFinish"></ElButton>
</template>
<template v-else>
<ElButton @click="modalApi.close()"></ElButton>
</template>
</template>
</Modal>
</template>

View File

@ -0,0 +1,40 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { ElColorPicker } from 'element-plus';
/** 甘特图颜色选择器(封装 ElColorPicker并在右侧展示 hex 文本) */
defineOptions({ name: 'RouteColorPicker' });
const props = withDefaults(
defineProps<{
disabled?: boolean;
modelValue?: string;
}>(),
{
disabled: false,
modelValue: '',
},
);
const emit = defineEmits<{
change: [value: string];
'update:modelValue': [value: string];
}>();
/** 内部 v-model 适配 ElColorPicker避免对外抛出 null */
const color = computed({
get: () => props.modelValue || '',
set: (value: null | string | undefined) => {
emit('update:modelValue', value ?? '');
emit('change', value ?? '');
},
});
</script>
<template>
<div class="flex items-center gap-2">
<ElColorPicker v-model="color" :disabled="disabled" />
<span v-if="color">{{ color }}</span>
</div>
</template>

View File

@ -0,0 +1 @@
export { default as RouteColorPicker } from './color-picker.vue';

View File

@ -7,7 +7,7 @@ import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElButton } from 'element-plus';
@ -20,6 +20,8 @@ import {
} from '#/views/mes/md/item/components';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
import { RouteColorPicker } from './components';
/** 工艺路线表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
@ -120,7 +122,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表字段 */
export function useGridColumns(): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
export function useGridColumns(
onStatusChange?: (
newStatus: number,
row: MesProRouteApi.Route,
) => PromiseLike<boolean | undefined>,
statusEditable = true,
): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
return [
{
field: 'code',
@ -134,7 +142,16 @@ export function useGridColumns(): VxeTableGridOptions<MesProRouteApi.Route>['col
field: 'status',
title: '状态',
width: 110,
slots: { default: 'status' },
align: 'center',
cellRender: {
attrs: { beforeChange: onStatusChange },
name: 'CellSwitch',
props: {
activeValue: CommonStatusEnum.ENABLE,
disabled: !statusEditable,
inactiveValue: CommonStatusEnum.DISABLE,
},
},
},
{ field: 'remark', title: '备注', minWidth: 160 },
{
@ -205,11 +222,7 @@ export function useRouteProcessFormSchema(
{
fieldName: 'colorCode',
label: '甘特图颜色',
component: 'Input',
componentProps: {
maxLength: 16,
placeholder: '请输入颜色 hex例如 #00AEF3',
},
component: RouteColorPicker,
},
{
fieldName: 'keyFlag',

View File

@ -2,8 +2,10 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import { useAccess } from '@vben/access';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import {
@ -11,7 +13,6 @@ import {
ElLoading,
ElMessage,
ElMessageBox,
ElSwitch,
ElTooltip,
} from 'element-plus';
@ -27,44 +28,54 @@ import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const { hasAccessByCodes } = useAccess();
const statusEditable = hasAccessByCodes(['mes:pro-route:update']); //
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;
/** 切换状态 */
async function handleStatusChange(
newStatus: number,
row: MesProRouteApi.Route,
): Promise<boolean | undefined> {
try {
await ElMessageBox.confirm(
`确认要"${text}""${row.name}"工艺路线吗?`,
`确认要将"${row.name}"工艺路线切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}吗?`,
'提示',
{ type: 'warning' },
);
await updateRouteStatus(row.id!, value);
await updateRouteStatus(row.id!, newStatus);
ElMessage.success($t('ui.actionMessage.operationSuccess'));
handleRefresh();
return true;
} catch {
row.status = previousStatus;
return false;
}
}
/** 删除(仅停用状态可删除) */
async function handleDelete(row: MesProRouteApi.Route) {
const hideLoading = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.name]),
@ -78,6 +89,7 @@ async function handleDelete(row: MesProRouteApi.Route) {
}
}
/** 导出 */
async function handleExport() {
const data = await exportRoute(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '工艺路线.xls', source: data });
@ -86,7 +98,7 @@ async function handleExport() {
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { schema: useGridFormSchema() },
gridOptions: {
columns: useGridColumns(),
columns: useGridColumns(handleStatusChange, statusEditable),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -141,20 +153,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
{{ 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"

View File

@ -10,6 +10,7 @@ import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createRouteProductBom,
getRouteProductBom,
updateRouteProductBom,
} from '#/api/mes/pro/route/productbom';
import { $t } from '#/locales';
@ -80,27 +81,33 @@ const [Modal, modalApi] = useVbenModal({
await formApi.resetForm();
//
const data = modalApi.getData<{
id?: number;
processId: number;
productId: number;
routeId: number;
row?: MesProRouteProductBomApi.RouteProductBom;
}>();
if (!data) {
return;
}
productId.value = data.productId;
if (data.row) {
formData.value = data.row;
// values
await formApi.setValues(data.row);
if (!data.id) {
// routeId/processId/productId
await formApi.setValues({
processId: data.processId,
productId: data.productId,
quantity: 1,
routeId: data.routeId,
});
return;
}
await formApi.setValues({
processId: data.processId,
productId: data.productId,
quantity: 1,
routeId: data.routeId,
});
modalApi.lock();
try {
formData.value = await getRouteProductBom(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>

View File

@ -0,0 +1 @@
export { default as ProWorkOrderSelect } from './pro-work-order-select.vue';

View File

@ -0,0 +1,150 @@
<script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { computed, onMounted, ref, watch } from 'vue';
import { ElOption, ElSelect, ElTag, ElTooltip } from 'element-plus';
import { getWorkOrder, getWorkOrderPage } from '#/api/mes/pro/workorder';
/**
* MES 生产工单选择器轻量版
*
* 当前用于安灯记录等只需要单选工单 ID 的业务页面
* - 默认按 `status` 过滤拉取首页 100 条工单作为下拉
* - 编辑回显走 `getWorkOrder(id)`
* - 后续 `mes/pro/workorder` 完整迁移后可替换为带弹窗的复杂选择器
*/
defineOptions({ name: 'ProWorkOrderSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: number;
pageSize?: number;
placeholder?: string;
status?: number;
type?: number;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
pageSize: 100,
placeholder: '请选择工单',
status: undefined,
type: undefined,
},
);
const emit = defineEmits<{
change: [item: MesProWorkOrderApi.WorkOrder | undefined];
'update:modelValue': [value: number | undefined];
}>();
const allList = ref<MesProWorkOrderApi.WorkOrder[]>([]);
const filteredList = ref<MesProWorkOrderApi.WorkOrder[]>([]);
const selectedItem = ref<MesProWorkOrderApi.WorkOrder>();
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
function handleFilter(query: string) {
if (!query) {
filteredList.value = allList.value;
return;
}
const keyword = query.toLowerCase();
filteredList.value = allList.value.filter(
(item) =>
item.code?.toLowerCase().includes(keyword) ||
item.name?.toLowerCase().includes(keyword),
);
}
/** 同步选中工单详情,未在列表内时单独拉取 */
async function syncSelectedItem(value: number | undefined) {
if (value === undefined) {
selectedItem.value = undefined;
return;
}
const found = allList.value.find((item) => item.id === value);
if (found) {
selectedItem.value = found;
return;
}
try {
selectedItem.value = await getWorkOrder(value);
} catch (error) {
console.error('[ProWorkOrderSelect] resolveItemById failed:', error);
}
}
/** 除 v-model 外,额外抛出完整工单对象给业务表单使用 */
function handleChange(value: number | undefined) {
syncSelectedItem(value);
emit('change', selectedItem.value);
}
watch(
() => props.modelValue,
(value) => {
syncSelectedItem(value);
},
);
onMounted(async () => {
const data = await getWorkOrderPage({
pageNo: 1,
pageSize: props.pageSize,
status: props.status,
type: props.type,
});
allList.value = data.list ?? [];
filteredList.value = allList.value;
syncSelectedItem(props.modelValue);
});
</script>
<template>
<ElTooltip :disabled="!selectedItem" placement="top" :show-after="500">
<template #content>
<div v-if="selectedItem" class="leading-6">
<div>编码{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div>产品{{ selectedItem.productName || '-' }}</div>
<div>数量{{ selectedItem.quantity ?? '-' }}</div>
</div>
</template>
<ElSelect
v-bind="$attrs"
v-model="selectValue"
:clearable="clearable"
:disabled="disabled"
:filter-method="handleFilter"
:placeholder="placeholder"
class="w-full"
filterable
@change="handleChange"
>
<ElOption
v-for="item in filteredList"
:key="item.id"
:label="item.code"
:value="item.id!"
>
<div class="flex items-center gap-2">
<span>{{ item.code }}</span>
<ElTag v-if="item.name" size="small" type="info">
{{ item.name }}
</ElTag>
</div>
</ElOption>
</ElSelect>
</ElTooltip>
</template>

View File

@ -181,20 +181,17 @@ export const MesProCardStatusEnum = {
CANCELLED: MesOrderStatusConstants.CANCELLED,
} as const;
/** MES 安灯类型枚举 */
export const MesProAndonTypeEnum = {
QUALITY: 1,
EQUIPMENT: 2,
MATERIAL: 3,
PROCESS: 4,
OTHER: 9,
/** MES 安灯处置状态枚举 */
export const MesProAndonStatusEnum = {
ACTIVE: 0, // 未处置
HANDLED: 1, // 已处置
} as const;
/** MES 安灯状态枚举 */
export const MesProAndonStatusEnum = {
TRIGGERED: 1,
HANDLING: 2,
CLOSED: 3,
/** MES 安灯级别枚举 */
export const MesProAndonLevelEnum = {
LEVEL1: 1,
LEVEL2: 2,
LEVEL3: 3,
} as const;
/** MES 编码规则分段类型枚举 */