Files
qhmes/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntryList.vue

564 lines
20 KiB
Vue
Raw Normal View History

<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>