修复产品需规保存与表格布局问题
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(join(__dirname, "ProductSpecLiteView.vue"), "utf8");
|
||||
|
||||
describe("ProductSpecLiteView operation foundation loading", () => {
|
||||
it("loads process and work-center master data sequentially so backend default seeding can settle", () => {
|
||||
assert.match(source, /const processRows = await fetchResource\("\/master-data\/processes", \[\]\);/);
|
||||
assert.match(source, /const centerRows = await fetchResource\("\/master-data\/work-centers", \[\]\);/);
|
||||
assert.ok(
|
||||
source.indexOf('fetchResource("/master-data/processes", [])') <
|
||||
source.indexOf('fetchResource("/master-data/work-centers", [])')
|
||||
);
|
||||
});
|
||||
|
||||
it("prepares route operations before creating a new product so validation cannot leave partial records", () => {
|
||||
assert.match(source, /async function ensureOperationFoundationReady\(\)/);
|
||||
assert.match(source, /await ensureOperationFoundationReady\(\);/);
|
||||
assert.match(source, /const preparedRouteOperations = buildRouteOperationsPayload\(processRowsForSave\);/);
|
||||
assert.ok(
|
||||
source.indexOf("const preparedRouteOperations = buildRouteOperationsPayload(processRowsForSave);") <
|
||||
source.indexOf('await postResource("/master-data/products", productPayload)')
|
||||
);
|
||||
});
|
||||
|
||||
it("cleans newly created product-spec records if a later save step fails", () => {
|
||||
assert.match(source, /let createdProductItemId = null;/);
|
||||
assert.match(source, /const createdMiniappPayloads = \[\];/);
|
||||
assert.match(source, /createdProductItemId = productItemId;/);
|
||||
assert.match(source, /createdMiniappPayloads\.push\(payload\);/);
|
||||
assert.match(source, /await deleteResource\(buildMiniappDeletePath\(payload\)\)\.catch\(\(\) => null\);/);
|
||||
assert.match(source, /await deleteResource\(`\/master-data\/product-specs\/\$\{createdProductItemId\}`\)\.catch\(\(\) => null\);/);
|
||||
});
|
||||
|
||||
it("uses a one-screen table layout so the action and pagination areas do not get pushed into the edge", () => {
|
||||
assert.match(source, /class="table-wrap product-spec-table-wrap"/);
|
||||
assert.match(source, /class="data-table compact-table product-spec-table"/);
|
||||
assert.match(source, /<colgroup>[\s\S]*product-spec-col-action[\s\S]*<\/colgroup>/);
|
||||
assert.match(source, /\.product-spec-table-wrap\.table-view-wide,[\s\S]*?overflow-x:\s*hidden !important;/);
|
||||
assert.match(source, /\.product-spec-table[\s\S]*?table-layout:\s*fixed !important;/);
|
||||
assert.match(source, /\.product-spec-table\s+\.action-row[\s\S]*?width:\s*100% !important;/);
|
||||
});
|
||||
});
|
||||
@@ -23,8 +23,25 @@
|
||||
placeholder="搜索考勤点、产品编码、名称、版本、原材料、小程序工序"
|
||||
/>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="data-table compact-table">
|
||||
<div class="table-wrap product-spec-table-wrap">
|
||||
<table class="data-table compact-table product-spec-table">
|
||||
<colgroup>
|
||||
<col class="product-spec-col-code" />
|
||||
<col class="product-spec-col-attendance" />
|
||||
<col class="product-spec-col-project" />
|
||||
<col class="product-spec-col-product" />
|
||||
<col class="product-spec-col-version" />
|
||||
<col class="product-spec-col-weight" />
|
||||
<col class="product-spec-col-material" />
|
||||
<col class="product-spec-col-gross" />
|
||||
<col class="product-spec-col-rate" />
|
||||
<col class="product-spec-col-rate" />
|
||||
<col class="product-spec-col-price" />
|
||||
<col class="product-spec-col-process" />
|
||||
<col class="product-spec-col-beat" />
|
||||
<col class="product-spec-col-status" />
|
||||
<col class="product-spec-col-action" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>产品编码</th>
|
||||
@@ -73,6 +90,7 @@
|
||||
<td class="action-row">
|
||||
<button
|
||||
class="ghost-button"
|
||||
data-semantic-label="编辑"
|
||||
type="button"
|
||||
:disabled="!canModifySpec(row)"
|
||||
:title="canModifySpec(row) ? '编辑' : '已停用的产品需规只读,不允许修改'"
|
||||
@@ -82,6 +100,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="ghost-button danger-ghost"
|
||||
data-semantic-label="删除"
|
||||
type="button"
|
||||
:disabled="!canModifySpec(row)"
|
||||
:title="canModifySpec(row) ? '删除' : '已停用的产品需规只读,不允许删除'"
|
||||
@@ -97,7 +116,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<PaginationBar v-model:page="page" v-model:page-size="pageSize" :total="filteredRows.length" />
|
||||
<PaginationBar class="product-spec-pagination" v-model:page="page" v-model:page-size="pageSize" :total="filteredRows.length" />
|
||||
</section>
|
||||
|
||||
<FormDrawer
|
||||
@@ -856,19 +875,23 @@ function buildAutoRouteOperation(processRow, index) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildRoutePayload(product, processRowsForSave) {
|
||||
const productItemId = product.item_id;
|
||||
function buildRouteOperationsPayload(processRowsForSave) {
|
||||
const processRows = buildProcessGroupForRoute(processRowsForSave);
|
||||
if (!processRows.length) {
|
||||
throw new Error("请至少维护一道小程序工序");
|
||||
}
|
||||
return processRows.map((processRow, index) => buildAutoRouteOperation(processRow, index));
|
||||
}
|
||||
|
||||
function buildRoutePayload(product, processRowsForSave, preparedOperations = null) {
|
||||
const productItemId = product.item_id;
|
||||
return {
|
||||
route_name: `${form.item_name} 工艺路线`,
|
||||
product_item_id: productItemId,
|
||||
version_no: form.version_no,
|
||||
is_default: true,
|
||||
status: "ACTIVE",
|
||||
operations: processRows.map((processRow, index) => buildAutoRouteOperation(processRow, index))
|
||||
operations: preparedOperations || buildRouteOperationsPayload(processRowsForSave)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -931,16 +954,16 @@ watch(calculatedLossRate, (lossRate) => {
|
||||
}, { immediate: true });
|
||||
|
||||
async function fetchMasterData() {
|
||||
const [productRows, bomRows, routeRows, materialRows, miniappProductRows, pointRows, processRows, centerRows] = await Promise.all([
|
||||
const [productRows, bomRows, routeRows, materialRows, miniappProductRows, pointRows] = await Promise.all([
|
||||
fetchResource("/master-data/products?limit=200", []),
|
||||
fetchResource("/master-data/boms?limit=500", []),
|
||||
fetchResource("/master-data/process-routes?limit=500", []),
|
||||
fetchResource("/master-data/materials?limit=200", []),
|
||||
fetchResource("/master-data/miniapp-products?limit=1000", []),
|
||||
fetchResource("/master-data/miniapp-attendance-points", []),
|
||||
fetchResource("/master-data/processes", []),
|
||||
fetchResource("/master-data/work-centers", [])
|
||||
fetchResource("/master-data/miniapp-attendance-points", [])
|
||||
]);
|
||||
const processRows = await fetchResource("/master-data/processes", []);
|
||||
const centerRows = await fetchResource("/master-data/work-centers", []);
|
||||
products.value = productRows;
|
||||
boms.value = bomRows;
|
||||
routes.value = routeRows;
|
||||
@@ -951,6 +974,18 @@ async function fetchMasterData() {
|
||||
workCenters.value = centerRows;
|
||||
}
|
||||
|
||||
async function ensureOperationFoundationReady() {
|
||||
if (!processes.value.length) {
|
||||
processes.value = await fetchResource("/master-data/processes", []);
|
||||
}
|
||||
if (!workCenters.value.length) {
|
||||
workCenters.value = await fetchResource("/master-data/work-centers", []);
|
||||
}
|
||||
if (!processes.value.length || !workCenters.value.length) {
|
||||
throw new Error("系统无法准备默认工艺基础资料,请稍后重试或联系管理员");
|
||||
}
|
||||
}
|
||||
|
||||
async function syncMiniappInBackground() {
|
||||
if (miniappSyncing.value) {
|
||||
return;
|
||||
@@ -977,8 +1012,12 @@ async function submitSpec() {
|
||||
feedbackMessage.value = "";
|
||||
errorMessage.value = "";
|
||||
submitting.value = true;
|
||||
let createdProductItemId = null;
|
||||
const createdMiniappPayloads = [];
|
||||
try {
|
||||
const processRowsForSave = normalizeProcessFormRowsForSave();
|
||||
await ensureOperationFoundationReady();
|
||||
const preparedRouteOperations = buildRouteOperationsPayload(processRowsForSave);
|
||||
const productPayload = {
|
||||
item_name: form.item_name,
|
||||
specification: editingRow.value?.specification || null,
|
||||
@@ -997,6 +1036,9 @@ async function submitSpec() {
|
||||
? await putResource(`/master-data/products/${editingRow.value.item_id}`, productPayload)
|
||||
: await postResource("/master-data/products", productPayload);
|
||||
const productItemId = product.item_id;
|
||||
if (!shouldUpdateErpProduct) {
|
||||
createdProductItemId = productItemId;
|
||||
}
|
||||
const canUpdateExistingSpec = editingRow.value && Number(editingRow.value.item_id) === Number(productItemId);
|
||||
if (canUpdateExistingSpec && editingRow.value.bom?.bom_id) {
|
||||
await putResource(`/master-data/boms/${editingRow.value.bom.bom_id}`, buildBomPayload(productItemId));
|
||||
@@ -1004,19 +1046,27 @@ async function submitSpec() {
|
||||
await postResource("/master-data/boms", buildBomPayload(productItemId));
|
||||
}
|
||||
if (canUpdateExistingSpec && editingRow.value.route?.route_id) {
|
||||
await putResource(`/master-data/process-routes/${editingRow.value.route.route_id}`, buildRoutePayload(product, processRowsForSave));
|
||||
await putResource(`/master-data/process-routes/${editingRow.value.route.route_id}`, buildRoutePayload(product, processRowsForSave, preparedRouteOperations));
|
||||
} else {
|
||||
await postResource("/master-data/process-routes", buildRoutePayload(product, processRowsForSave));
|
||||
await postResource("/master-data/process-routes", buildRoutePayload(product, processRowsForSave, preparedRouteOperations));
|
||||
}
|
||||
const miniappPayloads = buildMiniappProductPayloads(product, processRowsForSave);
|
||||
for (const payload of miniappPayloads) {
|
||||
await postResource("/master-data/miniapp-products?sync=false", payload);
|
||||
createdMiniappPayloads.push(payload);
|
||||
}
|
||||
await postResource("/master-data/miniapp-materials/sync?force=true", {});
|
||||
feedbackMessage.value = `产品需规 ${product.item_code} 已保存`;
|
||||
drawerOpen.value = false;
|
||||
await loadAll();
|
||||
} catch (error) {
|
||||
if (createdProductItemId) {
|
||||
for (const payload of createdMiniappPayloads) {
|
||||
await deleteResource(buildMiniappDeletePath(payload)).catch(() => null);
|
||||
}
|
||||
await deleteResource(`/master-data/product-specs/${createdProductItemId}`).catch(() => null);
|
||||
await fetchMasterData().catch(() => null);
|
||||
}
|
||||
errorMessage.value = error.message || "产品需规保存失败";
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
@@ -1038,6 +1088,177 @@ onActivated(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-spec-table-wrap,
|
||||
.product-spec-table-wrap.table-view-wide,
|
||||
.product-spec-table-wrap.table-view-overview {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.product-spec-table,
|
||||
.product-spec-table.data-table-wide,
|
||||
.product-spec-table.data-table-overview,
|
||||
.product-spec-table-wrap.table-view-wide .product-spec-table,
|
||||
.product-spec-table-wrap.table-view-overview .product-spec-table {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
|
||||
.product-spec-col-code {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.product-spec-col-attendance {
|
||||
width: 5.5%;
|
||||
}
|
||||
|
||||
.product-spec-col-project {
|
||||
width: 8.5%;
|
||||
}
|
||||
|
||||
.product-spec-col-product {
|
||||
width: 10.5%;
|
||||
}
|
||||
|
||||
.product-spec-col-version {
|
||||
width: 4.2%;
|
||||
}
|
||||
|
||||
.product-spec-col-weight {
|
||||
width: 4.6%;
|
||||
}
|
||||
|
||||
.product-spec-col-material {
|
||||
width: 11%;
|
||||
}
|
||||
|
||||
.product-spec-col-gross {
|
||||
width: 5.4%;
|
||||
}
|
||||
|
||||
.product-spec-col-rate {
|
||||
width: 5.7%;
|
||||
}
|
||||
|
||||
.product-spec-col-price {
|
||||
width: 5.4%;
|
||||
}
|
||||
|
||||
.product-spec-col-process {
|
||||
width: 6.2%;
|
||||
}
|
||||
|
||||
.product-spec-col-beat {
|
||||
width: 5.2%;
|
||||
}
|
||||
|
||||
.product-spec-col-status {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.product-spec-col-action {
|
||||
width: 8.6%;
|
||||
}
|
||||
|
||||
.product-spec-table th,
|
||||
.product-spec-table td,
|
||||
.product-spec-table.data-table-wide th,
|
||||
.product-spec-table.data-table-overview th,
|
||||
.product-spec-table-wrap.table-view-wide .product-spec-table th,
|
||||
.product-spec-table-wrap.table-view-overview .product-spec-table th {
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.product-spec-table th,
|
||||
.product-spec-table th :deep(.sortable-th-button),
|
||||
.product-spec-table th :deep(.sortable-th-label) {
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
white-space: normal !important;
|
||||
word-break: keep-all !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
line-height: 1.22;
|
||||
}
|
||||
|
||||
.product-spec-table th :deep(.sortable-th-button) {
|
||||
grid-template-columns: minmax(0, 1fr) 16px !important;
|
||||
column-gap: 4px !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.product-spec-table th :deep(.sortable-th-label) {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.product-spec-table td:not(.action-row),
|
||||
.product-spec-table.data-table-wide td:not(.action-row),
|
||||
.product-spec-table.data-table-overview td:not(.action-row) {
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.product-spec-table .action-row,
|
||||
.product-spec-table.data-table-wide .action-row,
|
||||
.product-spec-table.data-table-overview .action-row {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding-right: 8px !important;
|
||||
padding-left: 8px !important;
|
||||
text-align: center !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.product-spec-table-wrap.table-view-wide .product-spec-table:has(td.action-row) thead th:last-child,
|
||||
.product-spec-table-wrap.table-view-overview .product-spec-table:has(td.action-row) thead th:last-child,
|
||||
.product-spec-table.data-table-wide:has(td.action-row) thead th:last-child,
|
||||
.product-spec-table.data-table-overview:has(td.action-row) thead th:last-child {
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.product-spec-table .action-row .ghost-button,
|
||||
.product-spec-table .action-row :deep(.semantic-action-button) {
|
||||
width: calc((100% - 6px) / 2) !important;
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
justify-content: center !important;
|
||||
margin-right: 0 !important;
|
||||
padding: 0 6px !important;
|
||||
gap: 5px !important;
|
||||
}
|
||||
|
||||
.product-spec-table .action-row .ghost-button + .ghost-button,
|
||||
.product-spec-table .action-row :deep(.semantic-action-button + .semantic-action-button) {
|
||||
margin-left: 6px !important;
|
||||
}
|
||||
|
||||
.product-spec-table .action-row :deep(.semantic-action-label) {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-spec-pagination {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.product-spec-pagination :deep(.pagination-actions) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.process-field-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
Reference in New Issue
Block a user