564 lines
20 KiB
Vue
564 lines
20 KiB
Vue
<template>
|
||
<div>
|
||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||
<template #tableTitle>
|
||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||
<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>
|
||
<!-- 与原材料卡片一致:字典选桥接地址 + 打印机 + 刷新 -->
|
||
<JDictSelectTag
|
||
v-model:value="printDotWsUrl"
|
||
dictCode="xslmes_print_dot_ws"
|
||
:showChooseOption="false"
|
||
style="width: 280px; margin-left: 8px"
|
||
placeholder="选择打印桥接"
|
||
@change="onPrintDotWsUrlChange"
|
||
/>
|
||
<a-button v-if="!printDotConnected" style="margin-left: 8px" @click="downloadPrintPlugin">下载打印插件</a-button>
|
||
<a-select
|
||
v-model:value="selectedPrinterName"
|
||
:options="printerOptions"
|
||
style="width: 220px; margin-left: 8px"
|
||
allow-clear
|
||
show-search
|
||
option-filter-prop="label"
|
||
:placeholder="printerSelectPlaceholder"
|
||
/>
|
||
<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"
|
||
@click="handlePrintSelected"
|
||
>
|
||
<Icon icon="ant-design:printer-outlined" />
|
||
打印选中
|
||
</a-button>
|
||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="1" @click="batchHandleDelete">
|
||
<Icon icon="ant-design:delete-outlined" />
|
||
删除
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
<a-button v-auth="'xslmes:mes_xsl_raw_material_entry:deleteBatch'">批量操作
|
||
<Icon icon="mdi:chevron-down" />
|
||
</a-button>
|
||
</a-dropdown>
|
||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||
</template>
|
||
<template #action="{ record }">
|
||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||
</template>
|
||
</BasicTable>
|
||
<MesXslRawMaterialEntryModal @register="registerModal" @success="handleSuccess" />
|
||
<RawMaterialEntryPrintPreviewModal
|
||
v-model:open="printPreviewOpen"
|
||
:entry-id="printPreviewEntryId"
|
||
:barcode="printPreviewBarcode"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" name="xslmes-mesXslRawMaterialEntry" setup>
|
||
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';
|
||
import {
|
||
list,
|
||
deleteOne,
|
||
batchDelete,
|
||
batchStockIn,
|
||
getImportUrl,
|
||
getExportUrl,
|
||
getLinkedRawMaterialCards,
|
||
prepareNativePrint,
|
||
type MesXslRawMaterialCardBrief,
|
||
} from './MesXslRawMaterialEntry.api';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
import {
|
||
PRINT_TEMPLATE_SELECTED_PRINTER_KEY,
|
||
printNativeSchemaViaPrintDot,
|
||
} from '/@/views/print/template/utils/printNativeViaPrintDot';
|
||
import { normalizeImportedNativeSchema } from '/@/views/print/template/native/core/nativeSchemaNormalize';
|
||
import {
|
||
fetchPrintDotPrinters,
|
||
getPrintDotBridgeConfig,
|
||
setPrintDotBridgeConfig,
|
||
} from '/@/views/print/template/utils/printDotBridge';
|
||
|
||
const { createConfirm, createMessage } = useMessage();
|
||
/** 与 Flyway 中 sys_dict.dict_code 一致(与原材料卡片列表相同) */
|
||
const PRINT_DOT_WS_DICT = 'xslmes_print_dot_ws';
|
||
const printDotWsUrl = ref('');
|
||
/** 是否已成功连接桥并拿到响应(用于显示「下载打印插件」) */
|
||
const printDotConnected = ref(false);
|
||
|
||
function persistPrintDotConfig() {
|
||
// 与原材料卡片一致:桥接密钥固定为空,由本地 PrintDot 与字典地址对接
|
||
setPrintDotBridgeConfig(String(printDotWsUrl.value || '').trim(), '');
|
||
void refreshPrinterOptions(false);
|
||
}
|
||
|
||
function onPrintDotWsUrlChange() {
|
||
printDotConnected.value = false;
|
||
persistPrintDotConfig();
|
||
}
|
||
|
||
function downloadPrintPlugin() {
|
||
const base = import.meta.env.BASE_URL || '/';
|
||
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
|
||
const url = `${normalizedBase}print-plugin/XSL-PrintDot.exe`;
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.setAttribute('download', 'XSL-PrintDot.exe');
|
||
link.rel = 'noopener';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}
|
||
|
||
const queryParam = reactive<any>({});
|
||
const [registerModal, { openModal }] = useModal();
|
||
|
||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||
tableProps: {
|
||
title: '原料入场记录',
|
||
api: list,
|
||
columns,
|
||
canResize: true,
|
||
formConfig: {
|
||
schemas: searchFormSchema,
|
||
autoSubmitOnEnter: true,
|
||
showAdvancedButton: true,
|
||
fieldMapToTime: [
|
||
['entryTime', ['entryTime_begin', 'entryTime_end'], 'YYYY-MM-DD HH:mm:ss'],
|
||
],
|
||
},
|
||
actionColumn: {
|
||
width: 320,
|
||
fixed: 'right',
|
||
title: '操作',
|
||
dataIndex: 'action',
|
||
slots: { customRender: 'action' },
|
||
},
|
||
beforeFetch: (params) => {
|
||
return Object.assign(params, queryParam);
|
||
},
|
||
},
|
||
exportConfig: {
|
||
name: '原料入场记录',
|
||
url: getExportUrl,
|
||
params: queryParam,
|
||
},
|
||
importConfig: {
|
||
url: getImportUrl,
|
||
success: handleSuccess,
|
||
},
|
||
});
|
||
|
||
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
|
||
const superQueryConfig = reactive(superQuerySchema);
|
||
|
||
/** 打印预览弹窗 */
|
||
const printPreviewOpen = ref(false);
|
||
const printPreviewEntryId = ref<string | null>(null);
|
||
const printPreviewBarcode = ref<string | undefined>(undefined);
|
||
|
||
function handlePrintPreview(record: Recordable) {
|
||
printPreviewEntryId.value = record.id as string;
|
||
printPreviewBarcode.value = record.barcode as string | undefined;
|
||
printPreviewOpen.value = true;
|
||
}
|
||
|
||
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||
const selectedPrinterName = ref<string>('__system_default__');
|
||
const printLoading = ref(false);
|
||
const printerSelectPlaceholder = '选择打印机(PrintDot 桥接)';
|
||
|
||
watch(selectedPrinterName, (v) => {
|
||
if (v) {
|
||
localStorage.setItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY, v);
|
||
}
|
||
});
|
||
|
||
async function refreshPrinterOptions(showMessage = true) {
|
||
const optionMap = new Map<string, { label: string; value: string }>();
|
||
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;
|
||
const defMark = p.isDefault ? '(默认)' : '';
|
||
optionMap.set(name, { label: `${name}${defMark}`, value: name });
|
||
});
|
||
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
|
||
optionMap.set(selectedPrinterName.value, {
|
||
label: `${selectedPrinterName.value}(手动)`,
|
||
value: selectedPrinterName.value,
|
||
});
|
||
}
|
||
printerOptions.value = Array.from(optionMap.values());
|
||
if (showMessage) {
|
||
if (dotList.length) {
|
||
createMessage.success(`已从 PrintDot 桥接识别 ${dotList.length} 台打印机`);
|
||
} else {
|
||
createMessage.warning('PrintDot 已连接但未返回打印机列表');
|
||
}
|
||
}
|
||
} catch (e: unknown) {
|
||
printDotConnected.value = false;
|
||
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
|
||
optionMap.set(selectedPrinterName.value, {
|
||
label: `${selectedPrinterName.value}(手动)`,
|
||
value: selectedPrinterName.value,
|
||
});
|
||
}
|
||
printerOptions.value = Array.from(optionMap.values());
|
||
if (showMessage) {
|
||
createMessage.warning(`PrintDot:${e instanceof Error ? e.message : String(e)}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function executePrint(record: Recordable, options?: { silentSuccess?: boolean }) {
|
||
try {
|
||
const prep = (await prepareNativePrint(record.id as string)) as Record<string, unknown>;
|
||
const templateJsonRaw = prep.templateJson as string;
|
||
const printData = prep.printData as Record<string, unknown>;
|
||
const paperWidthMm = Number((prep as any).paperWidthMm ?? 0);
|
||
const paperHeightMm = Number((prep as any).paperHeightMm ?? 0);
|
||
const paperOrientation = String((prep as any).paperOrientation || '').toLowerCase();
|
||
if (!templateJsonRaw) {
|
||
throw new Error('模板 JSON 为空');
|
||
}
|
||
let raw: unknown;
|
||
try {
|
||
raw = typeof templateJsonRaw === 'string' ? JSON.parse(templateJsonRaw) : templateJsonRaw;
|
||
} catch {
|
||
throw new Error('模板 JSON 格式错误');
|
||
}
|
||
const schema = normalizeImportedNativeSchema(raw);
|
||
if (paperWidthMm > 0 && paperHeightMm > 0) {
|
||
const orient = paperOrientation === 'landscape' ? 'landscape' : paperOrientation === 'portrait' ? 'portrait' : '';
|
||
const normalized =
|
||
orient === 'landscape'
|
||
? {
|
||
width: Math.max(paperWidthMm, paperHeightMm),
|
||
height: Math.min(paperWidthMm, paperHeightMm),
|
||
}
|
||
: orient === 'portrait'
|
||
? {
|
||
width: Math.min(paperWidthMm, paperHeightMm),
|
||
height: Math.max(paperWidthMm, paperHeightMm),
|
||
}
|
||
: {
|
||
width: paperWidthMm,
|
||
height: paperHeightMm,
|
||
};
|
||
schema.page.width = normalized.width;
|
||
schema.page.height = normalized.height;
|
||
}
|
||
await printNativeSchemaViaPrintDot({
|
||
schema,
|
||
data: printData as Record<string, unknown>,
|
||
jobName: `原料入场记录-${(record.barcode as string) || record.id}.pdf`,
|
||
printerSelection:
|
||
selectedPrinterName.value ||
|
||
localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY) ||
|
||
'__system_default__',
|
||
});
|
||
if (!options?.silentSuccess) {
|
||
createMessage.success('已通过 PrintDot 提交打印');
|
||
}
|
||
} catch (e: unknown) {
|
||
throw new Error(e instanceof Error ? e.message : String(e));
|
||
}
|
||
}
|
||
|
||
function handlePrintSelected() {
|
||
if (!String(printDotWsUrl.value || '').trim()) {
|
||
createMessage.warning('请先选择打印桥接(PrintDot 地址)');
|
||
return;
|
||
}
|
||
const rows = selectedRows.value || [];
|
||
if (!rows.length) {
|
||
createMessage.warning('请至少勾选一条记录后再点击「打印选中」');
|
||
return;
|
||
}
|
||
printLoading.value = true;
|
||
const hideLoadingMsg = createMessage.loading(`正在打印 ${rows.length} 条记录,请稍候…`, 0);
|
||
(async () => {
|
||
let ok = 0;
|
||
let firstError = '';
|
||
for (const row of rows) {
|
||
try {
|
||
await executePrint(row, { silentSuccess: true });
|
||
ok += 1;
|
||
} catch (e: unknown) {
|
||
if (!firstError) {
|
||
firstError = e instanceof Error ? e.message : String(e);
|
||
}
|
||
}
|
||
}
|
||
if (ok === rows.length) {
|
||
createMessage.success(`已通过 PrintDot 提交 ${ok} 条打印任务`);
|
||
} else {
|
||
createMessage.warning(`打印完成:成功 ${ok},失败 ${rows.length - ok}${firstError ? `。首条错误:${firstError}` : ''}`);
|
||
}
|
||
hideLoadingMsg();
|
||
printLoading.value = false;
|
||
})();
|
||
}
|
||
|
||
function handlePrintRow(record: Recordable) {
|
||
if (!String(printDotWsUrl.value || '').trim()) {
|
||
createMessage.warning('请先选择打印桥接(PrintDot 地址)');
|
||
return;
|
||
}
|
||
printLoading.value = true;
|
||
const hideLoadingMsg = createMessage.loading('正在生成 PDF 并提交打印,版面复杂时可能需数十秒,请稍候…', 0);
|
||
executePrint(record)
|
||
.then(() => {
|
||
createMessage.success('已通过 PrintDot 提交打印');
|
||
})
|
||
.catch((e: unknown) => {
|
||
createMessage.error(e instanceof Error ? e.message : String(e));
|
||
})
|
||
.finally(() => {
|
||
hideLoadingMsg();
|
||
printLoading.value = false;
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
await refreshPrinterOptions(false);
|
||
});
|
||
|
||
function handleSuperQuery(params) {
|
||
Object.keys(params).map((k) => {
|
||
queryParam[k] = params[k];
|
||
});
|
||
reload();
|
||
}
|
||
|
||
function handleAdd() {
|
||
openModal(true, { isUpdate: false, showFooter: true });
|
||
}
|
||
|
||
function handleEdit(record: Recordable) {
|
||
openModal(true, { record, isUpdate: true, showFooter: true });
|
||
}
|
||
|
||
function handleDetail(record: Recordable) {
|
||
openModal(true, { record, isUpdate: true, showFooter: false });
|
||
}
|
||
|
||
/** 删除确认框:关联卡片以列表形式展示 */
|
||
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() {
|
||
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() {
|
||
if (!selectedRowKeys.value.length) {
|
||
createMessage.warning('请先勾选要结存入库的记录');
|
||
return;
|
||
}
|
||
createConfirm({
|
||
iconType: 'warning',
|
||
title: '结存入库',
|
||
content: `是否将选中的 ${selectedRowKeys.value.length} 条记录结存入库`,
|
||
okText: '确认',
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
await batchStockIn(selectedRowKeys.value.join(','));
|
||
createMessage.success('结存入库成功!');
|
||
handleSuccess();
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleSuccess() {
|
||
(selectedRowKeys.value = []) && reload();
|
||
}
|
||
|
||
function getTableAction(record) {
|
||
return [
|
||
{
|
||
label: '编辑',
|
||
onClick: handleEdit.bind(null, record),
|
||
auth: 'xslmes:mes_xsl_raw_material_entry:edit',
|
||
},
|
||
{
|
||
label: '打印预览',
|
||
onClick: handlePrintPreview.bind(null, record),
|
||
auth: 'xslmes:mes_xsl_raw_material_entry:edit',
|
||
},
|
||
{
|
||
label: '打印',
|
||
onClick: handlePrintRow.bind(null, record),
|
||
auth: 'xslmes:mes_xsl_raw_material_entry:edit',
|
||
},
|
||
];
|
||
}
|
||
|
||
function getDropDownAction(record) {
|
||
return [
|
||
{
|
||
label: '详情',
|
||
onClick: handleDetail.bind(null, record),
|
||
},
|
||
{
|
||
label: '删除',
|
||
onClick: handleDelete.bind(null, record),
|
||
auth: 'xslmes:mes_xsl_raw_material_entry:delete',
|
||
},
|
||
];
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.ant-picker-range) {
|
||
width: 100%;
|
||
}
|
||
</style>
|