Add optimization branch updates
This commit is contained in:
@@ -481,11 +481,11 @@ assert.match(
|
||||
);
|
||||
assert.match(
|
||||
inventoryLedgerSource,
|
||||
/source_doc_type: isProductionSurplusInbound\.value \? "WORK_ORDER" : null[\s\S]*source_doc_id: isProductionSurplusInbound\.value \? Number\(selectedGenericWorkOrder\.value\.work_order_id\) : null/
|
||||
/source_doc_type: isProductionSurplusInbound\.value \? "PRODUCTION_LEDGER" : null[\s\S]*source_doc_id: isProductionSurplusInbound\.value \? Number\(selectedGenericWorkOrder\.value\.production_ledger_id \|\| selectedGenericWorkOrder\.value\.work_order_id\) : null/
|
||||
);
|
||||
assert.match(
|
||||
inventoryLedgerSource,
|
||||
/source_doc_type: isProductionScrapInbound\.value \|\| isReworkScrapInbound\.value \? "WORK_ORDER" : activeWarehouseType\.value === "SEMI" \? "PRODUCT_ROUTE_OPERATION" : null[\s\S]*source_doc_id: isProductionScrapInbound\.value \|\| isReworkScrapInbound\.value \? Number\(selectedGenericWorkOrder\.value\.work_order_id\) : activeWarehouseType\.value === "SEMI" \? Number\(selectedSourceDocId \|\| 0\) : null/
|
||||
/source_doc_type: isProductionScrapInbound\.value \? "PRODUCTION_LEDGER" : isReworkScrapInbound\.value \? "WORK_ORDER" : activeWarehouseType\.value === "SEMI" \? "PRODUCT_ROUTE_OPERATION" : null[\s\S]*isProductionScrapInbound\.value[\s\S]*Number\(selectedGenericWorkOrder\.value\.production_ledger_id \|\| selectedGenericWorkOrder\.value\.work_order_id\)[\s\S]*isReworkScrapInbound\.value[\s\S]*Number\(selectedGenericWorkOrder\.value\.work_order_id\)/
|
||||
);
|
||||
assert.match(inventoryLedgerSource, /:key="`inventory-main-table-\$\{activeInventoryTab\}`"/);
|
||||
assert.match(inventoryLedgerSource, /:key="`inventory-controls-\$\{activeInventoryTab\}`"/);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = process.cwd();
|
||||
const source = readFileSync(path.join(root, "src/services/api.js"), "utf8");
|
||||
|
||||
assert.match(source, /\/auth\/login-key/, "login should fetch the backend public key before submitting");
|
||||
assert.match(source, /password_ciphertext/, "login payload should submit encrypted password ciphertext");
|
||||
assert.match(source, /login_key_id/, "login payload should include the public key id used for encryption");
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/JSON\.stringify\(\{\s*username,\s*password\s*\}\)/,
|
||||
"login must not serialize the plaintext password field"
|
||||
);
|
||||
assert.doesNotMatch(source, /password:\s*password/, "login payload must not include plaintext password");
|
||||
|
||||
console.log("login password encryption checks passed");
|
||||
@@ -19,7 +19,7 @@ assert.match(
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/!isFinishedOutsourcingInbound && !isProductionScrapInbound && !isOutsourcingScrapInbound/,
|
||||
/!isFinishedOutsourcingInbound && !isProductionScrapInbound && !isReworkScrapInbound && !isOutsourcingScrapInbound/,
|
||||
"outsourcing scrap inbound should hide the manual lot number field"
|
||||
);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const app = read("src/App.vue");
|
||||
const productionLedger = read("src/views/ProductionLedgerView.vue");
|
||||
const inventory = read("src/views/InventoryLedgerView.vue");
|
||||
|
||||
assert.match(router, /ProductionLedgerView/, "router should import ProductionLedgerView");
|
||||
assert.match(router, /component:\s*view\("ProductionLedgerView"\)/, "router should lazy load ProductionLedgerView");
|
||||
assert.match(router, /name:\s*"production-ledger"/, "router should expose production-ledger route");
|
||||
assert.match(router, /path:\s*"\/work-order-ledger"[\s\S]*redirect:\s*\{\s*name:\s*"production-ledger"\s*\}/, "old work-order-ledger route should redirect to production-ledger");
|
||||
assert.match(app, /生产台账/, "sidebar should show 生产台账");
|
||||
|
||||
@@ -15,12 +15,12 @@ const inventoryView = readSource("src/views/InventoryLedgerView.vue");
|
||||
|
||||
assert.match(
|
||||
router,
|
||||
/import\s+ProductionLedgerView\s+from\s+"..\/views\/ProductionLedgerView\.vue";/,
|
||||
"router should import ProductionLedgerView"
|
||||
/component:\s*view\("ProductionLedgerView"\)/,
|
||||
"router should lazy load ProductionLedgerView"
|
||||
);
|
||||
assert.match(
|
||||
router,
|
||||
/path:\s*"\/production-ledger"[\s\S]*?name:\s*"production-ledger"[\s\S]*?component:\s*ProductionLedgerView[\s\S]*?permission:\s*"MENU_WORK_ORDER"/,
|
||||
/path:\s*"\/production-ledger"[\s\S]*?name:\s*"production-ledger"[\s\S]*?component:\s*view\("ProductionLedgerView"\)[\s\S]*?permission:\s*"MENU_WORK_ORDER"/,
|
||||
"router should define production-ledger route with MENU_WORK_ORDER permission"
|
||||
);
|
||||
assert.match(
|
||||
|
||||
@@ -31,7 +31,7 @@ assert.match(
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/ref="salesOrderPickerRef"\s+class="order-tag-picker"\s+@pointerdown\.stop\s+@mousedown\.stop\s+@click\.stop/,
|
||||
/ref="salesOrderPickerRef"[\s\S]*class="[^"]*\border-tag-picker\b[^"]*"[\s\S]*@pointerdown\.stop[\s\S]*@mousedown\.stop[\s\S]*@click\.stop/,
|
||||
"sales order picker should keep internal clicks from bubbling into unrelated close handlers"
|
||||
);
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ assert.doesNotMatch(source, /YL-NB|buildDefaultLotNo/, "purchase receipt UI must
|
||||
assert.doesNotMatch(source, /送货单号|supplier_delivery_no/, "purchase receipt UI should not keep the duplicate supplier delivery number field");
|
||||
assert.match(source, /后端自动生成库存批次号|系统自动生成库存批次号/, "purchase receipt UI should explain that inventory lot numbers are generated by the backend");
|
||||
assert.match(source, /uploadResource/, "purchase receipt UI should upload logistics auxiliary photos");
|
||||
assert.match(source, /selectedPurchaseTargetType\s*===\s*["']RAW["'][\s\S]*运单号[\s\S]*辅助照片/, "raw purchase receipt UI should show waybill and auxiliary photo fields");
|
||||
assert.match(
|
||||
source,
|
||||
/if\s*\(\s*selectedPurchaseTargetType\.value\s*===\s*["']RAW["']\s*\)\s*\{[\s\S]*key:\s*["']waybill_no["'][\s\S]*label:\s*["']运单号["'][\s\S]*key:\s*["']order_photo["'][\s\S]*label:\s*["']辅助照片["']/,
|
||||
"raw purchase receipt UI should show waybill and auxiliary photo fields"
|
||||
);
|
||||
assert.match(source, /validateLogisticsForm\s*\(\s*\)/, "raw purchase receipt submit should validate logistics information before posting");
|
||||
assert.match(source, /waybill_no:\s*selectedPurchaseTargetType\.value\s*===\s*["']RAW["']\s*\?\s*form\.waybill_no/, "purchase receipt payload should include raw waybill number");
|
||||
assert.match(source, /order_photo_url:\s*selectedPurchaseTargetType\.value\s*===\s*["']RAW["']\s*\?\s*form\.order_photo_url/, "purchase receipt payload should include raw auxiliary photo URL");
|
||||
|
||||
@@ -11,7 +11,7 @@ const requiredInventorySnippets = [
|
||||
"返工废料入库",
|
||||
"reworkScrapInbound",
|
||||
"isReworkScrapInbound",
|
||||
"source_doc_type: isProductionScrapInbound.value || isReworkScrapInbound.value ? \"WORK_ORDER\"",
|
||||
"source_doc_type: isProductionScrapInbound.value ? \"PRODUCTION_LEDGER\" : isReworkScrapInbound.value ? \"WORK_ORDER\"",
|
||||
"inbound_qty: inboundQty"
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const routerSource = readFileSync(new URL("../src/router/index.js", import.meta.url), "utf8");
|
||||
|
||||
assert.doesNotMatch(
|
||||
routerSource,
|
||||
/import\s+\w+View\s+from\s+"..\/views\/[^"]+\.vue";/,
|
||||
"router should not statically import view components into the initial bundle"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
routerSource,
|
||||
/import\.meta\.glob\("\.\.\/views\/\*\.vue"\)/,
|
||||
"router should register lazy view modules with Vite import.meta.glob"
|
||||
);
|
||||
|
||||
for (const viewName of [
|
||||
"LoginView",
|
||||
"DashboardView",
|
||||
"InventoryLedgerView",
|
||||
"PurchaseOrderView",
|
||||
"ProductionLedgerView",
|
||||
"SystemPermissionView"
|
||||
]) {
|
||||
assert.match(routerSource, new RegExp(`component:\\s*view\\("${viewName}"\\)`), `${viewName} route should use lazy view loader`);
|
||||
}
|
||||
|
||||
console.log("router lazy loading static checks passed");
|
||||
@@ -1,9 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const appVuePath = resolve("frontend/src/App.vue");
|
||||
const source = readFileSync(appVuePath, "utf8");
|
||||
const source = readFileSync(new URL("../src/App.vue", import.meta.url), "utf8");
|
||||
|
||||
const navStart = source.indexOf("const navStages = [");
|
||||
const navEnd = source.indexOf("\n];", navStart);
|
||||
|
||||
@@ -139,13 +139,19 @@ const styleSource = readFileSync(
|
||||
fileURLToPath(new URL("../src/styles/main.css", import.meta.url)),
|
||||
"utf8"
|
||||
);
|
||||
const stocktakeDiffModalStart = styleSource.indexOf(".stocktake-diff-modal {");
|
||||
const stocktakeDiffModalEnd = styleSource.indexOf("\n}", stocktakeDiffModalStart);
|
||||
assert.notEqual(stocktakeDiffModalStart, -1);
|
||||
assert.notEqual(stocktakeDiffModalEnd, -1);
|
||||
const stocktakeDiffModalBlock = styleSource.slice(stocktakeDiffModalStart, stocktakeDiffModalEnd + 2);
|
||||
|
||||
assert.match(styleSource, /\.stocktake-diff-modal \.stocktake-diff-table-v2[\s\S]*margin: 14px 18px 0;/);
|
||||
assert.match(styleSource, /\.stocktake-diff-modal \.stocktake-footer[\s\S]*min-height: 72px;/);
|
||||
assert.match(styleSource, /\.stocktake-diff-modal-mask[\s\S]*box-sizing: border-box;/);
|
||||
assert.match(styleSource, /\.stocktake-diff-modal-mask[\s\S]*z-index: 3;/);
|
||||
assert.match(styleSource, /\.stocktake-diff-modal[\s\S]*height: min\(820px, 100%\);/);
|
||||
assert.match(styleSource, /\.stocktake-diff-modal[\s\S]*max-height: 100%;/);
|
||||
assert.doesNotMatch(styleSource, /\.stocktake-diff-modal[\s\S]*calc\(100vh - 64px\)/);
|
||||
assert.match(stocktakeDiffModalBlock, /height: min\(820px, 100%\);/);
|
||||
assert.match(stocktakeDiffModalBlock, /max-height: 100%;/);
|
||||
assert.doesNotMatch(stocktakeDiffModalBlock, /calc\(100vh - 64px\)/);
|
||||
assert.match(styleSource, /\.stocktake-diff-modal \.stocktake-footer[\s\S]*justify-content: flex-end;/);
|
||||
assert.match(styleSource, /\.stocktake-confirm-mask[\s\S]*z-index: 4;/);
|
||||
assert.match(styleSource, /\.stocktake-diff-cell,[\s\S]*overflow-wrap: anywhere;/);
|
||||
|
||||
@@ -18,8 +18,8 @@ assert.match(
|
||||
|
||||
assert.match(
|
||||
routerSource,
|
||||
/import SystemPermissionView from "\.\.\/views\/SystemPermissionView\.vue";/,
|
||||
"router should import SystemPermissionView"
|
||||
/component:\s*view\("SystemPermissionView"\)/,
|
||||
"router should lazy load SystemPermissionView"
|
||||
);
|
||||
assert.match(routerSource, /path:\s*"\/system-management"[\s\S]*redirect:/, "router should add /system-management redirect");
|
||||
assert.match(routerSource, /path:\s*"\/system-permissions"/, "router should add /system-permissions route");
|
||||
@@ -203,7 +203,7 @@ assert.doesNotMatch(
|
||||
);
|
||||
assert.match(
|
||||
systemPermissionSource,
|
||||
/function fitMindMapToScreen\(\)[\s\S]*mindMapFitRequestKey\.value \+= 1/,
|
||||
/async function requestMindMapFitToScreen[\s\S]*mindMapFitRequestKey\.value \+= 1[\s\S]*function fitMindMapToScreen\(\)\s*\{\s*requestMindMapFitToScreen\(\{ showToast: true, closeMenu: true \}\);/m,
|
||||
"one-screen fit action should request a viewport fit explicitly"
|
||||
);
|
||||
assert.match(systemPermissionSource, /const updatePayload\s*=/, "org edit must build an update-only payload");
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
function read(path) {
|
||||
return readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
function ruleBlock(source, selector) {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = source.match(new RegExp(`${escaped}\\s*\\{[\\s\\S]*?\\n\\}`, "m"));
|
||||
assert.ok(match, `Expected to find CSS block for ${selector}`);
|
||||
return match[0];
|
||||
}
|
||||
|
||||
function assertNoDeclaration(block, property) {
|
||||
assert.ok(!new RegExp(`\\b${property}\\s*:`, "m").test(block), `Unexpected ${property} declaration in:\n${block}`);
|
||||
}
|
||||
|
||||
const mainCss = read("src/styles/main.css");
|
||||
const purchaseOrderView = read("src/views/PurchaseOrderView.vue");
|
||||
const salesPlanningView = read("src/views/SalesPlanningView.vue");
|
||||
const orgMindMap = read("src/components/OrgMindMap.vue");
|
||||
|
||||
assertNoDeclaration(ruleBlock(mainCss, ".workflow-drawer"), "border-left");
|
||||
assertNoDeclaration(ruleBlock(mainCss, "body .login-console-frame"), "border-left");
|
||||
assertNoDeclaration(ruleBlock(purchaseOrderView, ".section-title"), "border-left");
|
||||
assertNoDeclaration(ruleBlock(salesPlanningView, ".section-title"), "border-left");
|
||||
assertNoDeclaration(ruleBlock(purchaseOrderView, ".purchase-decision-section"), "border-right");
|
||||
|
||||
assert.ok(
|
||||
!/transition:[^;]*(?:stroke-width|\br\s+[0-9.]+s)/.test(mainCss),
|
||||
"SVG DAG transitions should not animate stroke-width or radius-like r properties"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!/transition:[^;]*(?:\bwidth\b|\bheight\b)/.test(orgMindMap),
|
||||
"Org mind map canvas should not animate width or height"
|
||||
);
|
||||
|
||||
console.log("ui antipattern guard ok");
|
||||
@@ -484,7 +484,6 @@ defineExpose({
|
||||
|
||||
.org-mind-map-canvas {
|
||||
display: block;
|
||||
transition: width 0.18s ease, height 0.18s ease;
|
||||
}
|
||||
|
||||
.org-mind-map-connector {
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
import DashboardView from "../views/DashboardView.vue";
|
||||
import SalesPlanningView from "../views/SalesPlanningView.vue";
|
||||
import EmployeeManagementView from "../views/EmployeeManagementView.vue";
|
||||
import ProductManagementView from "../views/ProductManagementView.vue";
|
||||
import BomManagementView from "../views/BomManagementView.vue";
|
||||
import ProcessRouteManagementView from "../views/ProcessRouteManagementView.vue";
|
||||
import LoginView from "../views/LoginView.vue";
|
||||
import SupplierManagementView from "../views/SupplierManagementView.vue";
|
||||
import PurchaseOrderView from "../views/PurchaseOrderView.vue";
|
||||
import PurchaseReceiptView from "../views/PurchaseReceiptView.vue";
|
||||
import InventoryLedgerView from "../views/InventoryLedgerView.vue";
|
||||
import ProductionLedgerView from "../views/ProductionLedgerView.vue";
|
||||
import OperationReportView from "../views/OperationReportView.vue";
|
||||
import DeliveryManagementView from "../views/DeliveryManagementView.vue";
|
||||
import EquipmentManagementView from "../views/EquipmentManagementView.vue";
|
||||
import QualityInspectionView from "../views/QualityInspectionView.vue";
|
||||
import FinanceReportView from "../views/FinanceReportView.vue";
|
||||
import ProductSpecLiteView from "../views/ProductSpecLiteView.vue";
|
||||
import SupplierMaterialLiteView from "../views/SupplierMaterialLiteView.vue";
|
||||
import SystemPermissionView from "../views/SystemPermissionView.vue";
|
||||
import SystemExtensionView from "../views/SystemExtensionView.vue";
|
||||
import MasterDataImportView from "../views/MasterDataImportView.vue";
|
||||
import { isAuthenticated, readSession } from "../services/api";
|
||||
import { loadSmartOperationReportConfig } from "../services/systemFeatures";
|
||||
|
||||
const viewModules = import.meta.glob("../views/*.vue");
|
||||
|
||||
function view(name) {
|
||||
const loader = viewModules[`../views/${name}.vue`];
|
||||
if (!loader) {
|
||||
throw new Error(`Missing route view: ${name}`);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
const masterDataPermissionOrder = [
|
||||
{
|
||||
routeName: "supplier-management",
|
||||
@@ -224,13 +212,13 @@ const routes = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: LoginView,
|
||||
component: view("LoginView"),
|
||||
meta: { title: "工业风登录", layout: "auth" }
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "dashboard",
|
||||
component: DashboardView,
|
||||
component: view("DashboardView"),
|
||||
meta: { title: "原材料批次生命周期追踪", requiresAuth: true, permission: "MENU_DASHBOARD" }
|
||||
},
|
||||
{
|
||||
@@ -243,13 +231,13 @@ const routes = [
|
||||
{
|
||||
path: "/customer-management",
|
||||
name: "customer-management",
|
||||
component: SalesPlanningView,
|
||||
component: view("SalesPlanningView"),
|
||||
meta: { title: "客户名录", requiresAuth: true, permission: "MENU_SALES_PLANNING" }
|
||||
},
|
||||
{
|
||||
path: "/order-list",
|
||||
name: "order-list",
|
||||
component: SalesPlanningView,
|
||||
component: view("SalesPlanningView"),
|
||||
meta: { title: "销售订单列表", requiresAuth: true, permission: "MENU_SALES_PLANNING" }
|
||||
},
|
||||
{
|
||||
@@ -312,7 +300,7 @@ const routes = [
|
||||
{
|
||||
path: "/supplier-material-lite",
|
||||
name: "supplier-material-lite",
|
||||
component: SupplierMaterialLiteView,
|
||||
component: view("SupplierMaterialLiteView"),
|
||||
meta: {
|
||||
title: "原材料名录",
|
||||
requiresAuth: true,
|
||||
@@ -322,7 +310,7 @@ const routes = [
|
||||
{
|
||||
path: "/product-spec-lite",
|
||||
name: "product-spec-lite",
|
||||
component: ProductSpecLiteView,
|
||||
component: view("ProductSpecLiteView"),
|
||||
meta: {
|
||||
title: "产品需规清单",
|
||||
requiresAuth: true,
|
||||
@@ -332,31 +320,31 @@ const routes = [
|
||||
{
|
||||
path: "/supplier-management",
|
||||
name: "supplier-management",
|
||||
component: SupplierManagementView,
|
||||
component: view("SupplierManagementView"),
|
||||
meta: { title: "供应商名录", requiresAuth: true, permission: "MENU_SUPPLIER_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
path: "/purchase-orders",
|
||||
name: "purchase-orders",
|
||||
component: PurchaseOrderView,
|
||||
component: view("PurchaseOrderView"),
|
||||
meta: { title: "采购订单", requiresAuth: true, permission: "MENU_PURCHASE_ORDER" }
|
||||
},
|
||||
{
|
||||
path: "/purchase-receipts",
|
||||
name: "purchase-receipts",
|
||||
component: PurchaseReceiptView,
|
||||
component: view("PurchaseReceiptView"),
|
||||
meta: { title: "到货入库", requiresAuth: true, permission: "MENU_PURCHASE_RECEIPT" }
|
||||
},
|
||||
{
|
||||
path: "/inventory-ledger",
|
||||
name: "inventory-ledger",
|
||||
component: InventoryLedgerView,
|
||||
component: view("InventoryLedgerView"),
|
||||
meta: { title: "原材料库存台账与批次", requiresAuth: true, permission: "MENU_INVENTORY_LEDGER" }
|
||||
},
|
||||
{
|
||||
path: "/production-ledger",
|
||||
name: "production-ledger",
|
||||
component: ProductionLedgerView,
|
||||
component: view("ProductionLedgerView"),
|
||||
meta: { title: "生产台账", requiresAuth: true, permission: "MENU_WORK_ORDER" }
|
||||
},
|
||||
{
|
||||
@@ -368,7 +356,7 @@ const routes = [
|
||||
{
|
||||
path: "/operation-reports",
|
||||
name: "operation-reports",
|
||||
component: OperationReportView,
|
||||
component: view("OperationReportView"),
|
||||
meta: { title: "工序报工", requiresAuth: true, permission: "MENU_OPERATION_REPORT" }
|
||||
},
|
||||
{
|
||||
@@ -380,25 +368,25 @@ const routes = [
|
||||
{
|
||||
path: "/delivery-management",
|
||||
name: "delivery-management",
|
||||
component: DeliveryManagementView,
|
||||
component: view("DeliveryManagementView"),
|
||||
meta: { title: "发货台账", requiresAuth: true, permission: "MENU_DELIVERY_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
path: "/equipment-management",
|
||||
name: "equipment-management",
|
||||
component: EquipmentManagementView,
|
||||
component: view("EquipmentManagementView"),
|
||||
meta: { title: "设备管理", requiresAuth: true, permission: "MENU_EQUIPMENT_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
path: "/quality-inspection",
|
||||
name: "quality-inspection",
|
||||
component: QualityInspectionView,
|
||||
component: view("QualityInspectionView"),
|
||||
meta: { title: "质量检验", requiresAuth: true, permission: "MENU_QUALITY_INSPECTION" }
|
||||
},
|
||||
{
|
||||
path: "/finance-reports",
|
||||
name: "finance-reports",
|
||||
component: FinanceReportView,
|
||||
component: view("FinanceReportView"),
|
||||
meta: { title: "冲压产品材料成本核算", requiresAuth: true, permission: "MENU_FINANCE_REPORT" }
|
||||
},
|
||||
{
|
||||
@@ -416,25 +404,25 @@ const routes = [
|
||||
{
|
||||
path: "/bom-management",
|
||||
name: "bom-management",
|
||||
component: BomManagementView,
|
||||
component: view("BomManagementView"),
|
||||
meta: { title: "BOM管理", requiresAuth: true, permission: "MENU_BOM_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
path: "/process-route-management",
|
||||
name: "process-route-management",
|
||||
component: ProcessRouteManagementView,
|
||||
component: view("ProcessRouteManagementView"),
|
||||
meta: { title: "工艺路线管理", requiresAuth: true, permission: "MENU_ROUTE_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
path: "/employee-management",
|
||||
name: "employee-management",
|
||||
component: EmployeeManagementView,
|
||||
component: view("EmployeeManagementView"),
|
||||
meta: { title: "员工管理", requiresAuth: true, permission: "MENU_EMPLOYEE_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
path: "/master-data-import",
|
||||
name: "master-data-import",
|
||||
component: MasterDataImportView,
|
||||
component: view("MasterDataImportView"),
|
||||
meta: {
|
||||
title: "基础资料导入导出",
|
||||
requiresAuth: true,
|
||||
@@ -444,7 +432,7 @@ const routes = [
|
||||
{
|
||||
path: "/product-management",
|
||||
name: "product-management",
|
||||
component: ProductManagementView,
|
||||
component: view("ProductManagementView"),
|
||||
meta: { title: "产品管理", requiresAuth: true, permission: "MENU_PRODUCT_MANAGEMENT" }
|
||||
},
|
||||
{
|
||||
@@ -460,13 +448,13 @@ const routes = [
|
||||
{
|
||||
path: "/system-permissions",
|
||||
name: "system-permissions",
|
||||
component: SystemPermissionView,
|
||||
component: view("SystemPermissionView"),
|
||||
meta: { title: "系统权限管理", requiresAuth: true, permission: "MENU_SYSTEM_PERMISSION" }
|
||||
},
|
||||
{
|
||||
path: "/system-extension",
|
||||
name: "system-extension",
|
||||
component: SystemExtensionView,
|
||||
component: view("SystemExtensionView"),
|
||||
meta: { title: "系统拓展", requiresAuth: true, permission: "MENU_SYSTEM_EXTENSION" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -245,10 +245,64 @@ export function isAuthenticated() {
|
||||
return Boolean(readSession());
|
||||
}
|
||||
|
||||
function pemToArrayBuffer(pem) {
|
||||
const base64Text = pem
|
||||
.replace(/-----BEGIN PUBLIC KEY-----/g, "")
|
||||
.replace(/-----END PUBLIC KEY-----/g, "")
|
||||
.replace(/\s/g, "");
|
||||
const binary = window.atob(base64Text);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
for (let index = 0; index < bytes.byteLength; index += 1) {
|
||||
binary += String.fromCharCode(bytes[index]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
async function encryptLoginPassword(password, publicKeyPem) {
|
||||
if (!window.crypto?.subtle) {
|
||||
throw new Error("当前访问方式不支持安全加密登录,请使用 HTTPS 或 localhost 地址访问系统");
|
||||
}
|
||||
|
||||
const publicKey = await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
pemToArrayBuffer(publicKeyPem),
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256"
|
||||
},
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
const ciphertext = await window.crypto.subtle.encrypt(
|
||||
{ name: "RSA-OAEP" },
|
||||
publicKey,
|
||||
new TextEncoder().encode(password)
|
||||
);
|
||||
return arrayBufferToBase64(ciphertext);
|
||||
}
|
||||
|
||||
export async function login(username, password) {
|
||||
const loginKey = await requestJson("/auth/login-key", {
|
||||
method: "GET",
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
const passwordCiphertext = await encryptLoginPassword(password, loginKey.public_key_pem);
|
||||
return requestJson("/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password_ciphertext: passwordCiphertext,
|
||||
login_key_id: loginKey.key_id
|
||||
}),
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2556,7 +2556,7 @@ a {
|
||||
stroke-linejoin: bevel;
|
||||
filter: drop-shadow(0 0 7px rgba(79, 180, 255, 0.34));
|
||||
opacity: 0.92;
|
||||
transition: stroke 0.14s ease, stroke-width 0.14s ease, opacity 0.14s ease, filter 0.14s ease;
|
||||
transition: stroke 0.14s ease, opacity 0.14s ease, filter 0.14s ease;
|
||||
}
|
||||
|
||||
.dag-edge-highlighted {
|
||||
@@ -2605,7 +2605,7 @@ a {
|
||||
stroke: rgba(6, 21, 34, 0.96);
|
||||
stroke-width: 1.6;
|
||||
filter: drop-shadow(0 0 8px rgba(79, 180, 255, 0.5));
|
||||
transition: r 0.14s ease, opacity 0.14s ease, filter 0.14s ease, stroke-width 0.14s ease;
|
||||
transition: opacity 0.14s ease, filter 0.14s ease;
|
||||
}
|
||||
|
||||
.dag-edge-terminal-warning {
|
||||
@@ -2621,7 +2621,7 @@ a {
|
||||
stroke: rgba(144, 215, 255, 0.96);
|
||||
stroke-width: 2.4;
|
||||
filter: drop-shadow(0 0 8px rgba(79, 180, 255, 0.5));
|
||||
transition: r 0.14s ease, opacity 0.14s ease, filter 0.14s ease, stroke-width 0.14s ease;
|
||||
transition: opacity 0.14s ease, filter 0.14s ease;
|
||||
}
|
||||
|
||||
.dag-edge-start-warning {
|
||||
@@ -3825,7 +3825,7 @@ a {
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(79, 180, 255, 0.14), transparent 24%),
|
||||
linear-gradient(180deg, rgba(9, 18, 29, 0.98), rgba(5, 11, 18, 0.98));
|
||||
border-left: 1px solid rgba(79, 180, 255, 0.2);
|
||||
border: 1px solid rgba(79, 180, 255, 0.2);
|
||||
border-radius: 28px 0 0 28px;
|
||||
box-shadow: -24px 0 70px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
@@ -15060,7 +15060,7 @@ body .login-console-frame {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 20px 20px 22px 24px;
|
||||
border-left: 2px solid rgba(56, 189, 248, 0.48);
|
||||
border: 1px solid rgba(56, 189, 248, 0.28);
|
||||
border-radius: 34px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(56, 189, 248, 0.12), transparent 22%),
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
:class="{ 'is-uploaded': genericForm.order_photo_url, 'is-busy': logisticsPhotoUploading }"
|
||||
>
|
||||
<input
|
||||
class="modern-upload-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
:disabled="logisticsPhotoUploading"
|
||||
|
||||
@@ -160,6 +160,7 @@ async function handleLogin() {
|
||||
|
||||
try {
|
||||
const user = await login(form.username, form.password);
|
||||
form.password = "";
|
||||
saveSession(user);
|
||||
message.value = `欢迎回来,${user.display_name}`;
|
||||
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/";
|
||||
|
||||
@@ -1444,7 +1444,7 @@ function renderPurchaseContractHtml(contractOrders) {
|
||||
h1 { margin: 0 0 10px; text-align: center; letter-spacing: 10px; font-size: 28px; }
|
||||
.contract-no { text-align: right; color: #475569; margin-bottom: 22px; }
|
||||
.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 18px; line-height: 1.8; }
|
||||
.section-title { margin: 22px 0 10px; padding-left: 10px; border-left: 4px solid #2563eb; font-weight: 800; font-size: 16px; }
|
||||
.section-title { margin: 22px 0 10px; padding: 6px 10px; border: 1px solid #bfdbfe; border-radius: 4px; background: #eff6ff; font-weight: 800; font-size: 16px; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 8px 0 14px; font-size: 12px; }
|
||||
th, td { border: 1px solid #94a3b8; padding: 7px 6px; vertical-align: middle; }
|
||||
th { background: #eaf2ff; text-align: center; }
|
||||
@@ -2281,7 +2281,7 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-right: 2px solid var(--document-paper-grid, rgba(20, 14, 6, 0.88));
|
||||
box-shadow: inset -1px 0 0 var(--document-paper-grid, rgba(20, 14, 6, 0.88));
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -932,7 +932,7 @@ function renderContractHtml(orders) {
|
||||
h1 { margin: 0 0 10px; text-align: center; letter-spacing: 10px; font-size: 28px; }
|
||||
.contract-no { text-align: right; color: #475569; margin-bottom: 22px; }
|
||||
.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 18px; line-height: 1.8; }
|
||||
.section-title { margin: 22px 0 10px; padding-left: 10px; border-left: 4px solid #2563eb; font-weight: 800; font-size: 16px; }
|
||||
.section-title { margin: 22px 0 10px; padding: 6px 10px; border: 1px solid #bfdbfe; border-radius: 4px; background: #eff6ff; font-weight: 800; font-size: 16px; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 8px 0 14px; font-size: 12px; }
|
||||
th, td { border: 1px solid #94a3b8; padding: 7px 6px; vertical-align: middle; }
|
||||
th { background: #eaf2ff; text-align: center; }
|
||||
|
||||
Reference in New Issue
Block a user