新增原料入场记录的免密查询和删除功能,支持级联删除关联原材料卡片。重构相关服务和控制器以增强逻辑删除支持,优化数据处理流程,确保数据一致性和准确性。同时,更新前端表单以支持新功能。
This commit is contained in:
@@ -155,7 +155,8 @@ export const searchFormSchema: FormSchema[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
/** 新增:完整字段 */
|
||||
export const formSchemaAdd: FormSchema[] = [
|
||||
{
|
||||
label: '',
|
||||
field: 'id',
|
||||
@@ -288,6 +289,62 @@ export const formSchema: FormSchema[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 编辑/详情:仅展示条码、批次号、物料、剩余数量、剩余重量、库区;
|
||||
* 条码/批次号/物料只读,仅剩余数量、剩余重量、库区可编辑(详情时由表单全局禁用)。
|
||||
*/
|
||||
export const formSchemaEdit: FormSchema[] = [
|
||||
{
|
||||
label: '',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '条码',
|
||||
field: 'barcode',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '批次号',
|
||||
field: 'batchNo',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '物料名称',
|
||||
field: 'materialName',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '剩余数量',
|
||||
field: 'remainingQuantity',
|
||||
component: 'InputNumber',
|
||||
componentProps: { placeholder: '请输入剩余数量', min: 0, precision: 0 },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '剩余重量',
|
||||
field: 'remainingWeight',
|
||||
component: 'InputNumber',
|
||||
componentProps: { placeholder: '请输入剩余重量', min: 0, precision: 3 },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '库区',
|
||||
field: 'warehouseArea',
|
||||
component: 'Input',
|
||||
slot: 'warehouseAreaPickSlot',
|
||||
helpMessage: '编辑时点击输入框,在右侧列表中选择库区(保存库区编码)',
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
];
|
||||
|
||||
export const superQuerySchema = {
|
||||
barcode: { title: '条码', order: 0, view: 'text' },
|
||||
batchNo: { title: '批次号', order: 1, view: 'text' },
|
||||
|
||||
@@ -7,23 +7,15 @@
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_card:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_card:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_card:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-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="选择 PrintDot 地址"
|
||||
@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"
|
||||
@@ -33,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_card:edit'"
|
||||
:loading="printLoading"
|
||||
:disabled="selectedRowKeys.length === 0 || !printDotEnabled"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
@click="handlePrintSelected"
|
||||
>
|
||||
<Icon icon="ant-design:printer-outlined" />
|
||||
@@ -101,6 +86,8 @@
|
||||
import { BasicTable, useTable, 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 MesXslRawMaterialCardModal from './components/MesXslRawMaterialCardModal.vue';
|
||||
import RawMaterialCardPrintPreviewModal from './components/RawMaterialCardPrintPreviewModal.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslRawMaterialCard.data';
|
||||
@@ -126,19 +113,20 @@
|
||||
} from '/@/views/print/template/utils/printDotBridge';
|
||||
|
||||
const { 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);
|
||||
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() {
|
||||
@@ -210,9 +198,12 @@
|
||||
/** 与打印模板列表共用 localStorage 键,打印机选择保持一致 */
|
||||
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const selectedPrinterName = ref<string>('__system_default__');
|
||||
const manualPrinterName = ref('');
|
||||
const printLoading = ref(false);
|
||||
|
||||
/** 固定 key,便于关闭 loading(避免返回值在某些场景下未能 removeNotice) */
|
||||
const PRINT_ROW_LOADING_KEY = 'mesXslRawMaterialCard-print-row';
|
||||
const PRINT_BATCH_LOADING_KEY = 'mesXslRawMaterialCard-print-batch';
|
||||
|
||||
/** 与打印模板列表启用 PrintDot 时一致:仅本机桥接打印机 */
|
||||
const printerSelectPlaceholder = '选择打印机(PrintDot 桥接)';
|
||||
|
||||
@@ -228,18 +219,13 @@
|
||||
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) {
|
||||
@@ -249,12 +235,7 @@
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
|
||||
optionMap.set(selectedPrinterName.value, {
|
||||
label: `${selectedPrinterName.value}(手动)`,
|
||||
value: selectedPrinterName.value,
|
||||
});
|
||||
}
|
||||
printDotConnected.value = false;
|
||||
printerOptions.value = Array.from(optionMap.values());
|
||||
if (showMessage) {
|
||||
createMessage.warning(`PrintDot:${e instanceof Error ? e.message : String(e)}`);
|
||||
@@ -262,18 +243,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>;
|
||||
@@ -323,6 +292,7 @@
|
||||
localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY) ||
|
||||
'__system_default__',
|
||||
});
|
||||
/** 单行/批量外层会统一 toast,此处避免重复 success */
|
||||
if (!options?.silentSuccess) {
|
||||
createMessage.success('已通过 PrintDot 提交打印');
|
||||
}
|
||||
@@ -332,66 +302,94 @@
|
||||
}
|
||||
|
||||
function handlePrintSelected() {
|
||||
if (!printDotEnabled.value) {
|
||||
createMessage.warning('请先开启 PrintDot 桥接');
|
||||
return;
|
||||
}
|
||||
const rows = selectedRows.value || [];
|
||||
if (!rows.length) {
|
||||
createMessage.warning('请至少勾选一条记录后再点击「打印选中」');
|
||||
return;
|
||||
}
|
||||
printLoading.value = true;
|
||||
const hideLoadingMsg = createMessage.loading(`正在打印 ${rows.length} 条记录,请稍候…`, 0);
|
||||
createMessage.destroy(PRINT_BATCH_LOADING_KEY);
|
||||
createMessage.loading({
|
||||
content: `正在打印 ${rows.length} 条记录,请稍候…`,
|
||||
key: PRINT_BATCH_LOADING_KEY,
|
||||
duration: 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);
|
||||
try {
|
||||
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}` : ''}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
createMessage.destroy(PRINT_BATCH_LOADING_KEY);
|
||||
printLoading.value = false;
|
||||
}
|
||||
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 (!printDotEnabled.value) {
|
||||
createMessage.warning('请先开启 PrintDot 桥接');
|
||||
return;
|
||||
}
|
||||
async function handlePrintRow(record: Recordable) {
|
||||
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;
|
||||
});
|
||||
createMessage.destroy(PRINT_ROW_LOADING_KEY);
|
||||
createMessage.loading({
|
||||
content: '正在生成 PDF 并提交打印,版面复杂时可能需数十秒,请稍候…',
|
||||
key: PRINT_ROW_LOADING_KEY,
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await executePrint(record, { silentSuccess: true });
|
||||
createMessage.success('已通过 PrintDot 提交打印');
|
||||
} catch (e: unknown) {
|
||||
createMessage.error(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
createMessage.destroy(PRINT_ROW_LOADING_KEY);
|
||||
printLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY);
|
||||
if (saved) {
|
||||
selectedPrinterName.value = saved;
|
||||
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 && !printDotWsUrl.value.trim()) {
|
||||
printDotWsUrl.value = values[0];
|
||||
setPrintDotBridgeConfig(printDotWsUrl.value, '');
|
||||
}
|
||||
} catch {
|
||||
/* 字典未配置时沿用 localStorage */
|
||||
}
|
||||
refreshPrinterOptions(false);
|
||||
|
||||
const savedPrinter = localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY);
|
||||
if (savedPrinter) {
|
||||
selectedPrinterName.value = savedPrinter;
|
||||
}
|
||||
await refreshPrinterOptions(false);
|
||||
});
|
||||
|
||||
function handleSuperQuery(params) {
|
||||
|
||||
@@ -1,6 +1,66 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="1000" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" name="MesXslRawMaterialCardForm" />
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
wrap-class-name="mes-raw-card-edit-modal"
|
||||
@register="registerModal"
|
||||
destroyOnClose
|
||||
:title="title"
|
||||
:width="modalWidth"
|
||||
:bodyStyle="modalBodyStyle"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<div :class="['raw-card-modal__layout', layoutClass]">
|
||||
<div class="raw-card-modal__form">
|
||||
<BasicForm @register="registerForm" name="MesXslRawMaterialCardForm">
|
||||
<template #warehouseAreaPickSlot="{ model, field }">
|
||||
<a-input
|
||||
:value="warehouseAreaTriggerText(model[field])"
|
||||
readonly
|
||||
:disabled="!formEditable"
|
||||
placeholder="点击后在右侧选择库区"
|
||||
class="raw-card-modal__area-trigger"
|
||||
@click="onWarehouseAreaTriggerClick"
|
||||
>
|
||||
<template #suffix>
|
||||
<Icon icon="ant-design:unordered-list-outlined" />
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</div>
|
||||
<aside v-if="showSidePane" class="raw-card-modal__area-pane">
|
||||
<div class="raw-card-modal__area-pane-head">
|
||||
<span class="raw-card-modal__area-pane-title">库区列表</span>
|
||||
<a-button type="link" size="small" @click="areaPanelVisible = false">收起</a-button>
|
||||
</div>
|
||||
<a-input
|
||||
v-model:value="areaSearch"
|
||||
allow-clear
|
||||
placeholder="筛选仓库 / 库区名称 / 编码"
|
||||
class="raw-card-modal__area-search"
|
||||
/>
|
||||
<a-spin :spinning="areaLoading" class="raw-card-modal__area-spin">
|
||||
<div class="raw-card-modal__area-list">
|
||||
<div
|
||||
v-for="row in filteredAreaRows"
|
||||
:key="row.id || row.areaCode"
|
||||
:class="[
|
||||
'raw-card-modal__area-item',
|
||||
{ 'is-active': (row.areaCode || '').trim() === (pickedAreaCode || '').trim() },
|
||||
]"
|
||||
@click="onPickWarehouseArea(row)"
|
||||
>
|
||||
<div class="raw-card-modal__area-item-name">{{ row.areaName || row.areaCode || '—' }}</div>
|
||||
<div class="raw-card-modal__area-item-meta">
|
||||
<span v-if="row.warehouseName" class="raw-card-modal__area-item-meta-item">仓库:{{ row.warehouseName }}</span>
|
||||
<span class="raw-card-modal__area-item-meta-item">编码:{{ row.areaCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-if="!filteredAreaRows.length && !areaLoading" description="无启用库区" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</aside>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@@ -8,36 +68,169 @@
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from '../MesXslRawMaterialCard.data';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { formSchemaAdd, formSchemaEdit } from '../MesXslRawMaterialCard.data';
|
||||
import { saveOrUpdate } from '../MesXslRawMaterialCard.api';
|
||||
import { list as warehouseAreaList } from '/@/views/xslmes/mesXslWarehouseArea/MesXslWarehouseArea.api';
|
||||
|
||||
/** 库区行(列表接口 records) */
|
||||
interface WarehouseAreaRow {
|
||||
id?: string;
|
||||
areaCode: string;
|
||||
areaName?: string;
|
||||
/** 所属仓库名称(列表接口冗余字段) */
|
||||
warehouseName?: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const isUpdate = ref(true);
|
||||
/** 与标题逻辑一致:showFooter 为 true 表示可编辑(编辑),false 为详情 */
|
||||
const isDetail = ref(false);
|
||||
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
|
||||
const formEditable = ref(true);
|
||||
const areaPanelVisible = ref(false);
|
||||
const areaSearch = ref('');
|
||||
const areaLoading = ref(false);
|
||||
const areaRows = ref<WarehouseAreaRow[]>([]);
|
||||
const pickedAreaCode = ref<string>('');
|
||||
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField, resetSchema }] = useForm({
|
||||
labelWidth: 120,
|
||||
schemas: formSchema,
|
||||
schemas: formSchemaAdd,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 12 },
|
||||
});
|
||||
|
||||
const showSidePane = computed(() => unref(isUpdate) && unref(formEditable) && unref(areaPanelVisible));
|
||||
|
||||
const modalWidth = computed(() => {
|
||||
if (!unref(isUpdate)) {
|
||||
return 1000;
|
||||
}
|
||||
return unref(showSidePane) ? 980 : 640;
|
||||
});
|
||||
|
||||
const modalBodyStyle = computed(() =>
|
||||
unref(isUpdate) ? { paddingTop: '12px', paddingBottom: '8px' } : undefined
|
||||
);
|
||||
|
||||
const layoutClass = computed(() => ({
|
||||
'raw-card-modal__layout--split': unref(showSidePane),
|
||||
}));
|
||||
|
||||
const filteredAreaRows = computed(() => {
|
||||
const q = unref(areaSearch).trim().toLowerCase();
|
||||
const rows = unref(areaRows);
|
||||
if (!q) {
|
||||
return rows;
|
||||
}
|
||||
return rows.filter(
|
||||
(r) =>
|
||||
(r.areaCode || '').toLowerCase().includes(q) ||
|
||||
(r.areaName || '').toLowerCase().includes(q) ||
|
||||
(r.warehouseName || '').toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
function warehouseAreaTriggerText(code: string | undefined | null) {
|
||||
const c = (code || '').trim();
|
||||
if (!c) {
|
||||
return '';
|
||||
}
|
||||
const row = unref(areaRows).find((r) => (r.areaCode || '').trim() === c);
|
||||
if (!row) {
|
||||
return c;
|
||||
}
|
||||
const wh = (row.warehouseName || '').trim();
|
||||
const an = (row.areaName || '').trim();
|
||||
if (wh && an) {
|
||||
return `${wh} · ${an}(${row.areaCode})`;
|
||||
}
|
||||
if (an) {
|
||||
return `${an}(${row.areaCode})`;
|
||||
}
|
||||
return row.areaCode || c;
|
||||
}
|
||||
|
||||
let warehouseAreasLoadPromise: Promise<void> | null = null;
|
||||
|
||||
async function loadEnabledWarehouseAreas() {
|
||||
if (warehouseAreasLoadPromise) {
|
||||
return warehouseAreasLoadPromise;
|
||||
}
|
||||
warehouseAreasLoadPromise = (async () => {
|
||||
areaLoading.value = true;
|
||||
try {
|
||||
const page = await warehouseAreaList({
|
||||
pageNo: 1,
|
||||
pageSize: 5000,
|
||||
status: '0',
|
||||
});
|
||||
const records = (page?.records || []) as WarehouseAreaRow[];
|
||||
areaRows.value = records;
|
||||
} catch {
|
||||
areaRows.value = [];
|
||||
} finally {
|
||||
areaLoading.value = false;
|
||||
}
|
||||
})();
|
||||
try {
|
||||
await warehouseAreasLoadPromise;
|
||||
} finally {
|
||||
warehouseAreasLoadPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onWarehouseAreaTriggerClick() {
|
||||
if (!unref(formEditable)) {
|
||||
return;
|
||||
}
|
||||
areaPanelVisible.value = true;
|
||||
areaSearch.value = '';
|
||||
if (!unref(areaRows).length) {
|
||||
await loadEnabledWarehouseAreas();
|
||||
}
|
||||
}
|
||||
|
||||
function onPickWarehouseArea(row: WarehouseAreaRow) {
|
||||
if (!row?.areaCode || !unref(formEditable)) {
|
||||
return;
|
||||
}
|
||||
const code = String(row.areaCode).trim();
|
||||
pickedAreaCode.value = code;
|
||||
void setFieldsValue({ warehouseArea: code });
|
||||
}
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
areaPanelVisible.value = false;
|
||||
areaSearch.value = '';
|
||||
await resetFields();
|
||||
const isAdd = !data?.isUpdate;
|
||||
await resetSchema(isAdd ? formSchemaAdd : formSchemaEdit);
|
||||
await setProps({
|
||||
disabled: !data?.showFooter,
|
||||
baseColProps: { span: isAdd ? 12 : 24 },
|
||||
});
|
||||
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
isDetail.value = !!data?.showFooter;
|
||||
formEditable.value = !!data?.showFooter;
|
||||
if (unref(isUpdate)) {
|
||||
// 先写入行数据,避免等待库区列表接口导致整表单长时间空白
|
||||
pickedAreaCode.value = String(data.record?.warehouseArea || '').trim();
|
||||
await setFieldsValue({ ...data.record });
|
||||
void loadEnabledWarehouseAreas();
|
||||
} else {
|
||||
areaRows.value = [];
|
||||
pickedAreaCode.value = '';
|
||||
}
|
||||
setProps({ disabled: !data?.showFooter });
|
||||
});
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
|
||||
|
||||
async function handleSubmit(v) {
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
let values = await validate();
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdate(values, isUpdate.value);
|
||||
closeModal();
|
||||
@@ -57,6 +250,158 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.raw-card-modal__layout {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
min-height: 200px;
|
||||
|
||||
&--split {
|
||||
.raw-card-modal__form {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.raw-card-modal__form {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-pane {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 固定可视高度:仅列表区域滚动,避免整块侧栏把 Modal body 撑出滚动条 */
|
||||
height: min(440px, calc(100vh - 240px));
|
||||
max-height: calc(100vh - 240px);
|
||||
min-height: 240px;
|
||||
padding: 0 4px 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-pane-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 4px 6px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-pane-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-search {
|
||||
flex-shrink: 0;
|
||||
margin: 10px 0 8px;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-spin {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.ant-spin-nested-loading) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-spin-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-spin) {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.raw-card-modal__area-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
/* 明确上限,避免父级未约束高度时无法触发 overflow */
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-item {
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
background: rgba(22, 119, 255, 0.06);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: #1677ff;
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.raw-card-modal__area-item-name {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-item-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-item-meta-item {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-item-meta-item:first-of-type:last-of-type {
|
||||
/* 仅编码、无仓库名时占满一行 */
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.raw-card-modal__area-trigger {
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -64,3 +409,25 @@
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 挂载在 body 的 Modal 无法被 scoped 穿透;单独取消本弹窗外层 ScrollContainer 纵向滚动,由右侧库区列表自滚动 -->
|
||||
<style lang="less">
|
||||
.mes-raw-card-edit-modal {
|
||||
.ant-modal-body {
|
||||
.scroll-container {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.scroll-container .scrollbar__wrap {
|
||||
max-height: none !important;
|
||||
overflow-y: visible !important;
|
||||
}
|
||||
|
||||
/* ModalWrapper 内层包裹 slot 的容器,内联了 maxHeight */
|
||||
.scrollbar__view > div {
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user