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

564 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>