refactor(mes): stocktaking/plan 盘点条件表单 schema 化(R034)

将盘点方案条件弹窗从手写 <Form> 模板重构为 useVbenForm + schema,提升
antd/ele 复用度,对齐 Vben5「schema 放 data.ts」约定。

- 新增 condition-value-input.vue(antd/ele 各一份):把「条件值」的 7 选 1
  (仓库/库区/库位级联、物料、批次、质量状态)+ 级联临时态 + valueCode/
  valueName 回写封装为单一 v-model 组件,经 valueChange 事件回填,避免
  schema 出现重复 fieldName 的渲染层风险。归 components/,与 pro/route
  的 RouteColorPicker 等同类自定义控件保持一致。
- useParamFormSchema 合并进同目录 data.ts,删除临时 param-data.ts;
  param-form.vue 改为 useVbenForm + setState(schema),onConfirm/onOpenChange
  结构与注释(// 提交表单、// 关闭并提示、// 加载数据、// 设置到 values)
  对齐 dv/subject 范式。
- 字典 options 用 getDictOptions(..., 'number') + NumberDictDataType 断言,
  移除冗余 .map() 转换。
- defineProps/defineEmits 内去掉非 vben 风格的 JSDoc 注释;去掉无谓的
  ParamTypeEnum alias,直接使用 MesWmStockTakingParamTypeEnum。
- 质量状态无实体 id,提交校验按 type 区分 valueCode / valueId。

验证:
- pnpm exec eslint <stocktaking/plan 改动文件>(antd + ele)通过
- pnpm -F @vben/web-antd / @vben/web-ele exec vue-tsc 过滤 stocktaking/plan 无报错

Ref: project_duibiao/mes/review_vben/INDEX.md (MES-R034)
pull/351/MERGE
YunaiV 2026-05-30 22:15:55 +08:00
parent 7e62f9a5ef
commit 1edaf023c2
9 changed files with 1035 additions and 415 deletions

View File

@ -0,0 +1,204 @@
<script lang="ts" setup>
import type { NumberDictDataType } from '@vben/hooks';
import { ref, watch } from 'vue';
import { DICT_TYPE, MesWmStockTakingParamTypeEnum } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Select } from 'ant-design-vue';
import { getWarehouseArea } from '#/api/mes/wm/warehouse/area';
import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { WmBatchSelect } from '#/views/mes/wm/batch/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
defineOptions({ name: 'StockTakingPlanConditionValueInput' });
const props = withDefaults(
defineProps<{
modelValue?: number;
type?: number;
valueCode?: string;
}>(),
{
modelValue: undefined,
type: undefined,
valueCode: undefined,
},
);
const emit = defineEmits<{
'update:modelValue': [value?: number];
valueChange: [
payload: { valueCode?: string; valueId?: number; valueName?: string },
];
}>();
const qualityStatusOptions = getDictOptions(
DICT_TYPE.MES_WM_QUALITY_STATUS,
'number',
) as NumberDictDataType[];
const locationWarehouseId = ref<number>(); //
const areaWarehouseId = ref<number>(); //
const areaLocationId = ref<number>(); //
const qualityStatusValue = ref<number>();
/** 通用选择器变化:回填条件值编码、名称 */
function handleSelectorChange(item?: any) {
emit('update:modelValue', item?.id);
emit('valueChange', {
valueId: item?.id,
valueCode: item?.code || '',
valueName: item?.name || item?.nickname || '',
});
}
/** 批次选择器变化 */
function handleBatchChange(batch?: any) {
emit('update:modelValue', batch?.id);
emit('valueChange', {
valueId: batch?.id,
valueCode: batch?.code || '',
valueName: batch?.code || '',
});
}
/** 质量状态选择器变化:无实体编号,仅记录字典编码和文案 */
function handleQualityStatusChange(value?: number) {
qualityStatusValue.value = value;
const selected = qualityStatusOptions.find((item) => item.value === value);
emit('update:modelValue', undefined);
emit('valueChange', {
valueId: undefined,
valueCode: value == null ? '' : String(value),
valueName: selected?.label || '',
});
}
/** 库区仓库选择回调:清空库区 */
function handleLocationWarehouseChange() {
emit('update:modelValue', undefined);
emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** 库位仓库选择回调:清空库区和库位 */
function handleAreaWarehouseChange() {
areaLocationId.value = undefined;
emit('update:modelValue', undefined);
emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** 库位库区选择回调:清空库位 */
function handleAreaLocationChange() {
emit('update:modelValue', undefined);
emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** 编辑时回填级联选择器的上级数据(库区所属仓库、库位所属仓库/库区) */
async function loadCascadeData() {
if (props.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS) {
qualityStatusValue.value = props.valueCode
? Number(props.valueCode)
: undefined;
return;
}
if (props.modelValue == null) {
return;
}
if (props.type === MesWmStockTakingParamTypeEnum.LOCATION) {
const location = await getWarehouseLocation(props.modelValue);
locationWarehouseId.value = location?.warehouseId;
} else if (props.type === MesWmStockTakingParamTypeEnum.AREA) {
const area = await getWarehouseArea(props.modelValue);
areaWarehouseId.value = area?.warehouseId;
areaLocationId.value = area?.locationId;
}
}
//
watch(
() => props.type,
() => {
locationWarehouseId.value = undefined;
areaWarehouseId.value = undefined;
areaLocationId.value = undefined;
qualityStatusValue.value = undefined;
void loadCascadeData();
},
{ immediate: true },
);
</script>
<template>
<WmWarehouseSelect
v-if="type === MesWmStockTakingParamTypeEnum.WAREHOUSE"
:model-value="modelValue"
@change="handleSelectorChange"
/>
<div
v-else-if="type === MesWmStockTakingParamTypeEnum.LOCATION"
class="space-y-2"
>
<WmWarehouseSelect
v-model="locationWarehouseId"
placeholder="请选择仓库"
@change="handleLocationWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="locationWarehouseId"
:model-value="modelValue"
:warehouse-id="locationWarehouseId"
placeholder="请选择库区"
@change="handleSelectorChange"
/>
</div>
<div
v-else-if="type === MesWmStockTakingParamTypeEnum.AREA"
class="space-y-2"
>
<WmWarehouseSelect
v-model="areaWarehouseId"
placeholder="请选择仓库"
@change="handleAreaWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="areaWarehouseId"
v-model="areaLocationId"
placeholder="请选择库区"
:warehouse-id="areaWarehouseId"
@change="handleAreaLocationChange"
/>
<WmWarehouseAreaSelect
v-if="areaLocationId"
:model-value="modelValue"
:location-id="areaLocationId"
placeholder="请选择库位"
@change="handleSelectorChange"
/>
</div>
<MdItemSelect
v-else-if="type === MesWmStockTakingParamTypeEnum.ITEM"
:model-value="modelValue"
@change="handleSelectorChange"
/>
<WmBatchSelect
v-else-if="type === MesWmStockTakingParamTypeEnum.BATCH"
:model-value="modelValue"
@change="handleBatchChange"
/>
<Select
v-else-if="type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS"
:options="qualityStatusOptions"
:value="qualityStatusValue"
placeholder="请选择质量状态"
class="w-full"
@change="(value) => handleQualityStatusChange(value as number)"
/>
</template>

View File

@ -1,2 +1,3 @@
export { default as StockTakingPlanConditionValueInput } from './condition-value-input.vue';
export { default as StockTakingPlanSelectDialog } from './select-dialog.vue';
export { default as StockTakingPlanSelect } from './select.vue';

View File

@ -1,9 +1,11 @@
import type { NumberDictDataType } from '@vben/hooks';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmStockTakingPlanApi } from '#/api/mes/wm/stocktaking/plan';
import type { MesWmStockTakingPlanParamApi } from '#/api/mes/wm/stocktaking/plan/param';
import { h } from 'vue';
import { h, markRaw } from 'vue';
import {
CommonStatusEnum,
@ -18,6 +20,8 @@ import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { StockTakingPlanConditionValueInput } from './components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'update';
@ -292,6 +296,86 @@ export function useParamGridColumns(
];
}
/** 盘点方案条件表单 schema */
export function useParamFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'type',
label: '条件类型',
component: 'Select',
componentProps: {
options: getDictOptions(
DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE,
'number',
) as NumberDictDataType[],
placeholder: '请选择条件类型',
// 条件类型变化:清空已选条件值,避免残留旧类型的条件值
onChange: async () => {
await formApi?.setValues({
valueCode: '',
valueId: undefined,
valueName: '',
});
},
},
rules: 'selectRequired',
},
{
fieldName: 'valueId',
label: '条件值',
component: markRaw(StockTakingPlanConditionValueInput),
// 条件值控件内部按条件类型切换选择器,仅选择类型后展示
dependencies: {
triggerFields: ['type'],
if: (values) => values.type != null,
componentProps: (values) => ({
type: values.type,
valueCode: values.valueCode,
// 条件值控件回填 valueId / valueCode / valueName
onValueChange: async (payload: {
valueCode?: string;
valueId?: number;
valueName?: string;
}) => {
await formApi?.setValues({
valueCode: payload.valueCode ?? '',
valueId: payload.valueId,
valueName: payload.valueName ?? '',
});
},
}),
},
},
{
// 条件值编码:由条件值控件回写,隐藏字段
fieldName: 'valueCode',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
// 条件值名称:由条件值控件回写,隐藏字段
fieldName: 'valueName',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 选择弹窗的搜索表单 */
export function useSelectGridFormSchema(): VbenFormSchema[] {
return [

View File

@ -4,144 +4,68 @@ import type { MesWmStockTakingPlanParamApi } from '#/api/mes/wm/stocktaking/plan
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE, MesWmStockTakingParamTypeEnum } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { MesWmStockTakingParamTypeEnum } from '@vben/constants';
import { Form, message, Select, Textarea } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createStockTakingPlanParam,
getStockTakingPlanParam,
updateStockTakingPlanParam,
} from '#/api/mes/wm/stocktaking/plan/param';
import { getWarehouseArea } from '#/api/mes/wm/warehouse/area';
import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location';
import { $t } from '#/locales';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { WmBatchSelect } from '#/views/mes/wm/batch/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
import { useParamFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmStockTakingPlanParamApi.StockTakingPlanParam>({});
const planId = ref<number>(); // TODO @AI
const locationWarehouseId = ref<number>(); //
const areaWarehouseId = ref<number>(); //
const areaLocationId = ref<number>(); //
const formId = ref<number>(); //
const planId = ref<number>(); //
// TODO @AI mapping
const paramTypeOptions = getDictOptions(
DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE,
'number',
).map(({ label, value }) => ({ label, value: Number(value) }));
const qualityStatusOptions = getDictOptions(
DICT_TYPE.MES_WM_QUALITY_STATUS,
'string',
).map(({ label, value }) => ({ label, value: String(value) }));
// TODO @AI title const
const getTitle = computed(() =>
formData.value?.id
formId.value
? $t('ui.actionTitle.edit', ['盘点条件'])
: $t('ui.actionTitle.create', ['盘点条件']),
);
/** 条件类型变化:清空已选条件值和级联临时数据 */
function handleTypeChange() {
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
locationWarehouseId.value = undefined;
areaWarehouseId.value = undefined;
areaLocationId.value = undefined;
}
/** 通用选择器变化:回填条件值编码、名称 */
function handleSelectorChange(item?: any) {
formData.value.valueId = item?.id;
formData.value.valueCode = item?.code || '';
formData.value.valueName = item?.name || item?.nickname || '';
}
/** 批次选择器变化 */
function handleBatchChange(batch?: any) {
formData.value.valueId = batch?.id;
formData.value.valueCode = batch?.code || '';
formData.value.valueName = batch?.code || '';
}
/** 质量状态选择器变化:无实体编号,仅记录字典编码和文案 */
function handleQualityStatusChange(value: any) {
const selected = qualityStatusOptions.find((item) => item.value === value);
formData.value.valueId = undefined;
formData.value.valueCode = value;
formData.value.valueName = selected?.label || '';
}
/** 库区仓库选择回调:清空库区 */
function handleLocationWarehouseChange() {
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
}
/** 库位仓库选择回调:清空库区和库位 */
function handleAreaWarehouseChange() {
areaLocationId.value = undefined;
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
}
/** 库位库区选择回调:清空库位 */
function handleAreaLocationChange() {
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
}
/** 编辑时回填级联选择器的上级数据(库区所属仓库、库位所属仓库/库区) */
async function loadCascadeData() {
if (!formData.value.type || !formData.value.valueId) {
return;
}
const valueId = formData.value.valueId;
if (formData.value.type === MesWmStockTakingParamTypeEnum.LOCATION) {
const location = await getWarehouseLocation(valueId);
locationWarehouseId.value = location?.warehouseId;
} else if (formData.value.type === MesWmStockTakingParamTypeEnum.AREA) {
const area = await getWarehouseArea(valueId);
areaWarehouseId.value = area?.warehouseId;
areaLocationId.value = area?.locationId;
}
}
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!formData.value.type) {
message.warning('请选择条件类型');
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const values =
(await formApi.getValues()) as MesWmStockTakingPlanParamApi.StockTakingPlanParam;
// valueCode valueId
const valid =
formData.value.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS
? !!formData.value.valueCode
: formData.value.valueId != null;
if (!valid) {
const valueValid =
values.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS
? !!values.valueCode
: values.valueId != null;
if (!valueValid) {
message.warning('请选择条件值');
return;
}
modalApi.lock();
//
const data = {
...formData.value,
...values,
id: formId.value,
planId: planId.value,
} as MesWmStockTakingPlanParamApi.StockTakingPlanParam;
try {
await (formData.value.id
await (formId.value
? updateStockTakingPlanParam(data)
: createStockTakingPlanParam(data));
//
@ -154,113 +78,31 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = {};
locationWarehouseId.value = undefined;
areaWarehouseId.value = undefined;
areaLocationId.value = undefined;
formId.value = undefined;
return;
}
formApi.setState({ schema: useParamFormSchema(formApi) });
//
const data = modalApi.getData<{ id?: number; planId: number }>();
planId.value = data.planId;
formId.value = data.id;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getStockTakingPlanParam(data.id);
await loadCascadeData();
const param = await getStockTakingPlanParam(data.id);
// values
await formApi.setValues(param);
} finally {
modalApi.unlock();
}
},
});
const ParamTypeEnum = MesWmStockTakingParamTypeEnum;
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" :label-col="{ span: 4 }">
<!-- TODO @AI可以更加 form schema 更好的 antd ele 的复用 -->
<Form.Item label="条件类型" required>
<Select
v-model:value="formData.type"
:options="paramTypeOptions"
placeholder="请选择条件类型"
@change="handleTypeChange"
/>
</Form.Item>
<Form.Item v-if="formData.type" label="条件值" required>
<WmWarehouseSelect
v-if="formData.type === ParamTypeEnum.WAREHOUSE"
v-model="formData.valueId"
@change="handleSelectorChange"
/>
<div
v-else-if="formData.type === ParamTypeEnum.LOCATION"
class="space-y-2"
>
<WmWarehouseSelect
v-model="locationWarehouseId"
placeholder="请选择仓库"
@change="handleLocationWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="locationWarehouseId"
v-model="formData.valueId"
placeholder="请选择库区"
:warehouse-id="locationWarehouseId"
@change="handleSelectorChange"
/>
</div>
<div v-else-if="formData.type === ParamTypeEnum.AREA" class="space-y-2">
<WmWarehouseSelect
v-model="areaWarehouseId"
placeholder="请选择仓库"
@change="handleAreaWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="areaWarehouseId"
v-model="areaLocationId"
placeholder="请选择库区"
:warehouse-id="areaWarehouseId"
@change="handleAreaLocationChange"
/>
<WmWarehouseAreaSelect
v-if="areaLocationId"
v-model="formData.valueId"
:location-id="areaLocationId"
placeholder="请选择库位"
@change="handleSelectorChange"
/>
</div>
<MdItemSelect
v-else-if="formData.type === ParamTypeEnum.ITEM"
v-model="formData.valueId"
@change="handleSelectorChange"
/>
<WmBatchSelect
v-else-if="formData.type === ParamTypeEnum.BATCH"
v-model="formData.valueId"
@change="handleBatchChange"
/>
<Select
v-else-if="formData.type === ParamTypeEnum.QUALITY_STATUS"
v-model:value="formData.valueCode"
:options="qualityStatusOptions"
placeholder="请选择质量状态"
@change="handleQualityStatusChange"
/>
</Form.Item>
<Form.Item label="备注">
<Textarea
v-model:value="formData.remark"
placeholder="请输入备注"
:rows="3"
/>
</Form.Item>
</Form>
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,372 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import type { DescriptionItemSchema } from '#/components/description';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDate } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProTaskSelect } from '#/views/mes/pro/task/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { TmToolSelect } from '#/views/mes/tm/tool/components';
/** 批次选择弹窗的搜索表单 */
export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '批次编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入批次编号',
},
},
{
fieldName: 'itemId',
label: '产品物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品物料',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
placeholder: '请选择生产工单',
},
},
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
placeholder: '请选择工作站',
},
},
{
fieldName: 'taskId',
label: '生产任务',
component: markRaw(ProTaskSelect),
componentProps: {
placeholder: '请选择生产任务',
},
},
{
fieldName: 'toolId',
label: '工具',
component: markRaw(TmToolSelect),
componentProps: {
placeholder: '请选择工具',
},
},
{
fieldName: 'moldId',
label: '模具编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入模具编号',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入采购订单编号',
},
},
{
fieldName: 'lotNumber',
label: '生产批号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入生产批号',
},
},
{
fieldName: 'qualityStatus',
label: '质量状态',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS, 'number'),
placeholder: '请选择质量状态',
},
},
{
fieldName: 'produceDate',
label: '生产日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
{
fieldName: 'expireDate',
label: '有效期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
];
}
/** 批次选择弹窗的字段 */
export function useBatchSelectGridColumns(
multiple = false,
): VxeTableGridOptions<MesWmBatchApi.Batch>['columns'] {
return [
{
type: multiple ? 'checkbox' : 'radio',
width: 50,
},
{
field: 'code',
title: '批次编号',
width: 150,
},
{
field: 'itemCode',
title: '物料编码',
width: 150,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'itemSpecification',
title: '规格型号',
width: 120,
},
{
field: 'unitName',
title: '单位',
width: 80,
},
{
field: 'vendorCode',
title: '供应商编码',
width: 120,
},
{
field: 'vendorName',
title: '供应商名称',
width: 120,
},
{
field: 'clientCode',
title: '客户编码',
width: 110,
},
{
field: 'clientName',
title: '客户名称',
width: 110,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
width: 140,
},
{
field: 'purchaseOrderCode',
title: '采购订单编号',
width: 140,
},
{
field: 'workOrderCode',
title: '工单编码',
width: 140,
},
{
field: 'workstationCode',
title: '工作站编码',
width: 120,
},
{
field: 'taskCode',
title: '生产任务编号',
width: 140,
},
{
field: 'toolCode',
title: '工具编号',
width: 120,
},
{
field: 'lotNumber',
title: '生产批号',
width: 120,
},
{
field: 'qualityStatus',
title: '质量状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
},
},
{
field: 'produceDate',
title: '生产日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'expireDate',
title: '有效期',
width: 120,
formatter: 'formatDate',
},
{
field: 'receiptDate',
title: '入库日期',
width: 120,
formatter: 'formatDate',
},
];
}
/** 批次详情的描述字段 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{
field: 'code',
label: '批次编号',
},
{
field: 'itemCode',
label: '物料编码',
},
{
field: 'itemName',
label: '物料名称',
},
{
field: 'itemSpecification',
label: '规格型号',
},
{
field: 'unitName',
label: '单位',
},
{
field: 'lotNumber',
label: '生产批号',
},
{
field: 'produceDate',
label: '生产日期',
render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'),
},
{
field: 'expireDate',
label: '有效期',
render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'),
},
{
field: 'receiptDate',
label: '入库日期',
render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'),
},
{
field: 'vendorName',
label: '供应商',
render: (value) => value || '-',
},
{
field: 'clientName',
label: '客户',
render: (value) => value || '-',
},
{
field: 'workstationCode',
label: '工作站',
render: (value) => value || '-',
},
{
field: 'purchaseOrderCode',
label: '采购订单编号',
render: (value) => value || '-',
},
{
field: 'salesOrderCode',
label: '销售订单编号',
render: (value) => value || '-',
},
{
field: 'workOrderCode',
label: '生产工单',
render: (value) => value || '-',
},
{
field: 'qualityStatus',
label: '质量状态',
render: (value) =>
value == null
? '-'
: h(DictTag, { type: DICT_TYPE.MES_WM_QUALITY_STATUS, value }),
},
{
field: 'remark',
label: '备注',
span: 3,
render: (value) => value || '-',
},
];
}

View File

@ -0,0 +1,210 @@
<script lang="ts" setup>
import type { NumberDictDataType } from '@vben/hooks';
import { ref, watch } from 'vue';
import { DICT_TYPE, MesWmStockTakingParamTypeEnum } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElOption, ElSelect } from 'element-plus';
import { getWarehouseArea } from '#/api/mes/wm/warehouse/area';
import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { WmBatchSelect } from '#/views/mes/wm/batch/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
defineOptions({ name: 'StockTakingPlanConditionValueInput' });
const props = withDefaults(
defineProps<{
modelValue?: number;
type?: number;
valueCode?: string;
}>(),
{
modelValue: undefined,
type: undefined,
valueCode: undefined,
},
);
const emit = defineEmits<{
'update:modelValue': [value?: number];
valueChange: [
payload: { valueCode?: string; valueId?: number; valueName?: string },
];
}>();
const qualityStatusOptions = getDictOptions(
DICT_TYPE.MES_WM_QUALITY_STATUS,
'number',
) as NumberDictDataType[];
const locationWarehouseId = ref<number>(); //
const areaWarehouseId = ref<number>(); //
const areaLocationId = ref<number>(); //
const qualityStatusValue = ref<number>();
/** 通用选择器变化:回填条件值编码、名称 */
function handleSelectorChange(item?: any) {
emit('update:modelValue', item?.id);
emit('valueChange', {
valueId: item?.id,
valueCode: item?.code || '',
valueName: item?.name || item?.nickname || '',
});
}
/** 批次选择器变化 */
function handleBatchChange(batch?: any) {
emit('update:modelValue', batch?.id);
emit('valueChange', {
valueId: batch?.id,
valueCode: batch?.code || '',
valueName: batch?.code || '',
});
}
/** 质量状态选择器变化:无实体编号,仅记录字典编码和文案 */
function handleQualityStatusChange(value?: number) {
qualityStatusValue.value = value;
const selected = qualityStatusOptions.find((item) => item.value === value);
emit('update:modelValue', undefined);
emit('valueChange', {
valueId: undefined,
valueCode: value == null ? '' : String(value),
valueName: selected?.label || '',
});
}
/** 库区仓库选择回调:清空库区 */
function handleLocationWarehouseChange() {
emit('update:modelValue', undefined);
emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** 库位仓库选择回调:清空库区和库位 */
function handleAreaWarehouseChange() {
areaLocationId.value = undefined;
emit('update:modelValue', undefined);
emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** 库位库区选择回调:清空库位 */
function handleAreaLocationChange() {
emit('update:modelValue', undefined);
emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** 编辑时回填级联选择器的上级数据(库区所属仓库、库位所属仓库/库区) */
async function loadCascadeData() {
if (props.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS) {
qualityStatusValue.value = props.valueCode
? Number(props.valueCode)
: undefined;
return;
}
if (props.modelValue == null) {
return;
}
if (props.type === MesWmStockTakingParamTypeEnum.LOCATION) {
const location = await getWarehouseLocation(props.modelValue);
locationWarehouseId.value = location?.warehouseId;
} else if (props.type === MesWmStockTakingParamTypeEnum.AREA) {
const area = await getWarehouseArea(props.modelValue);
areaWarehouseId.value = area?.warehouseId;
areaLocationId.value = area?.locationId;
}
}
//
watch(
() => props.type,
() => {
locationWarehouseId.value = undefined;
areaWarehouseId.value = undefined;
areaLocationId.value = undefined;
qualityStatusValue.value = undefined;
void loadCascadeData();
},
{ immediate: true },
);
</script>
<template>
<WmWarehouseSelect
v-if="type === MesWmStockTakingParamTypeEnum.WAREHOUSE"
:model-value="modelValue"
@change="handleSelectorChange"
/>
<div
v-else-if="type === MesWmStockTakingParamTypeEnum.LOCATION"
class="w-full space-y-2"
>
<WmWarehouseSelect
v-model="locationWarehouseId"
placeholder="请选择仓库"
@change="handleLocationWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="locationWarehouseId"
:model-value="modelValue"
:warehouse-id="locationWarehouseId"
placeholder="请选择库区"
@change="handleSelectorChange"
/>
</div>
<div
v-else-if="type === MesWmStockTakingParamTypeEnum.AREA"
class="w-full space-y-2"
>
<WmWarehouseSelect
v-model="areaWarehouseId"
placeholder="请选择仓库"
@change="handleAreaWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="areaWarehouseId"
v-model="areaLocationId"
placeholder="请选择库区"
:warehouse-id="areaWarehouseId"
@change="handleAreaLocationChange"
/>
<WmWarehouseAreaSelect
v-if="areaLocationId"
:model-value="modelValue"
:location-id="areaLocationId"
placeholder="请选择库位"
@change="handleSelectorChange"
/>
</div>
<MdItemSelect
v-else-if="type === MesWmStockTakingParamTypeEnum.ITEM"
:model-value="modelValue"
@change="handleSelectorChange"
/>
<WmBatchSelect
v-else-if="type === MesWmStockTakingParamTypeEnum.BATCH"
:model-value="modelValue"
@change="handleBatchChange"
/>
<ElSelect
v-else-if="type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS"
:model-value="qualityStatusValue"
class="!w-full"
placeholder="请选择质量状态"
@change="(value) => handleQualityStatusChange(value as number)"
>
<ElOption
v-for="dict in qualityStatusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</ElSelect>
</template>

View File

@ -1,2 +1,3 @@
export { default as StockTakingPlanConditionValueInput } from './condition-value-input.vue';
export { default as StockTakingPlanSelectDialog } from './select-dialog.vue';
export { default as StockTakingPlanSelect } from './select.vue';

View File

@ -1,9 +1,11 @@
import type { NumberDictDataType } from '@vben/hooks';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmStockTakingPlanApi } from '#/api/mes/wm/stocktaking/plan';
import type { MesWmStockTakingPlanParamApi } from '#/api/mes/wm/stocktaking/plan/param';
import { h } from 'vue';
import { h, markRaw } from 'vue';
import {
CommonStatusEnum,
@ -18,6 +20,8 @@ import { ElButton } from 'element-plus';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { StockTakingPlanConditionValueInput } from './components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'update';
@ -293,6 +297,87 @@ export function useParamGridColumns(
];
}
/** 盘点方案条件表单 schema */
export function useParamFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'type',
label: '条件类型',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(
DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE,
'number',
) as NumberDictDataType[],
placeholder: '请选择条件类型',
// 条件类型变化:清空已选条件值,避免残留旧类型的条件值
onChange: async () => {
await formApi?.setValues({
valueCode: '',
valueId: undefined,
valueName: '',
});
},
},
rules: 'selectRequired',
},
{
fieldName: 'valueId',
label: '条件值',
component: markRaw(StockTakingPlanConditionValueInput),
// 条件值控件内部按条件类型切换选择器,仅选择类型后展示
dependencies: {
triggerFields: ['type'],
if: (values) => values.type != null,
componentProps: (values) => ({
type: values.type,
valueCode: values.valueCode,
// 条件值控件回填 valueId / valueCode / valueName
onValueChange: async (payload: {
valueCode?: string;
valueId?: number;
valueName?: string;
}) => {
await formApi?.setValues({
valueCode: payload.valueCode ?? '',
valueId: payload.valueId,
valueName: payload.valueName ?? '',
});
},
}),
},
},
{
// 条件值编码:由条件值控件回写,隐藏字段
fieldName: 'valueCode',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
// 条件值名称:由条件值控件回写,隐藏字段
fieldName: 'valueName',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 选择弹窗的搜索表单 */
export function useSelectGridFormSchema(): VbenFormSchema[] {
return [

View File

@ -4,149 +4,68 @@ import type { MesWmStockTakingPlanParamApi } from '#/api/mes/wm/stocktaking/plan
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE, MesWmStockTakingParamTypeEnum } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { MesWmStockTakingParamTypeEnum } from '@vben/constants';
import {
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElOption,
ElSelect,
} from 'element-plus';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createStockTakingPlanParam,
getStockTakingPlanParam,
updateStockTakingPlanParam,
} from '#/api/mes/wm/stocktaking/plan/param';
import { getWarehouseArea } from '#/api/mes/wm/warehouse/area';
import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location';
import { $t } from '#/locales';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { WmBatchSelect } from '#/views/mes/wm/batch/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
import { useParamFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmStockTakingPlanParamApi.StockTakingPlanParam>({});
const planId = ref<number>();
const locationWarehouseId = ref<number>(); //
const areaWarehouseId = ref<number>(); //
const areaLocationId = ref<number>(); //
const paramTypeOptions = getDictOptions(
DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE,
'number',
).map(({ label, value }) => ({ label, value: Number(value) }));
const qualityStatusOptions = getDictOptions(
DICT_TYPE.MES_WM_QUALITY_STATUS,
'string',
).map(({ label, value }) => ({ label, value: String(value) }));
const formId = ref<number>(); //
const planId = ref<number>(); //
const getTitle = computed(() =>
formData.value?.id
formId.value
? $t('ui.actionTitle.edit', ['盘点条件'])
: $t('ui.actionTitle.create', ['盘点条件']),
);
/** 条件类型变化:清空已选条件值和级联临时数据 */
function handleTypeChange() {
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
locationWarehouseId.value = undefined;
areaWarehouseId.value = undefined;
areaLocationId.value = undefined;
}
/** 通用选择器变化:回填条件值编码、名称 */
function handleSelectorChange(item?: any) {
formData.value.valueId = item?.id;
formData.value.valueCode = item?.code || '';
formData.value.valueName = item?.name || item?.nickname || '';
}
/** 批次选择器变化 */
function handleBatchChange(batch?: any) {
formData.value.valueId = batch?.id;
formData.value.valueCode = batch?.code || '';
formData.value.valueName = batch?.code || '';
}
/** 质量状态选择器变化:无实体编号,仅记录字典编码和文案 */
function handleQualityStatusChange(value: any) {
const selected = qualityStatusOptions.find((item) => item.value === value);
formData.value.valueId = undefined;
formData.value.valueCode = value;
formData.value.valueName = selected?.label || '';
}
/** 库区仓库选择回调:清空库区 */
function handleLocationWarehouseChange() {
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
}
/** 库位仓库选择回调:清空库区和库位 */
function handleAreaWarehouseChange() {
areaLocationId.value = undefined;
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
}
/** 库位库区选择回调:清空库位 */
function handleAreaLocationChange() {
formData.value.valueId = undefined;
formData.value.valueCode = '';
formData.value.valueName = '';
}
/** 编辑时回填级联选择器的上级数据(库区所属仓库、库位所属仓库/库区) */
async function loadCascadeData() {
if (!formData.value.type || !formData.value.valueId) {
return;
}
const valueId = formData.value.valueId;
if (formData.value.type === MesWmStockTakingParamTypeEnum.LOCATION) {
const location = await getWarehouseLocation(valueId);
locationWarehouseId.value = location?.warehouseId;
} else if (formData.value.type === MesWmStockTakingParamTypeEnum.AREA) {
const area = await getWarehouseArea(valueId);
areaWarehouseId.value = area?.warehouseId;
areaLocationId.value = area?.locationId;
}
}
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!formData.value.type) {
ElMessage.warning('请选择条件类型');
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const values =
(await formApi.getValues()) as MesWmStockTakingPlanParamApi.StockTakingPlanParam;
// valueCode valueId
const valid =
formData.value.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS
? !!formData.value.valueCode
: formData.value.valueId != null;
if (!valid) {
const valueValid =
values.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS
? !!values.valueCode
: values.valueId != null;
if (!valueValid) {
ElMessage.warning('请选择条件值');
return;
}
modalApi.lock();
//
const data = {
...formData.value,
...values,
id: formId.value,
planId: planId.value,
} as MesWmStockTakingPlanParamApi.StockTakingPlanParam;
try {
await (formData.value.id
await (formId.value
? updateStockTakingPlanParam(data)
: createStockTakingPlanParam(data));
//
@ -159,129 +78,31 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = {};
locationWarehouseId.value = undefined;
areaWarehouseId.value = undefined;
areaLocationId.value = undefined;
formId.value = undefined;
return;
}
formApi.setState({ schema: useParamFormSchema(formApi) });
//
const data = modalApi.getData<{ id?: number; planId: number }>();
planId.value = data.planId;
formId.value = data.id;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getStockTakingPlanParam(data.id);
await loadCascadeData();
const param = await getStockTakingPlanParam(data.id);
// values
await formApi.setValues(param);
} finally {
modalApi.unlock();
}
},
});
const ParamTypeEnum = MesWmStockTakingParamTypeEnum;
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<ElForm class="mx-4" label-width="100px">
<ElFormItem label="条件类型" required>
<ElSelect
v-model="formData.type"
class="!w-full"
placeholder="请选择条件类型"
@change="handleTypeChange"
>
<ElOption
v-for="dict in paramTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="formData.type" label="条件值" required>
<WmWarehouseSelect
v-if="formData.type === ParamTypeEnum.WAREHOUSE"
v-model="formData.valueId"
@change="handleSelectorChange"
/>
<div
v-else-if="formData.type === ParamTypeEnum.LOCATION"
class="w-full space-y-2"
>
<WmWarehouseSelect
v-model="locationWarehouseId"
placeholder="请选择仓库"
@change="handleLocationWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="locationWarehouseId"
v-model="formData.valueId"
placeholder="请选择库区"
:warehouse-id="locationWarehouseId"
@change="handleSelectorChange"
/>
</div>
<div
v-else-if="formData.type === ParamTypeEnum.AREA"
class="w-full space-y-2"
>
<WmWarehouseSelect
v-model="areaWarehouseId"
placeholder="请选择仓库"
@change="handleAreaWarehouseChange"
/>
<WmWarehouseLocationSelect
v-if="areaWarehouseId"
v-model="areaLocationId"
placeholder="请选择库区"
:warehouse-id="areaWarehouseId"
@change="handleAreaLocationChange"
/>
<WmWarehouseAreaSelect
v-if="areaLocationId"
v-model="formData.valueId"
:location-id="areaLocationId"
placeholder="请选择库位"
@change="handleSelectorChange"
/>
</div>
<MdItemSelect
v-else-if="formData.type === ParamTypeEnum.ITEM"
v-model="formData.valueId"
@change="handleSelectorChange"
/>
<WmBatchSelect
v-else-if="formData.type === ParamTypeEnum.BATCH"
v-model="formData.valueId"
@change="handleBatchChange"
/>
<ElSelect
v-else-if="formData.type === ParamTypeEnum.QUALITY_STATUS"
v-model="formData.valueCode"
class="!w-full"
placeholder="请选择质量状态"
@change="handleQualityStatusChange"
>
<ElOption
v-for="dict in qualityStatusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="备注">
<ElInput
v-model="formData.remark"
placeholder="请输入备注"
:rows="3"
type="textarea"
/>
</ElFormItem>
</ElForm>
<Form class="mx-4" />
</Modal>
</template>