新增原料入场记录的免密查询和删除功能,支持级联删除关联原材料卡片。重构相关服务和控制器以增强逻辑删除支持,优化数据处理流程,确保数据一致性和准确性。同时,更新前端表单以支持新功能。

This commit is contained in:
geht
2026-05-15 15:08:07 +08:00
parent b4b550a3de
commit 0c5e29044a
34 changed files with 1693 additions and 215 deletions

View File

@@ -1,14 +1,11 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
enum Api {
list = '/xslmes/mesXslRawMaterialEntry/list',
enum Api { list = '/xslmes/mesXslRawMaterialEntry/list',
save = '/xslmes/mesXslRawMaterialEntry/add',
edit = '/xslmes/mesXslRawMaterialEntry/edit',
deleteOne = '/xslmes/mesXslRawMaterialEntry/delete',
deleteBatch = '/xslmes/mesXslRawMaterialEntry/deleteBatch',
linkedRawMaterialCards = '/xslmes/mesXslRawMaterialEntry/linkedRawMaterialCards',
batchStockIn = '/xslmes/mesXslRawMaterialEntry/batchStockIn',
importExcel = '/xslmes/mesXslRawMaterialEntry/importExcel',
exportXls = '/xslmes/mesXslRawMaterialEntry/exportXls',
@@ -20,26 +17,29 @@ enum Api {
export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;
/** 删除前:查询入场记录关联的已生成原材料卡片 */
export const getLinkedRawMaterialCards = (ids: string) =>
defHttp.get<MesXslRawMaterialCardBrief[]>({ url: Api.linkedRawMaterialCards, params: { ids } });
export interface MesXslRawMaterialCardBrief {
id?: string;
barcode?: string;
batchNo?: string;
materialName?: string;
splitDetailId?: string;
}
export const list = (params) => defHttp.get({ url: Api.list, params });
export const deleteOne = (params, handleSuccess) => {
export const deleteOne = (params: { id: string; cascadeDeleteCards?: boolean }, handleSuccess?: () => void) => {
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
handleSuccess?.();
});
};
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
export const batchDelete = (params: { ids: string; cascadeDeleteCards?: boolean }, handleSuccess?: () => void) => {
return defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess?.();
});
};

View File

@@ -6,23 +6,16 @@
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:stockIn'" preIcon="ant-design:check-circle-outlined" @click="handleStockIn"> 结存入库</a-button>
<a-checkbox v-model:checked="printDotEnabled" style="margin-left: 8px" @change="onPrintDotEnabledChange">
PrintDot 桥接
</a-checkbox>
<a-input
<!-- 与原材料卡片一致字典选桥接地址 + 打印机 + 刷新 -->
<JDictSelectTag
v-model:value="printDotWsUrl"
style="width: 220px; margin-left: 8px"
placeholder="ws://127.0.0.1:1122/ws"
@blur="persistPrintDotConfig"
dictCode="xslmes_print_dot_ws"
:showChooseOption="false"
style="width: 280px; margin-left: 8px"
placeholder="选择打印桥接"
@change="onPrintDotWsUrlChange"
/>
<a-input-password
v-model:value="printDotKey"
style="width: 130px; margin-left: 8px"
placeholder="密钥(可选)"
autocomplete="new-password"
@blur="persistPrintDotConfig"
/>
<a-button @click="downloadPrintPlugin">下载打印插件</a-button>
<a-button v-if="!printDotConnected" style="margin-left: 8px" @click="downloadPrintPlugin">下载打印插件</a-button>
<a-select
v-model:value="selectedPrinterName"
:options="printerOptions"
@@ -32,20 +25,13 @@
option-filter-prop="label"
:placeholder="printerSelectPlaceholder"
/>
<a-button @click="() => refreshPrinterOptions(true)">刷新打印机</a-button>
<a-input
v-model:value="manualPrinterName"
style="width: 150px; margin-left: 8px"
placeholder="手动输入打印机名"
@press-enter="addManualPrinter"
/>
<a-button @click="addManualPrinter">添加</a-button>
<a-button style="margin-left: 8px" @click="() => refreshPrinterOptions(true)">刷新打印机</a-button>
<a-button
type="primary"
ghost
v-auth="'xslmes:mes_xsl_raw_material_entry:edit'"
:loading="printLoading"
:disabled="selectedRowKeys.length === 0 || !printDotEnabled"
:disabled="selectedRowKeys.length === 0"
@click="handlePrintSelected"
>
<Icon icon="ant-design:printer-outlined" />
@@ -80,11 +66,13 @@
</template>
<script lang="ts" name="xslmes-mesXslRawMaterialEntry" setup>
import { onMounted, ref, reactive, watch } from 'vue';
import { onMounted, ref, reactive, watch, h } from 'vue';
import { Icon } from '/@/components/Icon';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { initDictOptions } from '/@/utils/dict';
import { JDictSelectTag } from '/@/components/Form';
import MesXslRawMaterialEntryModal from './components/MesXslRawMaterialEntryModal.vue';
import RawMaterialEntryPrintPreviewModal from './components/RawMaterialEntryPrintPreviewModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './MesXslRawMaterialEntry.data';
@@ -95,7 +83,9 @@
batchStockIn,
getImportUrl,
getExportUrl,
getLinkedRawMaterialCards,
prepareNativePrint,
type MesXslRawMaterialCardBrief,
} from './MesXslRawMaterialEntry.api';
import { useMessage } from '/@/hooks/web/useMessage';
import {
@@ -110,19 +100,21 @@
} from '/@/views/print/template/utils/printDotBridge';
const { createConfirm, createMessage } = useMessage();
const LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled';
const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) !== '0');
const printDotCfg = getPrintDotBridgeConfig();
const printDotWsUrl = ref(printDotCfg.wsUrl);
const printDotKey = ref(printDotCfg.key);
/** 与 Flyway 中 sys_dict.dict_code 一致(与原材料卡片列表相同) */
const PRINT_DOT_WS_DICT = 'xslmes_print_dot_ws';
const printDotWsUrl = ref('');
/** 是否已成功连接桥并拿到响应(用于显示「下载打印插件」) */
const printDotConnected = ref(false);
function persistPrintDotConfig() {
setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value);
// 与原材料卡片一致:桥接密钥固定为空,由本地 PrintDot 与字典地址对接
setPrintDotBridgeConfig(String(printDotWsUrl.value || '').trim(), '');
void refreshPrinterOptions(false);
}
function onPrintDotEnabledChange() {
localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0');
function onPrintDotWsUrlChange() {
printDotConnected.value = false;
persistPrintDotConfig();
}
function downloadPrintPlugin() {
@@ -193,7 +185,6 @@
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
const selectedPrinterName = ref<string>('__system_default__');
const manualPrinterName = ref('');
const printLoading = ref(false);
const printerSelectPlaceholder = '选择打印机PrintDot 桥接)';
@@ -208,6 +199,7 @@
optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' });
try {
const dotList = await fetchPrintDotPrinters();
printDotConnected.value = true;
dotList.forEach((p) => {
const name = String(p.name || '').trim();
if (!name) return;
@@ -229,6 +221,7 @@
}
}
} catch (e: unknown) {
printDotConnected.value = false;
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
optionMap.set(selectedPrinterName.value, {
label: `${selectedPrinterName.value}(手动)`,
@@ -242,18 +235,6 @@
}
}
function addManualPrinter() {
const name = String(manualPrinterName.value || '').trim();
if (!name) return;
const exists = printerOptions.value.some((item) => item.value === name);
if (!exists) {
printerOptions.value = [...printerOptions.value, { label: `${name}(手动)`, value: name }];
}
selectedPrinterName.value = name;
manualPrinterName.value = '';
createMessage.success('已添加打印机');
}
async function executePrint(record: Recordable, options?: { silentSuccess?: boolean }) {
try {
const prep = (await prepareNativePrint(record.id as string)) as Record<string, unknown>;
@@ -310,8 +291,8 @@
}
function handlePrintSelected() {
if (!printDotEnabled.value) {
createMessage.warning('请先开启 PrintDot 桥接');
if (!String(printDotWsUrl.value || '').trim()) {
createMessage.warning('请先选择打印桥接(PrintDot 地址)');
return;
}
const rows = selectedRows.value || [];
@@ -345,8 +326,8 @@
}
function handlePrintRow(record: Recordable) {
if (!printDotEnabled.value) {
createMessage.warning('请先开启 PrintDot 桥接');
if (!String(printDotWsUrl.value || '').trim()) {
createMessage.warning('请先选择打印桥接(PrintDot 地址)');
return;
}
printLoading.value = true;
@@ -364,12 +345,35 @@
});
}
onMounted(() => {
onMounted(async () => {
const cfg = getPrintDotBridgeConfig();
// 密钥不使用,与原材料卡片列表一致
setPrintDotBridgeConfig(cfg.wsUrl || '', '');
printDotWsUrl.value = cfg.wsUrl || '';
try {
const raw = await initDictOptions(PRINT_DOT_WS_DICT);
const items = Array.isArray(raw) ? raw : [];
const values = items
.map((it: Recordable) => String(it.value ?? it.itemValue ?? '').trim())
.filter(Boolean);
const valueSet = new Set(values);
if (valueSet.size && printDotWsUrl.value && !valueSet.has(String(printDotWsUrl.value).trim())) {
printDotWsUrl.value = values[0];
setPrintDotBridgeConfig(printDotWsUrl.value, '');
} else if (valueSet.size && !String(printDotWsUrl.value || '').trim()) {
printDotWsUrl.value = values[0];
setPrintDotBridgeConfig(printDotWsUrl.value, '');
}
} catch {
/* 字典未配置时沿用 localStorage */
}
const saved = localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY);
if (saved) {
selectedPrinterName.value = saved;
}
refreshPrinterOptions(false);
await refreshPrinterOptions(false);
});
function handleSuperQuery(params) {
@@ -391,12 +395,107 @@
openModal(true, { record, isUpdate: true, showFooter: false });
}
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
/** 删除确认框:关联卡片以列表形式展示 */
function renderLinkedCardsConfirmContent(linked: MesXslRawMaterialCardBrief[], summaryText: string) {
const labelStyle = { color: 'rgba(0,0,0,0.45)', display: 'inline-block', minWidth: '40px' as const };
return h('div', { class: 'linked-raw-material-cards-confirm' }, [
h('p', { style: { margin: '0 0 12px', color: 'rgba(0,0,0,0.88)' } }, summaryText),
h(
'ul',
{
style: {
listStyle: 'none',
padding: 0,
margin: 0,
maxHeight: '360px',
overflowY: 'auto' as const,
border: '1px solid #f0f0f0',
borderRadius: '6px',
background: '#fafafa',
},
},
linked.map((c, idx) =>
h(
'li',
{
key: c.id ?? `${c.barcode ?? ''}-${c.batchNo ?? ''}-${idx}`,
style: {
padding: '10px 12px',
borderBottom: idx < linked.length - 1 ? '1px solid #f0f0f0' : 'none',
background: '#fff',
},
},
[
h('div', { style: { fontSize: '13px', lineHeight: '22px', color: 'rgba(0,0,0,0.88)', wordBreak: 'break-all' as const } }, [
h('div', null, [h('span', { style: labelStyle }, '条码'), '', c.barcode || '-']),
h('div', null, [h('span', { style: labelStyle }, '批次'), '', c.batchNo || '-']),
h('div', null, [h('span', { style: labelStyle }, '物料'), '', c.materialName || '-']),
]),
],
),
),
),
]);
}
async function handleDelete(record: Recordable) {
try {
const linked = await getLinkedRawMaterialCards(record.id as string);
if (linked && linked.length > 0) {
createConfirm({
iconType: 'warning',
title: '已生成原材料卡片',
width: 600,
content: renderLinkedCardsConfirmContent(linked, `以下 ${linked.length} 张卡片将随入场记录一并删除:`),
okText: '仍要删除',
cancelText: '取消',
onOk: async () => {
await deleteOne({ id: record.id as string, cascadeDeleteCards: true }, handleSuccess);
},
});
} else {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否确认删除该条原料入场记录?',
okText: '确认',
cancelText: '取消',
onOk: async () => {
await deleteOne({ id: record.id as string, cascadeDeleteCards: false }, handleSuccess);
},
});
}
} catch (e: unknown) {
createMessage.error(e instanceof Error ? e.message : String(e));
}
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
const ids = (selectedRowKeys.value || []).join(',');
if (!ids) {
createMessage.warning('请先勾选要删除的记录');
return;
}
try {
const linked = await getLinkedRawMaterialCards(ids);
const cardList: MesXslRawMaterialCardBrief[] = linked && linked.length > 0 ? linked : [];
const hasCards = cardList.length > 0;
createConfirm({
iconType: 'warning',
title: hasCards ? '已生成原材料卡片' : '确认删除',
width: hasCards ? 600 : 416,
content: hasCards
? renderLinkedCardsConfirmContent(cardList, `以下 ${cardList.length} 张卡片将随选中入场记录一并删除:`)
: `是否删除选中的 ${selectedRowKeys.value.length} 条数据?`,
okText: '确认删除',
cancelText: '取消',
onOk: async () => {
await batchDelete({ ids, cascadeDeleteCards: !!hasCards }, handleSuccess);
},
});
} catch (e: unknown) {
createMessage.error(e instanceof Error ? e.message : String(e));
}
}
function handleStockIn() {
@@ -450,11 +549,7 @@
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
onClick: handleDelete.bind(null, record),
auth: 'xslmes:mes_xsl_raw_material_entry:delete',
},
];