From c2e8526f8f857be5e3a8c6f2b39c5efd424de33e Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 15 十月 2026 11:50:24 +0800
Subject: [PATCH] refactor(auth): 优化认证逻辑和界面组件结构
---
src/views/bi/dashboard/index.vue | 136 --
src/views/mes/pro/mps/data.ts | 5
src/api/mes/pd/document-audit/index.ts | 4
src/api/mes/dv/spare/index.ts | 30
src/api/mes/qc/ipqc/index.ts | 8
src/views/mes/pro/mps/modules/form.vue | 3
src/api/mes/md/client/index.ts | 7
src/api/mes/wm/salesnotice/index.ts | 1
src/views/mes/pro/route/data.ts | 13
src/components/quality/engine/report-evaluator.ts | 13
src/api/mes/wm/productissue/index.ts | 1
src/api/wls/batch/index.ts | 1
src/api/mes/wm/productsales/index.ts | 1
src/views/wls/returnvendor/modules/line-list.vue | 3
src/views/crm/invoice/data.ts | 9
src/api/wls/productissue/index.ts | 1
src/views/mes/pro/workorder/data.ts | 19
src/utils/rangePickerProps.ts | 10
src/views/_core/authentication/login.vue | 36
src/views/mes/pro/card/modules/form.vue | 14
src/views/mes/qc/ledger/data.ts | 4
src/api/mes/qc/iqc/index.ts | 8
src/views/mes/pro/workorder/index.vue | 2
src/api/mes/qc/indicatorresult/index.ts | 1
src/views/wls/outsourcereceipt/modules/line-list.vue | 3
src/api/mes/pro/mps/index.ts | 1
src/api/wls/salesnotice/index.ts | 1
src/views/wls/productreceipt/data.ts | 6
src/packages/effects/hooks/src/use-design-tokens.ts | 3
src/views/mes/pro/card/index.vue | 4
src/api/mes/pro/task/index.ts | 49 +
src/api/wls/packages/index.ts | 1
src/api/mes/wm/returnsales/index.ts | 1
src/api/wls/productsales/index.ts | 1
src/views/mes/qc/iqc/data.ts | 21
vite.config.ts | 2
src/views/bi/warehouse/index.vue | 103 -
src/views/wls/returnsales/modules/line-list.vue | 3
src/layouts/auth-scene.vue | 78 +
src/api/core/auth.ts | 3
src/views/wls/itemreceipt/modules/line-list.vue | 3
src/views/mes/qc/indicatorresult/components/data.ts | 20
src/packages/constants/src/biz-mes-enum.ts | 9
src/api/mes/qc/date-filter.ts | 18
public/images/auth/herbal-courtyard.png | 0
scripts/check-logout-browser.cjs | 43 +
src/views/wls/returnsales/data.ts | 5
src/main.ts | 25
src/views/mes/qc/oqc/data.ts | 18
src/views/mes/pro/task/modules/auto-schedule.vue | 180 ++++
src/api/mes/pro/workorder/index.ts | 2
src/views/mes/md/client/data.ts | 14
src/api/mes/qc/ledger/index.ts | 6
src/api/mes/wm/packages/index.ts | 1
src/views/wls/productsales/data.ts | 5
src/views/mes/qc/rqc/data.ts | 4
scripts/login-theme-live-qa.cjs | 48 +
src/views/mes/pd/project/modules/document-audit-list.vue | 7
src/api/mes/wm/batch/index.ts | 2
src/api/wls/returnsales/index.ts | 1
src/views/mes/qc/ipqc/data.ts | 18
src/views/mes/qc/indicatorresult/components/form.vue | 41
src/api/request.ts | 20
scripts/spare-management-qa.mjs | 66 +
src/views/mes/dv/repair/modules/form.vue | 10
src/views/wls/productreceipt/modules/line-list.vue | 2
src/views/mes/process-design/route/data.ts | 15
src/preferences.ts | 4
src/views/wls/barcode/components/printer-label.vue | 164 +++
src/views/mes/qc/batchtrace/data.ts | 46 -
src/views/bi/dashboard/modules/RealtimeTable.vue | 164 +++
src/views/mes/dv/spare/modules/records.vue | 65 +
src/views/mes/pd/project/modules/document-audit-form.vue | 10
src/views/mes/pro/workorder-center/index.vue | 59
src/api/mes/qc/rqc/index.ts | 8
src/views/mes/pro/task/index.vue | 54 +
src/views/wls/returnissue/modules/line-list.vue | 2
src/store/auth.ts | 48
src/views/mes/dv/spare/modules/action.vue | 95 ++
scripts/bi-realtime-table-qa.mjs | 104 ++
src/views/wls/packages/data.ts | 15
src/api/mes/pro/route/process/index.ts | 2
src/views/mes/pro/task/data.ts | 17
scripts/one-click-schedule-qa.mjs | 85 +
src/packages/effects/request/src/request-client/request-client.ts | 2
scripts/check-logout.cjs | 69 +
src/views/mes/dv/spare-record/index.vue | 6
src/api/mes/qc/oqc/index.ts | 8
src/packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue | 9
src/views/wls/transfer/modules/line-list.vue | 3
src/packages/effects/request/src/request-client/preset-interceptors.ts | 13
DESIGN.md | 72 +
src/views/mes/dv/spare/index.vue | 64 +
src/views/wls/batch/data.ts | 5
src/bootstrap.ts | 1
src/views/mes/md/client/components/select.vue | 1
src/views/mes/process-design/route/modules/workbench.vue | 5
src/views/mes/dv/spare/modules/form.vue | 47 +
src/packages/effects/common-ui/src/ui/authentication/login.vue | 1
src/layouts/auth.vue | 36
src/styles/herbal-theme.css | 59 +
src/views/mes/qc/indicatorresult/components/list.vue | 18
102 files changed, 2,047 insertions(+), 507 deletions(-)
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..72684a3
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,72 @@
+---
+name: 鑽爞涓婚
+description: 鏈辩孩銆佹殩閲戜笌瀹g焊鐧芥瀯鎴愮殑鐧诲綍鍦烘櫙鍙婂簲鐢ㄤ富棰�
+colors:
+ primary: "#a51d24"
+ primary-dark: "hsl(357 64% 73%)"
+ paper: "hsl(40 50% 99%)"
+ paper-deep: "hsl(37 33% 95%)"
+ ink: "hsl(27 16% 17%)"
+ border: "hsl(33 25% 86%)"
+ dark-background: "hsl(26 16% 12%)"
+ dark-card: "hsl(26 15% 14%)"
+ dark-foreground: "hsl(35 30% 90%)"
+ frame-gold: "#cda878"
+typography:
+ display:
+ fontFamily: '"STKaiti", "KaiTi", "FangSong", serif'
+ fontWeight: 700
+ body:
+ fontFamily: "var(--font-family, system-ui, sans-serif)"
+rounded:
+ control: "9px"
+components:
+ login-button:
+ backgroundColor: "{colors.primary}"
+ textColor: "#ffffff"
+ rounded: "{rounded.control}"
+---
+
+# Design System: 鑽爞涓婚
+
+## Overview
+
+**Creative North Star: "瀹g焊涓婄殑鑽爞"**
+
+浠ュ凡纭鐨勬湵绾€�佹殩閲戙�佸绾哥櫧涓庤嵂鍫傚涵闄㈠弬鑰冧负渚濇嵁銆傜櫥褰曞満鏅敤鑽潗涓庡缓绛戞壙杞芥皵姘涳紝琛ㄥ崟淇濇寔娓呮櫚锛涘簲鐢ㄥ唴閮ㄦ部鐢ㄧ幇鏈夌粍浠朵笌甯冨眬锛岄�氳繃鍏ㄥ眬璇箟鑹蹭紶閫掑悓涓�瑙嗚韬唤銆傜櫥褰曢〉鐨勫爞璁�佽楗版涓庡涵闄㈡瀯鍥句笉鎺ㄥ箍鍒颁笟鍔¢〉闈€��
+
+## Colors
+
+鏈辩孩鎵挎媴涓昏鎿嶄綔涓庨�変腑鎬侊紝鏆栭噾鐢ㄤ簬鐧诲綍鍦烘櫙鐨勭粏妗嗗拰瑁呴グ锛涙殩鐧戒笌澧ㄨ壊鎵挎媴鏃ュ父闃呰銆俙src/preferences.ts` 瀹氫箟榛樿涓昏壊锛宍src/styles/herbal-theme.css` 瀹氫箟鏄庢殫涓�ц壊锛涗富鑹茶壊闃剁敱鍋忓ソ绯荤粺鐢熸垚銆�
+
+**The 鐢ㄦ埛鍋忓ソ Rule.** 棣栨涓婚鍗囩骇杩佺Щ鍒伴粯璁ゆ湵绾紝姝ゅ悗淇濈暀鐢ㄦ埛閫夋嫨鐨勪富鑹蹭笌鏄庢殫妯″紡銆備寒鏈辩爞鏆楄壊涓昏壊瑕嗙洊鍙湪褰撳墠涓昏壊绛変簬榛樿鏈辩孩鏃跺惎鐢ㄣ�傝縼绉荤粏鑺備笌瀛樺偍闄愬埗瑙乕鐧诲綍涓婚璁板綍](../docs/design/login-herbal-theme.md)銆�
+
+## Typography
+
+姝f枃缁ф壙搴旂敤瀛椾綋鍙橀噺涓庣郴缁熸棤琛嚎鍥為��锛屼繚鎸佽〃鍗曞拰涓氬姟鏁版嵁鍙銆傚叕鍙稿悕绉般�佸爞璁噰鐢ㄦ湰鏈烘シ浣擄紡浠垮畫鍥為��锛屼笉涓嬭浇瀛椾綋锛涗笉鍚屾搷浣滅郴缁熺殑瀛楀舰浼氭湁宸紓銆傜櫥褰曟爣棰樹娇鐢ㄦ鏂囦綋绯荤殑绮椾綋锛岄伩鍏嶆妸瑁呴グ瀛椾綋鐢ㄤ簬杈撳叆鍐呭銆�
+
+## Layout
+
+涓氬姟鐣岄潰淇濈暀鏃㈡湁甯冨眬瀵嗗害涓庣粍浠剁粨鏋勩�傜櫥褰曟闈㈤噰鐢ㄥ乏渚у爞璁�佸彸渚ц〃鍗曪紝瀹藉睆闄愬埗琛ㄥ崟瀹藉害锛涘皬浜庣瓑浜� 900px 鏀逛负鍗曞垪琛ㄥ崟骞堕殣钘忓爞璁紝灏忎簬绛変簬 540px 鏀剁揣杈硅窛銆佸厑璁稿搧鐗屽尯鍩熸崲琛屻�傝瑷�鍜屼富棰樺伐鍏蜂繚鐣欏湪椤甸銆傜煭妗岄潰瑙嗗彛闄嶄綆琛ㄥ崟鏈�灏忛珮搴﹀拰鍐呰竟璺濄��
+
+## Elevation & Depth
+
+搴櫌鍥剧墖浣嶄簬鐧诲綍鍐呭涓嬪眰锛涙祬鑹茶〃鍗曠敤鎺ヨ繎涓嶉�忔槑鐨勭焊鐧藉簳涓庢煍鍜屾殩鑹查槾褰辨彁渚涢槄璇诲眰銆傛殫鑹查檷浣庡浘鐗囦寒搴︿笌楗卞拰搴︼紝琛ㄥ崟鏀圭敤娣辨搴曘�傚簲鐢ㄥ唴閮ㄤ緷璧栫幇鏈夊崱鐗囥�佹诞灞傝涔夎壊锛屼笉杩藉姞鍦烘櫙瑁呴グ銆�
+
+## Shapes
+
+鐧诲綍杈撳叆妗嗕笌涓绘寜閽娇鐢ㄨ交鍦嗚锛涜〃鍗曞澹充娇鐢ㄦ洿瀹界殑鍦嗚锛屾殩閲戠粏绾垮拰鍥涜绾规牱浠呭睘浜庤璇佸満鏅�傛绾夸笌绾规牱涓鸿楗帮紝涓嶆壙杞戒氦浜掋��
+
+## Components
+
+璁よ瘉甯冨眬缁х画娓叉煋鐪熷疄璺敱锛屼繚鐣欑幇鏈� `AuthenticationLogin`銆佽〃鍗曟牎楠屻�佸姞杞芥�併�侀獙璇佺爜涓庤璇� store 璋冪敤銆傝緭鍏ユ敮鎸佽处鍙凤紡褰撳墠瀵嗙爜鑷姩濉厖锛涘彲瑙佺劍鐐圭幆鏄庣‘瀹氫綅閿洏鎿嶄綔銆傚瘑鐮佸垏鎹负鏄庢枃鍚庝粛淇濈暀鏄鹃殣鎸夐挳鐨勫彸渚х┖闂达紙44px锛夛紝閬垮厤鏂囧瓧涓庢寜閽噸鍙犮�傛殫鑹插伐鍏锋潯閲囩敤娣辫壊搴曘�傛爣棰樻梺浣跨敤鏃犳枃瀛楀嚑浣� SVG 绾规牱銆�
+
+**The 鍏徃韬唤 Rule.** 鍏徃鍚嶇О銆丩ogo銆佺櫥褰曟爣棰樺拰鎻忚堪浠嶈鍙栫幇鏈夐厤缃�傝嵂鍫傝儗鏅笌瑁呴グ灞炰簬涓婚锛屼笉鑳芥浛浠e叕鍙� Logo锛涙湭閰嶇疆鍏徃鏍囧織鏃堕殣钘忔鏋剁ず渚� Logo銆�
+
+## Do's and Don'ts
+
+- 浣跨敤璇箟鑹蹭笌鐜版湁鍋忓ソ绯荤粺鎵╁睍涓氬姟椤甸潰锛涗笟鍔$姸鎬佽壊淇濈暀鍘熸湁鍚箟銆�
+- 淇濈暀鍏徃鍔ㄦ�侀厤缃�佺湡瀹炶璇侀摼璺笌鐢ㄦ埛鍚庣画涓婚鍋忓ソ銆�
+- 涓嶆妸鑳屾櫙涓殑鑽爞鍦烘櫙鎴栧嚑浣曡楗拌В閲婁负浼佷笟璁よ瘉銆佸晢鏍囨垨浜у搧璧勮川銆�
+- 涓嶅湪鐢熸垚鐨勮儗鏅腑缁樺埗鏂囧瓧銆丩ogo銆佽〃鍗曟垨鍏朵粬 UI銆�
+- 涓嶆妸 mock 鎴浘楠岃瘉鎻忚堪涓虹湡瀹炲悗绔櫥褰曟垚鍔熴��
diff --git a/public/images/auth/herbal-courtyard.png b/public/images/auth/herbal-courtyard.png
new file mode 100644
index 0000000..4e3f236
--- /dev/null
+++ b/public/images/auth/herbal-courtyard.png
Binary files differ
diff --git a/scripts/bi-realtime-table-qa.mjs b/scripts/bi-realtime-table-qa.mjs
new file mode 100644
index 0000000..8a2ade2
--- /dev/null
+++ b/scripts/bi-realtime-table-qa.mjs
@@ -0,0 +1,104 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import http from 'node:http';
+import path from 'node:path';
+import { createRequire } from 'node:module';
+import { fileURLToPath } from 'node:url';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const require = createRequire(path.join(root, 'package.json'));
+const { parse, compileScript } = require('vue/compiler-sfc');
+const ts = require('typescript');
+const { chromium } = require('C:/Users/浜�/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
+const source = fs.readFileSync(path.join(root, 'src/views/bi/dashboard/modules/RealtimeTable.vue'), 'utf8');
+const { descriptor } = parse(source);
+const script = compileScript(descriptor, { id: 'qa', inlineTemplate: true });
+const module = ts.transpileModule(script.content, {
+ compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext },
+}).outputText.replace(/from ['"]vue['"]/g, 'from "/vue.js"');
+const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>BI 婊氬姩楠岃瘉</title>
+<style>body{background:#020817;color:#ebf0fc;font-family:Microsoft YaHei,sans-serif;margin:24px}h1{font-size:18px}main{max-width:1100px;margin:auto}.card{padding:16px;background:#0a1834;border:1px solid #153554;border-radius:8px} ${descriptor.styles.map(s => s.content).join('\n')}</style>
+<main><h1>鏈�杩戝叆搴撹褰�</h1><div class="card"><div id="app"></div></div></main>
+<script type="module">
+import {createApp, h, ref} from '/vue.js';import Table from '/table.js';
+const rows=ref([]), height=ref(180), show=ref(true); const makeRows=n=>Array.from({length:n},(_,i)=>({'鍏ュ簱鍗曞彿':'RK-'+String(i+1).padStart(4,'0'),'浜у搧鍚嶇О':'楠岃瘉浜у搧 '+(i+1),'浠撳簱':'鍗楅�氫粨','鏁伴噺':100+i}));
+rows.value=makeRows(20); window.qa={setRows:n=>rows.value=makeRows(n), refresh:()=>rows.value=[...rows.value], setHeight:n=>height.value=n, hide:()=>show.value=false};
+createApp({render:()=>show.value?h(Table,{rows:rows.value,maxHeight:height.value,speed:30}):null}).mount('#app');
+</script></html>`;
+const vueFile = path.join(path.dirname(require.resolve('vue/package.json')), 'dist/vue.esm-browser.js');
+const server = http.createServer((request, response) => {
+ response.setHeader('Content-Type', request.url === '/' ? 'text/html; charset=utf-8' : 'text/javascript; charset=utf-8');
+ response.end(request.url === '/table.js' ? module : request.url === '/vue.js' ? fs.readFileSync(vueFile) : html);
+});
+await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+let browser;
+const errors = [];
+const output = path.join(root, '../scripts/bi-realtime-scroll-20261014');
+fs.mkdirSync(output, { recursive: true });
+try {
+ browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe' });
+ const page = await browser.newPage({ viewport: { width: 1440, height: 800 } });
+ page.on('pageerror', error => errors.push(error.message));
+ await page.goto(`http://127.0.0.1:${server.address().port}`);
+ await page.waitForFunction(() => document.querySelector('.table-track')?.getAnimations().length === 1);
+ const offset = () => page.locator('.table-track').evaluate(el => new DOMMatrix(getComputedStyle(el).transform).m42);
+ const top = await page.locator('.header-table').evaluate(el => el.getBoundingClientRect().top);
+ const first = await offset();
+ await page.waitForTimeout(500);
+ assert.ok((await offset()) < first - 8, 'Rows must move upwards');
+ assert.equal(await page.locator('.header-table').evaluate(el => el.getBoundingClientRect().top), top, 'Header must stay fixed');
+ await page.locator('.realtime-table').hover();
+ const paused = await offset();
+ await page.waitForTimeout(250);
+ assert.ok(Math.abs((await offset()) - paused) < 1, 'Hover must pause');
+ await page.mouse.move(0, 0);
+ await page.waitForTimeout(200);
+ assert.ok((await offset()) < paused - 3, 'Mouse leave must resume');
+ const refreshOffset = await offset();
+ await page.evaluate(() => window.qa.refresh());
+ await page.waitForTimeout(100);
+ assert.ok((await offset()) < refreshOffset, 'Refresh must preserve scroll position');
+ await page.locator('.table-track').evaluate(el => {
+ const animation = el.getAnimations()[0];
+ animation.currentTime = Number(animation.effect.getTiming().duration) - 200;
+ });
+ await page.waitForTimeout(400);
+ assert.ok((await offset()) > -20, 'Loop must wrap seamlessly');
+ await page.locator('.realtime-table').focus();
+ const focused = await offset();
+ await page.waitForTimeout(100);
+ assert.ok(Math.abs((await offset()) - focused) < 1, 'Focus must pause');
+ await page.keyboard.press('ArrowDown');
+ assert.ok((await offset()) < focused - 35, 'Keyboard must advance rows');
+ await page.evaluate(() => document.activeElement.blur());
+ await page.screenshot({ path: path.join(output, 'desktop.png'), fullPage: true });
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.waitForTimeout(150);
+ assert.ok(await page.locator('.table-track').evaluate(el => el.getAnimations().length === 1), 'Resize must keep one animation');
+ await page.screenshot({ path: path.join(output, 'mobile.png'), fullPage: true });
+ await page.evaluate(() => window.qa.setRows(2));
+ await page.waitForTimeout(150);
+ assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Short table must not scroll');
+ assert.equal(await page.locator('table[aria-hidden="true"]').count(), 0, 'Short table must not duplicate rows');
+ await page.evaluate(() => window.qa.setRows(30));
+ await page.waitForFunction(() => document.querySelector('.table-track').getAnimations().length === 1);
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.waitForTimeout(150);
+ assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Reduced motion must disable autoplay');
+ assert.equal(await page.locator('.table-viewport').evaluate(el => getComputedStyle(el).overflowY), 'auto', 'Reduced motion must allow manual scroll');
+ await page.emulateMedia({ reducedMotion: 'no-preference' });
+ await page.waitForFunction(() => document.querySelector('.table-track').getAnimations().length === 1);
+ await page.evaluate(() => window.qa.setHeight(2000));
+ await page.waitForTimeout(150);
+ assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Larger viewport must stop scrolling when rows fit');
+ await page.evaluate(() => window.qa.hide());
+ await page.waitForTimeout(100);
+ assert.equal(await page.evaluate(() => document.getAnimations().length), 0, 'Unmount must clean up animations');
+ assert.deepEqual(errors, []);
+ const result = { passed: true, checks: ['continuous scroll', 'fixed header', 'hover pause/resume', 'refresh continuity', 'seamless loop', 'keyboard navigation', 'responsive resize', 'short data', 'reduced motion', 'unmount cleanup'], screenshots: ['desktop.png', 'mobile.png'] };
+ fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2));
+ console.log(JSON.stringify(result));
+} finally {
+ await browser?.close();
+ server.close();
+}
diff --git a/scripts/check-logout-browser.cjs b/scripts/check-logout-browser.cjs
new file mode 100644
index 0000000..b687c81
--- /dev/null
+++ b/scripts/check-logout-browser.cjs
@@ -0,0 +1,43 @@
+// Real user dropdown, confirmation and router; all APIs simulated.
+const assert=require('node:assert/strict'),fs=require('fs'),path=require('path');
+const {chromium}=require('C:/Users/浜�/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
+(async()=>{
+ const server=await(await import('vite')).createServer({root:path.resolve(__dirname,'..'),server:{host:'127.0.0.1',port:5183}});await server.listen();
+ const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'});
+ const page=await browser.newPage({viewport:{width:1440,height:950},reducedMotion:'reduce'});const errors=[];let logoutCalls=0;let releaseLogout;let logoutHeader;
+ page.on('pageerror',e=>errors.push(e.message));
+ await page.route('**/admin-api/**',async route=>{
+ const url=route.request().url();
+ if(url.includes('/system/auth/logout')){logoutCalls++;logoutHeader=route.request().headers().authorization;await new Promise(r=>releaseLogout=r);}
+ const data=url.includes('company-config')?{companyName:'閫�鍑洪獙璇佸钩鍙�'}:url.includes('count')?0:url.includes('list')?[]:null;
+ await route.fulfill({contentType:'application/json',body:JSON.stringify({code:0,data,msg:''})}).catch(()=>{});
+ });
+ try{
+ await page.goto('http://127.0.0.1:5183/auth/login',{waitUntil:'domcontentloaded'});await page.getByRole('heading',{name:'娆㈣繋鍥炴潵'}).waitFor();
+ await page.waitForFunction(()=>!document.querySelector('#__app-loading__')||getComputedStyle(document.querySelector('#__app-loading__')).opacity==='0');
+ await page.evaluate(async()=>{
+ const stores=await import('/src/packages/stores/src/index.ts'),{router}=await import('/src/router/index.ts'),{h}=await import('/node_modules/.vite/deps/vue.js');
+ stores.useAccessStore().setAccessToken('qa-session-token');stores.useAccessStore().setRefreshToken('qa-refresh-token');stores.useAccessStore().setIsAccessChecked(true);
+ stores.useUserStore().setUserInfo({userId:'1',username:'logout-qa',nickname:'閫�鍑洪獙璇佺敤鎴�',avatar:''});
+ router.addRoute('Root',{name:'LogoutQa',path:'/qa-logout-session',meta:{title:'閫�鍑洪獙璇佸伐浣滃尯'},component:{render(){return h('div','閫�鍑洪獙璇佸伐浣滃尯')}}});
+ await router.push('/qa-logout-session');
+ });
+ await page.locator('[id="__vben_main_content"]').getByText('閫�鍑洪獙璇佸伐浣滃尯',{exact:true}).waitFor({timeout:60000});
+ const avatars=page.locator('button[aria-haspopup="menu"]').filter({has:page.locator('.size-8')}).first();
+ await avatars.click();
+ await page.getByRole('menuitem',{name:/閫�鍑虹櫥褰�/}).click();
+ const dialog=page.getByRole('dialog');await dialog.waitFor();
+ const started=Date.now();await dialog.getByRole('button',{name:/^纭甛s*[璁ゅ畾]$/}).click();
+ await page.waitForURL('**/auth/login',{timeout:5000});await page.getByRole('heading',{name:'娆㈣繋鍥炴潵'}).waitFor();
+ const elapsed=Date.now()-started;
+ const state=await page.evaluate(async()=>{const s=await import('/src/packages/stores/src/index.ts');return{access:s.useAccessStore().accessToken,refresh:s.useAccessStore().refreshToken,user:s.useUserStore().userInfo}});
+ assert.equal(state.access,null);assert.equal(state.refresh,null);assert.equal(state.user,null);
+ assert.equal(logoutCalls,1);assert.equal(logoutHeader,'Bearer qa-session-token');assert.equal(typeof releaseLogout,'function','Server logout must still be pending when login page appears');
+ releaseLogout();await page.waitForTimeout(100);
+ const result={passed:true,scope:'Real application user dropdown, confirmation, stores and routing; simulated authenticated session and pending backend, no real account login',checks:['click logout and confirm','navigate without refresh while server pending','access and refresh token cleared','user info cleared','server revoke uses original token'],elapsedMs:elapsed,errors};
+ assert.deepEqual(errors,[]);
+ const out=path.resolve(__dirname,'../../scripts/logout-fix-20261015');fs.mkdirSync(out,{recursive:true});fs.writeFileSync(path.join(out,'browser-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));
+ }catch(error){console.error(JSON.stringify({message:error.message,errors,body:(await page.locator('body').innerText()).slice(0,1800)}));throw error;}
+ finally{releaseLogout?.();await browser.close();await server.close();}
+})();
+
diff --git a/scripts/check-logout.cjs b/scripts/check-logout.cjs
index 6b42309..6506f0f 100644
--- a/scripts/check-logout.cjs
+++ b/scripts/check-logout.cjs
@@ -62,7 +62,8 @@
'vue-router': { useRouter: () => ({ currentRoute: vue.ref({ fullPath: '/analytics?tab=1' }), replace: async (to) => navigations.push(to) }) },
'@vben/constants': { LOGIN_PATH: '/auth/login' }, '@vben/preferences': {},
'@vben/stores': { ...setup, ...accessModule, ...userModule }, 'ant-design-vue': {}, '#/locales': {},
- '#/api': { logoutApi: async (token) => { apiCalls.push(token); if (apiFails) throw new Error('offline'); } },
+ '#/store/audit-policy': { automaticApproval: vue.ref(false), loadAuditPolicy: async () => {} },
+ '#/api': { logoutApi: async (token) => { apiCalls.push(token); if (apiFails === 'pending') return new Promise(() => {}); if (apiFails) throw new Error('offline'); } },
}).useAuthStore();
const errors = [];
const originalError = console.error;
@@ -90,10 +91,74 @@
assert.equal(rtc.getGroupCall(1), undefined);
assert.equal(errors.length, resetFails ? 1 : 0);
}
+
+async function sessionFixture(api) {
+ const app=vue.createApp({});
+ const setup=load('src/packages/stores/src/setup.ts',{'secure-ls':class{},'pinia-plugin-persistedstate':{createPersistedState:()=>()=>{}}});
+ pinia.setActivePinia(await setup.initStores(app,{namespace:'logout-races'}));
+ const accessModule=load('src/packages/stores/src/modules/access.ts'),userModule=load('src/packages/stores/src/modules/user.ts');
+ const access=accessModule.useAccessStore(),user=userModule.useUserStore();const navigations=[];
+ access.setAccessToken('old-access');access.setRefreshToken('old-refresh');
+ const auth=load('src/store/auth.ts',{
+ 'vue-router':{useRouter:()=>({currentRoute:vue.ref({fullPath:'/private'}),replace:async to=>navigations.push(to),push:async to=>navigations.push(to)})},
+ '@vben/constants':{LOGIN_PATH:'/auth/login'},'@vben/preferences':{},
+ '@vben/stores':{...setup,...accessModule,...userModule},'ant-design-vue':{notification:{success(){throw Error('Stale success notification')}}},'#/locales':{$t:v=>v},
+ '#/store/audit-policy':{automaticApproval:vue.ref(false),loadAuditPolicy:async()=>{}},
+ '#/api':{logoutApi:async()=>{},...api},
+ }).useAuthStore();
+ return{access,user,auth,navigations};
+}
+function deferred(){let resolve,reject;const promise=new Promise((a,b)=>{resolve=a;reject=b});return{promise,resolve,reject};}
+async function pendingLoginCannotRestoreSession(){
+ const pending=deferred();const f=await sessionFixture({loginApi:()=>pending.promise});
+ const login=f.auth.authLogin('username',{});await f.auth.logout(false);
+ pending.resolve({accessToken:'late-access',refreshToken:'late-refresh'});await login;
+ assert.equal(f.access.accessToken,null);assert.equal(f.access.refreshToken,null);assert.equal(f.navigations.length,1);
+}
+async function pendingUserInfoCannotRestorePrivateData(){
+ const pending=deferred();const f=await sessionFixture({getAuthPermissionInfoApi:()=>pending.promise});
+ const info=f.auth.fetchUserInfo();const rejected=assert.rejects(info,/鐧诲綍鐘舵�佸凡鍙樺寲/);
+ await f.auth.logout(false);pending.resolve({user:{nickname:'old-user'},roles:['old-role'],menus:[],permissions:['old-permission']});await rejected;
+ assert.equal(f.user.userInfo,null);assert.deepEqual(f.access.accessCodes,[]);
+}
+async function requestRaces(){
+ const f=await sessionFixture({});const pending=deferred();let hooks;
+ class Client{responses=[];requests=[];isRefreshing=false;refreshTokenQueue=[];addRequestInterceptor(x){this.requests.push(x)}addResponseInterceptor(x){this.responses.push(x)}}
+ const presets=load('src/packages/effects/request/src/request-client/preset-interceptors.ts',{
+ '../../../../locales/src':{$t:v=>v},'../../../../utils/src':{isFunction:v=>typeof v==='function'},axios:{isCancel:()=>false},
+ });
+ const request=load('src/api/request.ts',{
+ '@vben/hooks':{isTenantEnable:()=>true,useAppConfig:()=>({apiURL:'/mock'})},
+ '@vben/preferences':{preferences:{app:{enableRefreshToken:true,loginExpiredMode:'page'}}},
+ '@vben/request':{RequestClient:Client,defaultResponseInterceptor:()=>({}),errorMessageResponseInterceptor:()=>({}),authenticateResponseInterceptor:options=>{hooks=options;return presets.authenticateResponseInterceptor(options)}},
+ '@vben/stores':{useAccessStore:()=>f.access},'@vben/utils':{createApiEncrypt:()=>({})},
+ 'ant-design-vue':{message:{}},'#/store':{useAuthStore:()=>f.auth},'./core':{refreshTokenApi:()=>pending.promise},
+ });
+ const refresh=hooks.doRefreshToken();const rejected=assert.rejects(refresh,/鐧诲綍鐘舵�佸凡鍙樺寲/);
+ await f.auth.logout(false);pending.resolve({data:{data:{accessToken:'late-refreshed'}}});await rejected;assert.equal(f.access.accessToken,null);
+ f.access.setAccessToken('new-account-token');f.access.setRefreshToken('new-account-refresh');
+ await hooks.doReAuthenticate({config:{headers:{Authorization:'Bearer old-access'}}});assert.equal(f.access.accessToken,'new-account-token');
+ const cfg={headers:{'tenant-id':7,'visit-tenant-id':9}};
+ await request.baseRequestClient.requests[0].fulfilled(cfg);assert.equal(cfg.headers['tenant-id'],7);assert.equal(cfg.headers['visit-tenant-id'],9);
+
+ const client={isRefreshing:false,refreshTokenQueue:[],request(){throw Error('Failed refresh must not replay queued requests')}};
+ const failed=deferred();let reauthError;const authError={config:{url:'/private',headers:{Authorization:'Bearer old'}},response:{status:401}};
+ const interceptor=presets.authenticateResponseInterceptor({client,doRefreshToken:()=>failed.promise,doReAuthenticate:async e=>{reauthError=e},enableRefreshToken:true,formatToken:t=>t});
+ const first=interceptor.rejected(authError),firstRejected=assert.rejects(first,/refresh failed/);
+ const second=interceptor.rejected({config:{url:'/other',headers:{}},response:{status:401}}),secondRejected=assert.rejects(second,/refresh failed/);
+ const originalError=console.error;console.error=()=>{};
+ try{failed.reject(new Error('refresh failed'));await Promise.all([firstRejected,secondRejected]);}finally{console.error=originalError;}
+ assert.equal(reauthError,authError);assert.equal(client.isRefreshing,false);assert.equal(client.refreshTokenQueue.length,0);
+}
+
(async () => {
await scenario(false, false, false);
await scenario(true, false, true);
await scenario(false, true, false);
await scenario(true, true, false);
- console.log('PASS: logout redirects and clears session with API success/failure and store reset failures; company and IM stores reset');
+ await scenario('pending', false, false);
+ await pendingLoginCannotRestoreSession();
+ await pendingUserInfoCannotRestorePrivateData();
+ await requestRaces();
+ console.log('PASS: pending APIs do not block logout, stale login/user/refresh cannot restore sessions, failed refresh queue rejects; logout redirects and clears session with API success/failure and store reset failures; company and IM stores reset');
})();
diff --git a/scripts/login-theme-live-qa.cjs b/scripts/login-theme-live-qa.cjs
new file mode 100644
index 0000000..9c3c21a
--- /dev/null
+++ b/scripts/login-theme-live-qa.cjs
@@ -0,0 +1,48 @@
+// Actual application/router/forms, simulated APIs, no real authentication.
+const assert=require('node:assert/strict');
+const {chromium}=require('C:/Users/浜�/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
+const fs=require('fs'),path=require('path');
+const output=path.resolve(__dirname,'../../scripts/login-theme-20261015');fs.mkdirSync(output,{recursive:true});
+(async()=>{
+ const server=await(await import('vite')).createServer({root:path.resolve(__dirname,'..'),server:{host:'127.0.0.1',port:5182}});await server.listen();
+ const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'});
+ const page=await browser.newPage({viewport:{width:1664,height:941},reducedMotion:'reduce'});
+ const errors=[];let authPosts=0;page.on('pageerror',e=>errors.push(e.message));
+ await page.route('**/admin-api/**',async route=>{
+ const url=route.request().url();if(route.request().method()==='POST')authPosts++;
+ const data=url.includes('company-config')?{companyName:'婊嬭ˉ鍝佺鐞嗗钩鍙�',loginTitle:'婊嬭ˉ鍝佽涓氭暟瀛楀寲绠$悊骞冲彴',loginDescription:'浼犳壙涓尰鑽簿鍗� 路 鏈嶅姟鍋ュ悍缇庡ソ鐢熸椿'}:url.includes('tenant')?[]:null;
+ await route.fulfill({contentType:'application/json',body:JSON.stringify({code:0,data,msg:''})});
+ });
+ try {
+ await page.goto('http://127.0.0.1:5182/auth/login',{waitUntil:'domcontentloaded',timeout:60000});
+ await page.getByRole('heading',{name:'娆㈣繋鍥炴潵'}).waitFor({timeout:60000});
+ await page.waitForFunction(()=>!document.querySelector('#__app-loading__')||getComputedStyle(document.querySelector('#__app-loading__')).opacity==='0');
+ await page.waitForFunction(()=>document.querySelector('.herbal-auth__background')?.complete);
+ const user=page.getByRole('textbox',{name:'璐﹀彿',exact:true}),password=page.locator('input[autocomplete="current-password"]');
+ await user.fill('婕旂ず璐︽埛');await password.fill('not-a-real-password');await page.locator('.herbal-login__heading').click();await page.waitForTimeout(400);
+ assert.equal(await user.getAttribute('autocomplete'),'username');assert.equal(await password.getAttribute('type'),'password');
+ await page.getByRole('button',{name:'鏄剧ず瀵嗙爜',exact:true}).focus();await page.keyboard.press('Space');assert.equal(await password.getAttribute('type'),'text');
+ assert.equal(await password.evaluate(e=>getComputedStyle(e).paddingRight),'44px');
+ await page.getByRole('button',{name:'闅愯棌瀵嗙爜',exact:true}).click();assert.equal(await password.getAttribute('type'),'password');
+ const remember=page.locator('button[role="checkbox"]').first();await remember.click();assert.equal(await remember.getAttribute('aria-checked'),'true');
+ await page.screenshot({path:path.join(output,'desktop-login.png'),fullPage:true});
+ const tokens=await page.evaluate(()=>{const s=getComputedStyle(document.documentElement);return{brand:document.documentElement.dataset.brandTheme,primary:s.getPropertyValue('--primary').trim(),background:s.getPropertyValue('--background-deep').trim()}});
+ assert.equal(tokens.brand,'herbal');assert.equal(tokens.background,'37 33% 95%');
+ await page.locator('.herbal-auth__toolbar button').last().click();await page.waitForFunction(()=>document.documentElement.classList.contains('dark'));await page.waitForTimeout(350);
+ assert.equal(await page.evaluate(()=>getComputedStyle(document.documentElement).getPropertyValue('--primary').trim()),'357 64% 73%');
+ assert.equal(await page.locator('.herbal-panel').evaluate(e=>getComputedStyle(e).backgroundColor),'rgba(39, 31, 26, 0.97)');
+ assert.equal(await page.locator('.herbal-auth__toolbar >div').evaluate(e=>getComputedStyle(e).backgroundColor),'rgb(48, 39, 31)');
+ assert.equal(await page.evaluate(()=>getComputedStyle(document.documentElement).filter),'none','Dark styles must not apply a filter to the document');
+ await page.screenshot({path:path.join(output,'desktop-dark.png'),fullPage:true});
+ await page.locator('.herbal-auth__toolbar button').last().click();await page.waitForFunction(()=>!document.documentElement.classList.contains('dark'));
+ await page.setViewportSize({width:390,height:844});await page.waitForTimeout(250);
+ assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);
+ await page.screenshot({path:path.join(output,'mobile-login.png'),fullPage:true});
+ await user.fill('');await password.fill('');await page.getByRole('button',{name:'login',exact:true}).click();
+ await page.getByText('璇疯緭鍏ョ敤鎴峰悕',{exact:true}).first().waitFor();assert.equal(authPosts,0);
+ assert.deepEqual(errors,[]);
+ const result={passed:true,scope:'Real application/router/theme/login components; mocked backend and company APIs, demo account, no real authentication',checks:['dynamic company config','background loaded','autocomplete','keyboard password toggle','remember account','light theme variables','dark theme switching','mobile no overflow','required fields block backend'],tokens,screenshots:['desktop-login.png','desktop-dark.png','mobile-login.png']};
+ fs.writeFileSync(path.join(output,'result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));
+ } catch(error){console.error(JSON.stringify({message:error.message,errors}));throw error;}
+ finally{await browser.close();await server.close();}
+})();
diff --git a/scripts/one-click-schedule-qa.mjs b/scripts/one-click-schedule-qa.mjs
new file mode 100644
index 0000000..91a1a07
--- /dev/null
+++ b/scripts/one-click-schedule-qa.mjs
@@ -0,0 +1,85 @@
+// Component interaction check with simulated API data; never creates production tasks.
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { createRequire } from 'node:module';
+import { fileURLToPath } from 'node:url';
+import { createServer } from 'vite';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const require = createRequire(path.join(root, 'package.json'));
+const { parse, compileScript } = require('vue/compiler-sfc');
+const ts = require('typescript');
+const { chromium } = require('C:/Users/浜�/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
+const output = path.resolve(root, '../scripts/one-click-schedule-20261014');
+fs.mkdirSync(output, { recursive: true });
+const modal = `import {defineComponent,h,reactive,onMounted} from 'vue';
+export function useVbenModal(options){const state=reactive({open:true,locked:false,confirmDisabled:true,...options});
+const api={getData:()=>({workOrderIds:[1,2]}),setState:v=>Object.assign(state,v),lock:()=>state.locked=true,unlock:()=>state.locked=false,close:async()=>{if(options.onBeforeClose&&!options.onBeforeClose())return;state.open=false;await options.onOpenChange(false)}};
+window.qaModal={state,api,options};onMounted(()=>options.onOpenChange(true));
+const Modal=defineComponent({props:['title'],setup(props,{slots}){return()=>state.open?h('section',{class:'qa-modal'},[
+h('header',props.title),h('div',{class:'qa-content'},slots.default?.()),h('footer',[
+h('button',{class:'qa-close',disabled:state.locked,onClick:()=>api.close()},'鍙栨秷'),
+h('button',{class:'qa-confirm',disabled:state.locked||state.confirmDisabled,onClick:()=>options.onConfirm()},state.locked?'姝e湪鎺掍骇鈥�':state.confirmText)])]):null}});return[Modal,api]}`;
+const api = `export async function previewBatchSchedule(request){window.qa.previewCalls++;await new Promise(r=>setTimeout(r,120));
+const blocked=window.qa.mode==='blocked',late=window.qa.mode==='late';return {canExecute:!blocked,previewHash:'test-hash',previewTime:'2026-10-14 15:00:00',warnings:late?['宸ュ崟WO-001棰勮瀹屾垚鏃堕棿鏅氫簬浜ゆ湡']:[],
+orders:[{workOrderId:1,workOrderCode:'WO-001',quantity:100,taskCount:blocked?0:2,requestDate:'2026-10-16 00:00:00',endTime:'2026-10-16 12:00:00',late,error:blocked?'WO-001锛氬伐搴忔垚鍨嬫病鏈夊彲鐢ㄥ伐浣滅珯鍙婃湁鏁堣澶囦骇鑳斤紝璇峰厛缁存姢':undefined},{workOrderId:2,workOrderCode:'WO-002',quantity:200,taskCount:blocked?0:2,requestDate:'2026-10-19 00:00:00',endTime:'2026-10-19 13:00:00',late:false,error:blocked?'WO-002锛氬伐搴忓寘瑁呯己灏戣澶囦骇鑳斤紝璇峰厛缁存姢':undefined}],
+tasks:blocked?[]:[{workOrderId:1,workOrderCode:'WO-001',processId:1,processName:'鎴愬瀷',workstationName:'鎴愬瀷宸ヤ綔绔橝',machineryNames:'鎴愬瀷璁惧A01',quantity:100,hourlyCapacity:50,duration:2,startTime:'2026-10-16 09:00:00',endTime:'2026-10-16 11:00:00'},{workOrderId:1,workOrderCode:'WO-001',processId:2,processName:'鍖呰',workstationName:'鍖呰宸ヤ綔绔橞',machineryNames:'鍖呰璁惧B01',quantity:100,hourlyCapacity:100,duration:1,startTime:'2026-10-16 11:00:00',endTime:'2026-10-16 12:00:00'},{workOrderId:2,workOrderCode:'WO-002',processId:1,processName:'鎴愬瀷',workstationName:'鎴愬瀷宸ヤ綔绔橝',machineryNames:'鎴愬瀷璁惧A01',quantity:200,hourlyCapacity:100,duration:2,startTime:'2026-10-19 09:00:00',endTime:'2026-10-19 11:00:00'},{workOrderId:2,workOrderCode:'WO-002',processId:2,processName:'鍖呰',workstationName:'鍖呰宸ヤ綔绔橞',machineryNames:'鍖呰璁惧B01',quantity:200,hourlyCapacity:100,duration:2,startTime:'2026-10-19 11:00:00',endTime:'2026-10-19 13:00:00'}]};}
+export async function executeBatchSchedule(request){window.qa.executeCalls++;window.qa.lastRequest=request;await new Promise(r=>setTimeout(r,200));if(window.qa.mode==='executeError')throw new Error('鎺掍骇鏁版嵁宸插彉鍖栵紝璇烽噸鏂扮敓鎴愰瑙堝悗纭');return{taskIds:[1,2,3,4]}}`;
+const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>涓�閿帓浜т氦浜掗獙璇�</title>
+<style>body{margin:0;background:#f4f6f8;font-family:Microsoft YaHei,sans-serif;color:#172033}.qa-modal{width:calc(100% - 32px);max-width:1200px;margin:20px auto;background:white;border-radius:8px}.qa-modal header{padding:18px 24px;font-size:18px;font-weight:600;border-bottom:1px solid #e8ebee}.qa-content{padding:24px}.qa-modal footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #e8ebee}.qa-modal button{padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:white;cursor:pointer}.qa-modal .qa-confirm{background:#1677ff;color:white;border-color:#1677ff}.qa-modal button:disabled{color:#8c8c8c;background:#f5f5f5;border-color:#d9d9d9;cursor:default}.space-y-5>*+*{margin-top:20px}.flex{display:flex}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-end{align-items:flex-end}.gap-3{gap:12px}.gap-2{gap:8px}.font-medium{font-weight:500}.text-sm{font-size:14px}.text-muted-foreground{color:#657083}.text-destructive{color:#cf1322}.mb-2{margin-bottom:8px}.mt-2{margin-top:8px}h3{font-size:15px;margin-top:0}p{margin:4px 0}@media(max-width:600px){.qa-content{padding:16px}.qa-modal header{padding:16px}.ant-descriptions{overflow-x:auto}}</style>
+<div id="app"></div><script type="module" src="/qa-entry.js"></script></html>`;
+const entry = `import{createApp,h}from'vue';import AutoSchedule from'/src/views/mes/pro/task/modules/auto-schedule.vue';window.qa={mode:new URLSearchParams(location.search).get('mode')||'valid',previewCalls:0,executeCalls:0,success:0};createApp({render:()=>h(AutoSchedule,{onSuccess:()=>window.qa.success++})}).mount('#app')`;
+
+const server = await createServer({ root, configFile: false, appType: 'custom', cacheDir: 'node_modules/.cache/one-click-schedule-qa', server: { host: '127.0.0.1', port: 0 },
+ plugins: [{ name: 'schedule-qa', enforce: 'pre',
+ resolveId(id) { if (id === '@vben/common-ui') return '\0qa-modal'; if (id === '#/api/mes/pro/task') return '\0qa-api'; if (id === '/qa-entry.js') return '\0qa-entry'; },
+ load(id) { if (id === '\0qa-modal') return modal; if (id === '\0qa-api') return api; if (id === '\0qa-entry') return entry; },
+ transform(code, id) { if (!id.endsWith('.vue')) return;
+ const { descriptor } = parse(code); const script = compileScript(descriptor, { id: 'schedule-qa', inlineTemplate: true });
+ return { code: ts.transpileModule(script.content, { compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } }).outputText, map: null };
+ },
+ configureServer(server) { server.middlewares.use((request,response,next)=>{if(request.url?.startsWith('/?')||request.url==='/'){response.setHeader('Content-Type','text/html;charset=utf-8');response.end(html)}else next()}); },
+ }], optimizeDeps: { entries: [], noDiscovery: true, include: ['vue', 'ant-design-vue', 'dayjs'] } });
+await server.listen();
+let browser;
+const errors=[];
+try {
+ browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'});
+ const page=await browser.newPage({viewport:{width:1440,height:1000}});
+ page.on('pageerror',error=>errors.push(error.message));
+ const url=`http://127.0.0.1:${server.httpServer.address().port}`;
+ await page.goto(url);await page.getByText('宸ュ崟鏍¢獙',{exact:true}).waitFor();
+ assert.equal(await page.locator('.qa-confirm').isEnabled(),true);
+ assert.equal(await page.evaluate(()=>window.qa.previewCalls),1,'Opening modal must generate exactly one preview');
+ await page.screenshot({path:path.join(output,'desktop-preview.png'),fullPage:true});
+ await page.locator('.ant-picker').click();
+ await page.locator('td[title="2026-10-20"]').click();
+ await page.locator('.ant-picker-ok button').click();
+ await page.waitForTimeout(150);
+ assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Changed earliest time must invalidate preview');
+ await page.getByText('鐢熸垚鎺掍骇棰勮',{exact:true}).click();await page.getByText('宸ュ崟鏍¢獙',{exact:true}).waitFor();
+ await page.locator('.qa-confirm').click();
+ assert.equal(await page.locator('.qa-close').isEnabled(),false,'Cancel must be disabled while executing');
+ await page.waitForFunction(()=>window.qa.success===1);
+ assert.equal(await page.evaluate(()=>window.qa.executeCalls),1,'Confirm must execute only once');
+ await page.goto(url+'/?mode=blocked');await page.getByText('宸ュ崟鏍¢獙',{exact:true}).waitFor();
+ assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Configuration errors must block submission');
+ await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(output,'mobile-blocked.png'),fullPage:true});
+ await page.goto(url+'/?mode=late');await page.getByText('宸ュ崟鏍¢獙',{exact:true}).waitFor();
+ assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Late risk requires acknowledgement');
+ await page.getByText('鎴戝凡鏍稿骞舵帴鍙椾互涓婁氦鏈熷欢鏈熼闄�',{exact:true}).click();
+ assert.equal(await page.locator('.qa-confirm').isEnabled(),true);
+ await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.success===1);
+ assert.equal(await page.evaluate(()=>window.qa.lastRequest.allowLate),true);
+ await page.goto(url+'/?mode=executeError');await page.getByText('宸ュ崟鏍¢獙',{exact:true}).waitFor();
+ await page.locator('.qa-confirm').click();await page.getByText('鎺掍骇鏁版嵁宸插彉鍖栵紝璇烽噸鏂扮敓鎴愰瑙堝悗纭',{exact:true}).waitFor();
+ assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Failed execution must discard old preview');
+ assert.equal(await page.locator('.qa-close').isEnabled(),true,'Failure must unlock modal');
+ assert.deepEqual(errors,[]);
+ const result={passed:true,scope:'Actual scheduling component, simulated APIs and modal adapter; no production writes',checks:['initial preview','time change invalidates preview','single submit','execution locks cancel','missing config blocks','late acknowledgement','server rejection unlocks and invalidates'],screenshots:['desktop-preview.png','mobile-blocked.png']};
+ fs.writeFileSync(path.join(output,'frontend-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));
+} catch (error) {
+ console.error(JSON.stringify({message:error.message,browserErrors:errors}));
+ throw error;
+} finally { await browser?.close();await server.close(); }
diff --git a/scripts/spare-management-qa.mjs b/scripts/spare-management-qa.mjs
new file mode 100644
index 0000000..129909f
--- /dev/null
+++ b/scripts/spare-management-qa.mjs
@@ -0,0 +1,66 @@
+// Actual Vue components with simulated API data; no production stock writes.
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import {createRequire} from 'node:module';
+import {fileURLToPath} from 'node:url';
+import {createServer} from 'vite';
+const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
+const require=createRequire(path.join(root,'package.json'));
+const {parse,compileScript}=require('vue/compiler-sfc'),ts=require('typescript');
+const {chromium}=require('C:/Users/浜�/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
+const output=path.resolve(root,'../scripts/spare-management-20261014');fs.mkdirSync(output,{recursive:true});
+const modal=`import {defineComponent,h,reactive,onMounted} from 'vue';
+export const Page=defineComponent({setup(p,{slots}){return()=>h('main',slots.default?.())}});
+export function useVbenModal(options){
+ if(options.connectedComponent||!options.onOpenChange)return[defineComponent({setup(){return()=>null}}),{setData(){return this},open(){}}];
+ const state=reactive({open:true,locked:false,...options});const api={getData:()=>window.qa.data,setState:v=>Object.assign(state,v),lock:()=>state.locked=true,unlock:()=>state.locked=false,close:async()=>{if(options.onBeforeClose&&!options.onBeforeClose())return;state.open=false;await options.onOpenChange(false)}};
+ window.qaModal={state,api,options};onMounted(()=>options.onOpenChange(true));
+ return[defineComponent({props:['title'],setup(props,{slots}){return()=>state.open?h('section',{class:'qa-modal'},[h('header',props.title),h('div',slots.default?.()),h('footer',[h('button',{class:'qa-close',disabled:state.locked,onClick:()=>api.close()},'鍙栨秷'),h('button',{class:'qa-confirm',disabled:state.locked,onClick:()=>options.onConfirm().catch(()=>{})},'纭')])]):null}}),api];}`;
+const api=`export const movementLabels={1:'鍒濆搴撳瓨',2:'鍏ュ簱',3:'缁翠慨棰嗙敤',4:'閫�鍥�'};
+export async function getSparePage(){return {list:window.qa.parts,total:window.qa.parts.length}}
+export async function getSpare(){return window.qa.parts[0]}
+export async function getSpareMovements(){return {list:window.qa.movements,total:window.qa.movements.length}}
+export async function spareAction(type,data){window.qa.calls++;window.qa.request={type,...data};await new Promise(r=>setTimeout(r,250));if(window.qa.fail)throw Error('server rejection');return 1}
+export async function createSpare(data){window.qa.saved=data;return 3}
+export async function updateSpare(data){window.qa.saved=data}
+export async function deleteSpare(){}
+`;
+const entry=`import {createApp,h} from 'vue';import 'ant-design-vue/dist/reset.css';
+import Ledger from '/src/views/mes/dv/spare/index.vue';import Records from '/src/views/mes/dv/spare/modules/records.vue';import Action from '/src/views/mes/dv/spare/modules/action.vue';import Form from '/src/views/mes/dv/spare/modules/form.vue';
+const mode=new URLSearchParams(location.search).get('mode')||'ledger';
+const parts=[{id:1,code:'SP-001',name:'娣辨矡鐞冭酱鎵�',specification:'6205-2RS',unit:'浠�',location:'澶囦欢搴� A-01',stockQuantity:10,safetyQuantity:15,status:0},{id:2,code:'SP-002',name:'浼犲姩鐨甫',specification:'B-1250',unit:'鏉�',location:'澶囦欢搴� B-02',stockQuantity:25,safetyQuantity:5,status:0}];
+const movement={id:1,sparePartId:1,sparePartCode:'SP-001',sparePartName:'娣辨矡鐞冭酱鎵�',specification:'6205-2RS',unit:'浠�',type:3,quantity:4,returnedQuantity:1,returnableQuantity:3,repairCode:'WX-20261014-001',repairId:1,machineryName:'鏁版帶鍔犲伐涓績',receiverName:'缁翠慨鍛樼敳',remark:'鏇存崲涓昏酱杞存壙',createTime:'2026-10-14 15:30:00'};
+window.qa={parts,movements:[movement],calls:0,data:mode==='return'?{type:'return',original:movement}:mode==='form'?{}:{type:mode==='receive'?'receive':'issue',part:parts[0],repairId:1,repairCode:'WX-20261014-001'}};
+const C=mode==='ledger'?Ledger:mode==='records'?Records:mode==='form'?Form:Action;
+const app=createApp({render:()=>mode==='records'?h('main',[h(C)]):h(C)});app.directive('access',{});app.mount('#app');`;
+const html=`<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>澶囦欢缁勪欢楠岃瘉 路 妯℃嫙鏁版嵁</title><style>body{margin:0;background:#f5f5f5;color:#1f1f1f;font:14px system-ui}main{padding:24px;background:white;margin:20px}.qa-modal{background:white;max-width:600px;margin:24px auto;padding:24px}header{font-size:20px;font-weight:600;margin-bottom:20px}footer{display:flex;gap:12px;justify-content:flex-end;padding-top:16px}footer button{padding:8px 20px}footer button:disabled{opacity:.5}@media(max-width:640px){main{margin:8px;padding:12px}.qa-modal{margin:8px;padding:16px}} .flex{display:flex}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-3{gap:12px}.gap-y-3{row-gap:12px}.mb-4{margin-bottom:16px}.mb-3{margin-bottom:12px}.mt-4{margin-top:16px}.w-full{width:100%}.w-28{width:112px}.w-36{width:144px}.text-lg{font-size:18px}.text-base{font-size:16px}.font-semibold{font-weight:600}.grid{display:grid}.gap-x-4{column-gap:16px}.ml-2{margin-left:8px}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:col-span-2{grid-column:span 2}}</style><div id="app"></div><script type="module" src="/qa-entry.js"></script></html>`;
+const server=await createServer({root,configFile:false,appType:'custom',cacheDir:'node_modules/.cache/spare-management-qa',server:{host:'127.0.0.1',port:0},plugins:[{name:'spare-qa',enforce:'pre',
+resolveId(id){if(id==='@vben/common-ui')return '\0qa-modal';if(id==='#/api/mes/dv/spare')return '\0qa-api';if(id==='#/api/mes/dv/repair')return '\0qa-repair';if(id==='#/api/system/user')return '\0qa-user';if(id==='/qa-entry.js')return '\0qa-entry'},
+load(id){if(id==='\0qa-modal')return modal;if(id==='\0qa-api')return api;if(id==='\0qa-repair')return 'export async function getRepairPage(){return {list:[{id:1,code:"WX-001",status:0,machineryName:"璁惧A"}]}}';if(id==='\0qa-user')return 'export async function getSimpleUserList(){return [{id:1,nickname:"缁翠慨鍛樼敳"}]}';if(id==='\0qa-entry')return entry},
+transform(code,id){if(!id.endsWith('.vue'))return;const {descriptor}=parse(code);const script=compileScript(descriptor,{id,inlineTemplate:true});return{code:ts.transpileModule(script.content,{compilerOptions:{target:ts.ScriptTarget.ESNext,module:ts.ModuleKind.ESNext}}).outputText,map:null}},
+configureServer(s){s.middlewares.use((req,res,next)=>{if(req.url==='/'||req.url?.startsWith('/?')){res.setHeader('Content-Type','text/html;charset=utf-8');res.end(html)}else next()})}
+}],optimizeDeps:{entries:[],noDiscovery:true,include:['vue','ant-design-vue']}});
+await server.listen();let browser;const errors=[];
+try{
+ browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'});const page=await browser.newPage({viewport:{width:1440,height:950}});
+ page.on('pageerror',e=>errors.push(e.message));const url='http://127.0.0.1:'+server.httpServer.address().port;
+ await page.goto(url);await page.getByText('娣辨矡鐞冭酱鎵�',{exact:true}).waitFor();await page.screenshot({path:path.join(output,'desktop-ledger.png'),fullPage:true});
+ await page.goto(url+'/?mode=records');await page.getByText('鏇存崲涓昏酱杞存壙',{exact:true}).waitFor();await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(output,'mobile-records.png'),fullPage:true});
+ await page.goto(url+'/?mode=issue');await page.locator('.qa-confirm').waitFor();await page.locator('.qa-confirm').click();await page.waitForTimeout(100);assert.equal(await page.evaluate(()=>window.qa.calls),0,'Missing receiver and reason blocks issue');
+ await page.locator('#form_item_receiverUserId').click();await page.locator('.ant-select-item-option-content').filter({hasText:'缁翠慨鍛樼敳'}).click();
+ await page.locator('textarea').fill('鏇存崲涓昏酱杞存壙');await page.locator('.ant-input-number-input').fill('11');await page.locator('.qa-confirm').click();await page.getByText('瓒呰繃褰撳墠鍙敤搴撳瓨',{exact:true}).waitFor();assert.equal(await page.evaluate(()=>window.qa.calls),0);
+ await page.locator('.ant-input-number-input').fill('2');await page.setViewportSize({width:1440,height:950});await page.screenshot({path:path.join(output,'desktop-issue.png'),fullPage:true});
+ await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.calls===1);assert.equal(await page.locator('.qa-close').isEnabled(),false);await page.waitForFunction(()=>!window.qaModal.state.open);assert.equal(await page.evaluate(()=>window.qa.calls),1);
+ await page.goto(url+'/?mode=return');await page.locator('.qa-confirm').waitFor();await page.locator('textarea').fill('鏈娇鐢ㄩ��鍥�');await page.locator('.ant-input-number-input').fill('4');await page.locator('.qa-confirm').click();await page.getByText('瓒呰繃鍙��鍥炴暟閲�',{exact:true}).waitFor();assert.equal(await page.evaluate(()=>window.qa.calls),0);
+ await page.locator('.ant-input-number-input').fill('2');await page.evaluate(()=>window.qa.fail=true);await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.calls===1&&!window.qaModal.state.locked);assert.equal(await page.locator('.qa-close').isEnabled(),true);
+ const oldKey=await page.evaluate(()=>window.qa.request.requestKey);await page.evaluate(()=>window.qa.fail=false);await page.locator('.qa-confirm').click();await page.waitForFunction(()=>!window.qaModal.state.open);assert.equal(await page.evaluate(()=>window.qa.request.requestKey),oldKey,'Failed submission retry uses same idempotency key');
+ await page.goto(url+'/?mode=form');await page.locator('.qa-confirm').waitFor();await page.locator('.qa-confirm').click();await page.waitForTimeout(80);assert.equal(await page.evaluate(()=>window.qa.saved),undefined,'Required fields block empty part creation');
+ await page.goto(url+'/?mode=issue');await page.locator('.qa-confirm').waitFor();await page.locator('#form_item_receiverUserId').click();await page.locator('.ant-select-item-option-content').filter({hasText:'缁翠慨鍛樼敳'}).click();await page.locator('textarea').fill('鏇存崲涓昏酱杞存壙');await page.locator('.ant-input-number-input').fill('2');await page.locator('header').click();await page.waitForTimeout(300);await page.screenshot({path:path.join(output,'desktop-issue.png'),fullPage:true});
+ assert.deepEqual(errors,[]);const result={passed:true,scope:'Actual Vue/Ant Design components with mock APIs and modal adapter; no production writes',checks:['ledger display and shortage','records mobile display','required issue fields','issue stock limit','single submit and cancel lock','return limit','failure unlock','retry idempotency key','required part fields'],screenshots:['desktop-ledger.png','mobile-records.png','desktop-issue.png']};fs.writeFileSync(path.join(output,'frontend-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));
+}finally{await browser?.close();await server.close()}
+
+
+
+
+
diff --git a/src/api/core/auth.ts b/src/api/core/auth.ts
index cec93f5..9012e53 100644
--- a/src/api/core/auth.ts
+++ b/src/api/core/auth.ts
@@ -79,13 +79,14 @@
}
/** 閫�鍑虹櫥褰� */
-export async function logoutApi(accessToken: string) {
+export async function logoutApi(accessToken: string, tenant?: { tenantId: number | null; visitTenantId: number | null }) {
return baseRequestClient.post(
'/system/auth/logout',
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
+ ...(tenant ? { 'tenant-id': tenant.tenantId, 'visit-tenant-id': tenant.visitTenantId } : {}),
},
},
);
diff --git a/src/api/mes/dv/spare/index.ts b/src/api/mes/dv/spare/index.ts
new file mode 100644
index 0000000..8888a58
--- /dev/null
+++ b/src/api/mes/dv/spare/index.ts
@@ -0,0 +1,30 @@
+import type { PageParam, PageResult } from '@vben/request';
+import { requestClient } from '#/api/request';
+export interface SparePart {
+ id: number; code: string; name: string; specification?: string; unit: string;
+ location?: string; stockQuantity: number; safetyQuantity: number; status: number; remark?: string;
+}
+export interface SpareMovement {
+ id: number; sparePartId: number; sparePartCode: string; sparePartName: string;
+ specification?: string; unit: string; type: number; quantity: number;
+ beforeQuantity: number; afterQuantity: number; repairId?: number; repairCode?: string;
+ machineryName?: string; receiverName?: string; receiverUserId?: number; operatorUserId: number;
+ originalId?: number; returnedQuantity: number; returnableQuantity: number; remark: string; createTime: string;
+}
+export interface SpareAction {
+ requestKey: string; sparePartId?: number; repairId?: number; originalId?: number;
+ receiverUserId?: number; quantity: number; remark: string;
+}
+export type SpareSave = Omit<SparePart, 'id' | 'stockQuantity'> & { id?: number; initialQuantity: number };
+export const getSparePage = (params: PageParam & Record<string, unknown>) =>
+ requestClient.get<PageResult<SparePart>>('/mes/dv/spare/page', { params });
+export const getSpare = (id: number) => requestClient.get<SparePart>('/mes/dv/spare/get', { params: { id } });
+export const createSpare = (data: SpareSave) => requestClient.post<number>('/mes/dv/spare/create', data);
+export const updateSpare = (data: SpareSave) => requestClient.put('/mes/dv/spare/update', data);
+export const deleteSpare = (id: number) => requestClient.delete('/mes/dv/spare/delete', { params: { id } });
+export const getSpareMovements = (params: PageParam & Record<string, unknown>) =>
+ requestClient.get<PageResult<SpareMovement>>('/mes/dv/spare/movement-page', { params });
+export const spareAction = (type: 'issue' | 'receive' | 'return', data: SpareAction) =>
+ requestClient.post<number>(`/mes/dv/spare/${type}`, data);
+export const movementLabels: Record<number, string> = { 1: '鍒濆搴撳瓨', 2: '鍏ュ簱', 3: '缁翠慨棰嗙敤', 4: '閫�鍥�' };
+
diff --git a/src/api/mes/md/client/index.ts b/src/api/mes/md/client/index.ts
index 9be868d..41b0347 100644
--- a/src/api/mes/md/client/index.ts
+++ b/src/api/mes/md/client/index.ts
@@ -6,7 +6,6 @@
/** MES 瀹㈡埛 */
export interface Client {
id?: number; // 瀹㈡埛缂栧彿
- code?: string; // 瀹㈡埛缂栫爜
name?: string; // 瀹㈡埛鍚嶇О
nickname?: string; // 瀹㈡埛绠�绉�
englishName?: string; // 瀹㈡埛鑻辨枃鍚嶇О
@@ -31,9 +30,9 @@
/** 瀹㈡埛瀵煎叆缁撴灉 */
export interface ClientImportRespVO {
- createCodes?: string[]; // 鏂板鎴愬姛鐨勫鎴风紪鐮�
- updateCodes?: string[]; // 鏇存柊鎴愬姛鐨勫鎴风紪鐮�
- failureCodes?: Record<string, string>; // 瀵煎叆澶辫触鐨勫鎴风紪鐮佸強鍘熷洜
+ createCustomerNames?: string[]; // 鏂板鎴愬姛鐨勫鎴峰悕绉�
+ updateCustomerNames?: string[]; // 鏇存柊鎴愬姛鐨勫鎴峰悕绉�
+ failureCustomerNames?: Record<string, string>; // 瀵煎叆澶辫触鐨勫鎴峰悕绉板強鍘熷洜
}
}
diff --git a/src/api/mes/pd/document-audit/index.ts b/src/api/mes/pd/document-audit/index.ts
index b6e21c8..4d64e4e 100644
--- a/src/api/mes/pd/document-audit/index.ts
+++ b/src/api/mes/pd/document-audit/index.ts
@@ -14,6 +14,10 @@
documentName?: string;
/** 璧勬枡鐗堟湰锛堝啑浣欙級 */
documentVersion?: string;
+ /** 璧勬枡鏂囦欢鍚嶏紙鍙栬祫鏂欑涓�涓檮浠讹級 */
+ fileName?: string;
+ /** 璧勬枡鏂囦欢鍦板潃锛堝彇璧勬枡绗竴涓檮浠讹級 */
+ fileUrl?: string;
/** 瀹℃牳浜猴紙鐢ㄦ埛ID锛� */
reviewer?: number;
/** 瀹℃牳浜哄悕绉� */
diff --git a/src/api/mes/pro/mps/index.ts b/src/api/mes/pro/mps/index.ts
index 0ead6e3..652dbf8 100644
--- a/src/api/mes/pro/mps/index.ts
+++ b/src/api/mes/pro/mps/index.ts
@@ -19,7 +19,6 @@
quantity?: number; // 璁″垝鐢熶骇鏁伴噺
quantityProduced?: number; // 宸茬敓浜ф暟閲�
clientId?: number; // 瀹㈡埛ID
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
requestDate?: number; // 闇�姹傛棩鏈�
issueDate?: number; // 涓嬪彂鏃ユ湡
diff --git a/src/api/mes/pro/route/process/index.ts b/src/api/mes/pro/route/process/index.ts
index a08056f..332f71a 100644
--- a/src/api/mes/pro/route/process/index.ts
+++ b/src/api/mes/pro/route/process/index.ts
@@ -16,6 +16,8 @@
routeId?: number;
processId?: number;
paramTemplateId?: number;
+ /** 宸ュ簭鏀剁巼锛堜骇鍑�/鎶曞叆锛�0~1锛� */
+ yieldRate?: number;
processCode?: string;
processName?: string;
sort?: number;
diff --git a/src/api/mes/pro/task/index.ts b/src/api/mes/pro/task/index.ts
index 13fb333..6ec30d3 100644
--- a/src/api/mes/pro/task/index.ts
+++ b/src/api/mes/pro/task/index.ts
@@ -176,6 +176,55 @@
}
/** 鏌ヨ寰呮姤宸ュ彴璐︼紙鍒嗛〉锛� */
+export interface BatchScheduleRequest {
+ workOrderIds: number[];
+ startTime: string;
+ previewHash?: string;
+ previewTime?: string;
+ allowLate?: boolean;
+}
+
+export interface BatchSchedulePreview {
+ canExecute: boolean;
+ previewHash: string;
+ previewTime: string;
+ warnings: string[];
+ orders: {
+ workOrderId: number;
+ workOrderCode?: string;
+ workOrderName?: string;
+ quantity?: number;
+ taskCount: number;
+ requestDate?: string;
+ endTime?: string;
+ late: boolean;
+ error?: string;
+ }[];
+ tasks: {
+ workOrderId: number;
+ workOrderCode: string;
+ processId: number;
+ processName: string;
+ workstationId: number;
+ workstationName: string;
+ machineryNames: string;
+ quantity: number;
+ hourlyCapacity: number;
+ duration: number;
+ startTime: string;
+ endTime: string;
+ }[];
+}
+
+export function previewBatchSchedule(data: BatchScheduleRequest) {
+ return requestClient.post<BatchSchedulePreview>('/mes/pro/task/auto-schedule/preview', data);
+}
+
+export function executeBatchSchedule(data: BatchScheduleRequest) {
+ return requestClient.post<{ taskIds: number[] }>('/mes/pro/task/auto-schedule', data);
+}
+
+/** 鏌ヨ寰呮姤宸ュ彴璐︼紙鍒嗛〉锛� */
export function getPendingFeedbackPage(
params: MesProTaskApi.PendingFeedbackPageParams,
) {
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 68b8037..63dcc6e 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -22,7 +22,6 @@
quantityChanged?: number; // 璋冩暣鏁伴噺
quantityScheduled?: number; // 宸叉帓浜ф暟閲�
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
vendorId?: number; // 渚涘簲鍟嗙紪鍙�
vendorName?: string; // 渚涘簲鍟嗗悕绉�
@@ -35,7 +34,6 @@
cancelDate?: number; // 鍙栨秷鏃堕棿
status?: number; // 璁㈠崟鐘舵��
scheduleStatus?: number; // 鎺掍骇鐘舵��
- productionStatus?: number; // 鐢熶骇鐘舵��
processProgressList?: ProcessProgress[]; // 宸ュ簭鐢熶骇杩涘害
completionProgress?: number; // 瀹屾垚杩涘害
remark?: string; // 澶囨敞
diff --git a/src/api/mes/qc/date-filter.ts b/src/api/mes/qc/date-filter.ts
new file mode 100644
index 0000000..9dea968
--- /dev/null
+++ b/src/api/mes/qc/date-filter.ts
@@ -0,0 +1,18 @@
+import dayjs from 'dayjs';
+
+/** 鏃ユ湡鏌ヨ鍖呭惈缁撴潫鏃ユ湡褰撳ぉ鐨勫叏閮ㄨ川妫�璁板綍锛屽垎椤典笌瀵煎嚭浣跨敤鐩稿悓鍙e緞銆� */
+export function normalizeQcDateFilter<T extends object>(params: T): T {
+ const normalized: Record<string, string[]> = {};
+ for (const field of ['inspectDate', 'receiveDate'] as const) {
+ const range = (params as { inspectDate?: string[]; receiveDate?: string[] })[field];
+ if (!Array.isArray(range) || range.length !== 2) continue;
+ const start = dayjs(range[0]);
+ const end = dayjs(range[1]);
+ if (!start.isValid() || !end.isValid()) continue;
+ normalized[field] = [
+ start.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+ end.endOf('day').format('YYYY-MM-DD HH:mm:ss'),
+ ];
+ }
+ return Object.keys(normalized).length > 0 ? { ...params, ...normalized } : params;
+}
diff --git a/src/api/mes/qc/indicatorresult/index.ts b/src/api/mes/qc/indicatorresult/index.ts
index dfa8a7f..b0fd5bb 100644
--- a/src/api/mes/qc/indicatorresult/index.ts
+++ b/src/api/mes/qc/indicatorresult/index.ts
@@ -24,7 +24,6 @@
qcId?: number; // 鍏宠仈璐ㄦ鍗� ID
qcType?: number; // 璐ㄦ绫诲瀷
itemId?: number; // 浜у搧鐗╂枡 ID
- sn?: string; // 鐗╄祫 SN
remark?: string; // 澶囨敞
createTime?: Date; // 鍒涘缓鏃堕棿
items?: IndicatorResultDetail[]; // 妫�楠岀粨鏋滄槑缁嗗垪琛�
diff --git a/src/api/mes/qc/ipqc/index.ts b/src/api/mes/qc/ipqc/index.ts
index 48fe15c..ab2804f 100644
--- a/src/api/mes/qc/ipqc/index.ts
+++ b/src/api/mes/qc/ipqc/index.ts
@@ -2,6 +2,8 @@
import { requestClient } from '#/api/request';
+import { normalizeQcDateFilter } from '../date-filter';
+
export namespace MesQcIpqcApi {
/** MES 杩囩▼妫�楠屽崟 */
export interface Ipqc {
@@ -58,7 +60,7 @@
/** 鏌ヨ杩囩▼妫�楠屽崟鍒嗛〉 */
export function getIpqcPage(params: PageParam) {
return requestClient.get<PageResult<MesQcIpqcApi.Ipqc>>('/mes/qc/ipqc/page', {
- params,
+ params: normalizeQcDateFilter(params),
});
}
@@ -89,7 +91,9 @@
/** 瀵煎嚭杩囩▼妫�楠屽崟 */
export function exportIpqc(params: any) {
- return requestClient.download('/mes/qc/ipqc/export-excel', { params });
+ return requestClient.download('/mes/qc/ipqc/export-excel', {
+ params: normalizeQcDateFilter(params),
+ });
}
/** 涓嬭浇杩囩▼妫�楠屽崟瀵煎叆妯℃澘 */
diff --git a/src/api/mes/qc/iqc/index.ts b/src/api/mes/qc/iqc/index.ts
index 1600d54..a2e8297 100644
--- a/src/api/mes/qc/iqc/index.ts
+++ b/src/api/mes/qc/iqc/index.ts
@@ -2,6 +2,8 @@
import { requestClient } from '#/api/request';
+import { normalizeQcDateFilter } from '../date-filter';
+
export namespace MesQcIqcApi {
/** MES 鏉ユ枡妫�楠屽崟 */
export interface Iqc {
@@ -53,7 +55,7 @@
/** 鏌ヨ鏉ユ枡妫�楠屽崟鍒嗛〉 */
export function getIqcPage(params: PageParam) {
return requestClient.get<PageResult<MesQcIqcApi.Iqc>>('/mes/qc/iqc/page', {
- params,
+ params: normalizeQcDateFilter(params),
});
}
@@ -84,7 +86,9 @@
/** 瀵煎嚭鏉ユ枡妫�楠屽崟 */
export function exportIqc(params: any) {
- return requestClient.download('/mes/qc/iqc/export-excel', { params });
+ return requestClient.download('/mes/qc/iqc/export-excel', {
+ params: normalizeQcDateFilter(params),
+ });
}
/** 涓嬭浇鏉ユ枡妫�楠屽崟瀵煎叆妯℃澘 */
diff --git a/src/api/mes/qc/ledger/index.ts b/src/api/mes/qc/ledger/index.ts
index 40f86a8..50852b3 100644
--- a/src/api/mes/qc/ledger/index.ts
+++ b/src/api/mes/qc/ledger/index.ts
@@ -2,6 +2,8 @@
import { requestClient } from '#/api/request';
+import { normalizeQcDateFilter } from '../date-filter';
+
export namespace MesQcInspectionLedgerApi {
/** MES 妫�娴嬬粨鏋滃彴璐︼紙涓�琛屼竴涓娴嬮」缁撴灉锛涙楠屽崟鏈綍鍏ユ娴嬮」鏃惰缁勫瓧娈典负绌猴級 */
export interface Ledger {
@@ -36,13 +38,13 @@
export function getInspectionLedgerPage(params: PageParam) {
return requestClient.get<PageResult<MesQcInspectionLedgerApi.Ledger>>(
'/mes/qc/inspection-ledger/page',
- { params },
+ { params: normalizeQcDateFilter(params) },
);
}
/** 瀵煎嚭妫�娴嬬粨鏋滃彴璐� */
export function exportInspectionLedger(params: Record<string, unknown>) {
return requestClient.download('/mes/qc/inspection-ledger/export-excel', {
- params,
+ params: normalizeQcDateFilter(params),
});
}
diff --git a/src/api/mes/qc/oqc/index.ts b/src/api/mes/qc/oqc/index.ts
index 98c341a..75a07eb 100644
--- a/src/api/mes/qc/oqc/index.ts
+++ b/src/api/mes/qc/oqc/index.ts
@@ -2,6 +2,8 @@
import { requestClient } from '#/api/request';
+import { normalizeQcDateFilter } from '../date-filter';
+
export namespace MesQcOqcApi {
/** MES 鍑鸿揣妫�楠屽崟 */
export interface Oqc {
@@ -53,7 +55,7 @@
/** 鏌ヨ鍑鸿揣妫�楠屽崟鍒嗛〉 */
export function getOqcPage(params: PageParam) {
return requestClient.get<PageResult<MesQcOqcApi.Oqc>>('/mes/qc/oqc/page', {
- params,
+ params: normalizeQcDateFilter(params),
});
}
@@ -84,7 +86,9 @@
/** 瀵煎嚭鍑鸿揣妫�楠屽崟 */
export function exportOqc(params: any) {
- return requestClient.download('/mes/qc/oqc/export-excel', { params });
+ return requestClient.download('/mes/qc/oqc/export-excel', {
+ params: normalizeQcDateFilter(params),
+ });
}
/** 涓嬭浇鍑鸿揣妫�楠屽崟瀵煎叆妯℃澘 */
diff --git a/src/api/mes/qc/rqc/index.ts b/src/api/mes/qc/rqc/index.ts
index 2765f99..d2898e9 100644
--- a/src/api/mes/qc/rqc/index.ts
+++ b/src/api/mes/qc/rqc/index.ts
@@ -2,6 +2,8 @@
import { requestClient } from '#/api/request';
+import { normalizeQcDateFilter } from '../date-filter';
+
export namespace MesQcRqcApi {
/** MES 閫�璐ф楠屽崟 */
export interface Rqc {
@@ -43,7 +45,7 @@
/** 鏌ヨ閫�璐ф楠屽崟鍒嗛〉 */
export function getRqcPage(params: PageParam) {
return requestClient.get<PageResult<MesQcRqcApi.Rqc>>('/mes/qc/rqc/page', {
- params,
+ params: normalizeQcDateFilter(params),
});
}
@@ -74,5 +76,7 @@
/** 瀵煎嚭閫�璐ф楠屽崟 */
export function exportRqc(params: any) {
- return requestClient.download('/mes/qc/rqc/export-excel', { params });
+ return requestClient.download('/mes/qc/rqc/export-excel', {
+ params: normalizeQcDateFilter(params),
+ });
}
diff --git a/src/api/mes/wm/batch/index.ts b/src/api/mes/wm/batch/index.ts
index 569efd7..3b5b70c 100644
--- a/src/api/mes/wm/batch/index.ts
+++ b/src/api/mes/wm/batch/index.ts
@@ -19,7 +19,6 @@
vendorCode?: string; // 渚涘簲鍟嗙紪鐮�
vendorName?: string; // 渚涘簲鍟嗗悕绉�
clientId?: number; // 瀹㈡埛 ID
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
purchaseOrderCode?: string; // 閲囪喘璁㈠崟缂栧彿
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
@@ -80,7 +79,6 @@
salesDate?: Date; // 鍑哄簱鏃ユ湡
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
clientId?: number; // 瀹㈡埛 ID
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
oqcCode?: string; // OQC 妫�楠屽崟鍙�
checkResult?: number; // OQC 妫�楠岀粨璁猴紙瀛楀吀 mes_qc_check_result锛�
diff --git a/src/api/mes/wm/packages/index.ts b/src/api/mes/wm/packages/index.ts
index cdbe51c..b30bdd0 100644
--- a/src/api/mes/wm/packages/index.ts
+++ b/src/api/mes/wm/packages/index.ts
@@ -12,7 +12,6 @@
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
invoiceCode?: string; // 鍙戠エ缂栧彿
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
clientNickname?: string; // 瀹㈡埛绠�绉�
length?: number; // 绠遍暱搴�
diff --git a/src/api/mes/wm/productissue/index.ts b/src/api/mes/wm/productissue/index.ts
index d17e086..bee832f 100644
--- a/src/api/mes/wm/productissue/index.ts
+++ b/src/api/mes/wm/productissue/index.ts
@@ -16,7 +16,6 @@
workstationName?: string; // 宸ヤ綔绔欏悕绉�
workOrderId?: number; // 鐢熶骇璁㈠崟缂栧彿
workOrderCode?: string; // 鐢熶骇璁㈠崟缂栫爜
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
requiredTime?: number; // 闇�姹傛椂闂�
status?: number; // 鍗曟嵁鐘舵��
diff --git a/src/api/mes/wm/productsales/index.ts b/src/api/mes/wm/productsales/index.ts
index 08c1e86..af39ac3 100644
--- a/src/api/mes/wm/productsales/index.ts
+++ b/src/api/mes/wm/productsales/index.ts
@@ -11,7 +11,6 @@
noticeId?: number; // 鍙戣揣閫氱煡鍗曠紪鍙�
noticeCode?: string; // 鍙戣揣閫氱煡鍗曠紪鐮�
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
saleOrderId?: number; // 閿�鍞鍗旾D
diff --git a/src/api/mes/wm/returnsales/index.ts b/src/api/mes/wm/returnsales/index.ts
index 8ad32ce..9111da1 100644
--- a/src/api/mes/wm/returnsales/index.ts
+++ b/src/api/mes/wm/returnsales/index.ts
@@ -10,7 +10,6 @@
name?: string; // 閫�璐у崟鍚嶇О
salesOrderCode?: string; // 閿�鍞鍗曞彿
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
returnDate?: number; // 閫�璐ф棩鏈�
returnReason?: string; // 閫�璐у師鍥�
diff --git a/src/api/mes/wm/salesnotice/index.ts b/src/api/mes/wm/salesnotice/index.ts
index 66fe8af..588df7d 100644
--- a/src/api/mes/wm/salesnotice/index.ts
+++ b/src/api/mes/wm/salesnotice/index.ts
@@ -11,7 +11,6 @@
saleOrderId?: number; // 閿�鍞鍗曠紪鍙�
saleOrderCode?: string; // 閿�鍞鍗曠紪鍙�
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
salesDate?: number; // 鍙戣揣鏃ユ湡
recipientName?: string; // 鏀惰揣浜�
diff --git a/src/api/request.ts b/src/api/request.ts
index 7389c31..679b78f 100644
--- a/src/api/request.ts
+++ b/src/api/request.ts
@@ -33,10 +33,13 @@
/**
* 閲嶆柊璁よ瘉閫昏緫
*/
- async function doReAuthenticate() {
+ async function doReAuthenticate(error?: { config?: { headers?: Record<string, unknown> } }) {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
+ const requestToken = error?.config?.headers?.Authorization;
+ // 宸查��鍑烘垨宸插垏鎹㈣处鍙锋椂锛屾棫璇锋眰鐨� 401 涓嶅緱娓呴櫎鏂颁細璇濄��
+ if (requestToken && requestToken !== formatToken(accessStore.accessToken)) return;
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
@@ -58,6 +61,9 @@
throw new Error('Refresh token is null!');
}
const resp = await refreshTokenApi(refreshToken);
+ if (accessStore.refreshToken !== refreshToken) {
+ throw new Error('鐧诲綍鐘舵�佸凡鍙樺寲锛屽拷鐣ユ棫浼氳瘽鐨勪护鐗屽埛鏂扮粨鏋�');
+ }
const newToken = resp?.data?.data?.accessToken;
// add by 鑺嬭壙锛氳繖閲屼竴瀹氳鎶涘嚭 resp.data锛屼粠鑰岃Е鍙� authenticateResponseInterceptor 涓紝鍒锋柊浠ょ墝澶辫触锛侊紒锛�
if (!newToken) {
@@ -220,13 +226,13 @@
fulfilled: (config) => {
const accessStore = useAccessStore();
// 娣诲姞绉熸埛缂栧彿
- config.headers['tenant-id'] = tenantEnable
- ? accessStore.tenantId
- : undefined;
+ if (!('tenant-id' in config.headers)) {
+ config.headers['tenant-id'] = tenantEnable ? accessStore.tenantId : undefined;
+ }
// 鍙湁鐧诲綍鏃讹紝鎵嶈缃� visit-tenant-id 璁块棶绉熸埛
- config.headers['visit-tenant-id'] = tenantEnable
- ? accessStore.visitTenantId
- : undefined;
+ if (!('visit-tenant-id' in config.headers)) {
+ config.headers['visit-tenant-id'] = tenantEnable ? accessStore.visitTenantId : undefined;
+ }
return config;
},
});
diff --git a/src/api/wls/batch/index.ts b/src/api/wls/batch/index.ts
index abf2fd2..bf56e3c 100644
--- a/src/api/wls/batch/index.ts
+++ b/src/api/wls/batch/index.ts
@@ -19,7 +19,6 @@
vendorCode?: string; // 渚涘簲鍟嗙紪鐮�
vendorName?: string; // 渚涘簲鍟嗗悕绉�
clientId?: number; // 瀹㈡埛 ID
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
purchaseOrderCode?: string; // 閲囪喘璁㈠崟缂栧彿
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
diff --git a/src/api/wls/packages/index.ts b/src/api/wls/packages/index.ts
index 4207112..226e95c 100644
--- a/src/api/wls/packages/index.ts
+++ b/src/api/wls/packages/index.ts
@@ -12,7 +12,6 @@
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
invoiceCode?: string; // 鍙戠エ缂栧彿
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
clientNickname?: string; // 瀹㈡埛绠�绉�
length?: number; // 绠遍暱搴�
diff --git a/src/api/wls/productissue/index.ts b/src/api/wls/productissue/index.ts
index 5da6598..e16b2b1 100644
--- a/src/api/wls/productissue/index.ts
+++ b/src/api/wls/productissue/index.ts
@@ -13,7 +13,6 @@
workstationName?: string; // 宸ヤ綔绔欏悕绉�
workOrderId?: number; // 鐢熶骇璁㈠崟缂栧彿
workOrderCode?: string; // 鐢熶骇璁㈠崟缂栫爜
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
requiredTime?: number; // 闇�姹傛椂闂�
status?: number; // 鍗曟嵁鐘舵��
diff --git a/src/api/wls/productsales/index.ts b/src/api/wls/productsales/index.ts
index a2fee8a..f232668 100644
--- a/src/api/wls/productsales/index.ts
+++ b/src/api/wls/productsales/index.ts
@@ -11,7 +11,6 @@
noticeId?: number; // 鍙戣揣閫氱煡鍗曠紪鍙�
noticeCode?: string; // 鍙戣揣閫氱煡鍗曠紪鐮�
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
saleOrderId?: number; // 閿�鍞鍗旾D
diff --git a/src/api/wls/returnsales/index.ts b/src/api/wls/returnsales/index.ts
index 311acf2..813e9ab 100644
--- a/src/api/wls/returnsales/index.ts
+++ b/src/api/wls/returnsales/index.ts
@@ -10,7 +10,6 @@
name?: string; // 閫�璐у崟鍚嶇О
salesOrderCode?: string; // 閿�鍞鍗曞彿
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
returnDate?: number; // 閫�璐ф棩鏈�
returnReason?: string; // 閫�璐у師鍥�
diff --git a/src/api/wls/salesnotice/index.ts b/src/api/wls/salesnotice/index.ts
index 7f23d46..5075059 100644
--- a/src/api/wls/salesnotice/index.ts
+++ b/src/api/wls/salesnotice/index.ts
@@ -10,7 +10,6 @@
name?: string; // 閫氱煡鍗曞悕绉�
salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
clientId?: number; // 瀹㈡埛缂栧彿
- clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
salesDate?: number; // 鍙戣揣鏃ユ湡
recipientName?: string; // 鏀惰揣浜�
diff --git a/src/bootstrap.ts b/src/bootstrap.ts
index bbfb549..1cb5cca 100644
--- a/src/bootstrap.ts
+++ b/src/bootstrap.ts
@@ -7,6 +7,7 @@
import { initStores } from './packages/stores/src';
import './packages/styles/src';
import './packages/styles/src/antd/index.css';
+import './styles/herbal-theme.css';
import { useTitle } from '@vueuse/core';
diff --git a/src/components/quality/engine/report-evaluator.ts b/src/components/quality/engine/report-evaluator.ts
index 55b45ac..e91ac54 100644
--- a/src/components/quality/engine/report-evaluator.ts
+++ b/src/components/quality/engine/report-evaluator.ts
@@ -197,6 +197,17 @@
return;
}
+ const hasLimit = item.upperLimit != null || item.lowerLimit != null;
+ if (
+ hasLimit &&
+ (!String(item.actualValue ?? '').trim() || !Number.isFinite(toNumber(item.actualValue)))
+ ) {
+ applyResult(item, undefined);
+ errors.push(
+ `绗� ${index + 1} 椤广��${item.itemName ?? ''}銆嶇己灏戞湁鏁堟暟鍊肩粨鏋滐紝寰呰ˉ娴嬫垨纭鍘熷璁板綍`,
+ );
+ return;
+ }
const byLimit = evaluateByLimit(item);
if (byLimit !== undefined) {
applyResult(item, byLimit ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL);
@@ -274,4 +285,4 @@
const node = parseRule(expression);
ruleCache.set(expression, node);
return node;
-}
\ No newline at end of file
+}
diff --git a/src/layouts/auth-scene.vue b/src/layouts/auth-scene.vue
new file mode 100644
index 0000000..1ee0093
--- /dev/null
+++ b/src/layouts/auth-scene.vue
@@ -0,0 +1,78 @@
+<script setup lang="ts">
+withDefaults(defineProps<{ appName?: string; logo?: string; platformTitle?: string; platformDescription?: string }>(), {
+ appName: '', logo: '', platformTitle: '婊嬭ˉ鍝佽涓氭暟瀛楀寲绠$悊骞冲彴', platformDescription: '浼犳壙涓尰鑽簿鍗� 路 鏈嶅姟鍋ュ悍缇庡ソ鐢熸椿',
+});
+</script>
+<template>
+ <div class="herbal-auth">
+ <img class="herbal-auth__background" src="/images/auth/herbal-courtyard.png" alt="" fetchpriority="high" decoding="async" aria-hidden="true" />
+ <div class="herbal-auth__veil" aria-hidden="true"></div>
+ <header class="herbal-auth__header">
+ <div class="herbal-brand">
+ <img v-if="logo" :src="logo" :alt="appName || '鍏徃鏍囧織'" class="herbal-brand__logo" />
+ <span v-if="appName" class="herbal-brand__name">{{ appName }}</span>
+ <div class="herbal-brand__platform">
+ <p class="herbal-brand__title">{{ platformTitle }}</p>
+ <p class="herbal-brand__description">{{ platformDescription }}</p>
+ </div>
+ </div>
+ <div class="herbal-auth__toolbar"><slot name="toolbar"></slot></div>
+ </header>
+ <main class="herbal-auth__main">
+ <section class="herbal-motto" aria-labelledby="herbal-motto-title">
+ <h2 id="herbal-motto-title"><span></span>鍫傝<span></span></h2>
+ <p>鍝佸懗铏借吹锛屽繀涓嶆暍鍑忕墿鍔�</p>
+ <p>鐐埗铏界箒锛屽繀涓嶆暍鐪佷汉宸�</p>
+ <div class="herbal-motto__rule" aria-hidden="true"><span></span><svg viewBox="0 0 60 24"><path d="M3 12h10c0-6 8-9 12-5 0-7 10-7 10 0 5-4 12-1 12 5h10M13 12c0 8 12 11 17 3 5 8 17 5 17-3M24 12c0-7 12-7 12 0s-12 7-12 0Z" /></svg><span></span></div>
+ </section>
+ <section class="herbal-panel" aria-label="璐︽埛鐧诲綍">
+ <div class="herbal-panel__frame" aria-hidden="true">
+ <svg v-for="corner in ['tl','tr','bl','br']" :key="corner" :class="'herbal-corner herbal-corner--'+corner" viewBox="0 0 44 44"><path d="M2 42V12A10 10 0 0 1 12 2h30M9 42V9h33M2 21h19V2M9 28h19V9M2 35h33V2" /></svg>
+ </div>
+ <div class="herbal-panel__content"><slot></slot></div>
+ </section>
+ </main>
+ </div>
+</template>
+<style scoped>
+.herbal-auth{position:relative;isolation:isolate;min-height:100svh;overflow-x:clip;background:#f6eddf;color:#29251f;padding:clamp(24px,3.2vw,52px) clamp(24px,5vw,88px) 42px;font-family:var(--font-family,system-ui,sans-serif)}
+.herbal-auth__background,.herbal-auth__veil{position:absolute;inset:0;width:100%;height:100%;z-index:-2;object-fit:cover;object-position:center}
+.herbal-auth__veil{z-index:-1;background:linear-gradient(90deg,transparent 48%,rgba(248,239,226,.14))}
+.herbal-auth__header{display:flex;align-items:center;justify-content:space-between;gap:24px;position:relative;z-index:2}
+.herbal-brand{display:flex;align-items:center;gap:18px;min-width:0}
+.herbal-brand__logo{width:70px;height:70px;object-fit:contain;flex-shrink:0}
+.herbal-brand__name{font-family:"STKaiti","KaiTi","FangSong",serif;font-weight:700;font-size:clamp(24px,2.3vw,38px);white-space:nowrap}
+.herbal-brand__platform{border-left:1px solid #8e7e6a;padding-left:24px;min-width:0}
+.herbal-brand__title{font-size:clamp(17px,1.5vw,24px);letter-spacing:.04em;margin:0 0 7px;line-height:1.4}
+.herbal-brand__description{font-size:13px;color:#71685b;letter-spacing:.16em;margin:0;line-height:1.5}
+.herbal-auth__toolbar{flex-shrink:0;position:relative;min-width:80px}
+.herbal-auth__toolbar :deep(>div){position:static!important;background:rgba(255,253,248,.75);border:1px solid #e5d8c5;border-radius:30px;padding:4px 10px}
+.herbal-auth__main{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(400px,.85fr);gap:clamp(36px,5vw,88px);align-items:start;min-height:calc(100svh - 176px);padding-top:34px}
+.herbal-motto{align-self:start;margin-top:12px;padding:0 0 24px;text-align:center}
+.herbal-motto h2{display:flex;align-items:center;justify-content:center;gap:34px;font-family:"STKaiti","KaiTi","FangSong",serif;font-size:clamp(40px,4.3vw,72px);font-weight:700;line-height:1.2;letter-spacing:.2em;color:#8b1419;margin:0 0 12px}
+.herbal-motto h2 span{height:1px;background:#ae7650;width:90px}
+.herbal-motto p{font-family:"STKaiti","KaiTi","FangSong",serif;font-size:clamp(26px,3.15vw,54px);font-weight:700;line-height:1.65;letter-spacing:.015em;color:#863310;margin:0;white-space:nowrap}
+.herbal-motto__rule{display:flex;align-items:center;justify-content:center;gap:22px;margin-top:22px;color:#a46628}
+.herbal-motto__rule span{width:110px;height:1px;background:currentColor}
+.herbal-motto__rule svg{width:55px;height:26px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
+.herbal-panel{position:relative;width:100%;max-width:584px;justify-self:end;min-height:690px;border-radius:20px;background:rgba(255,253,249,.96);box-shadow:0 22px 55px rgba(91,49,19,.13);padding:clamp(76px,12.8vh,120px) clamp(34px,4.6vw,78px) 64px;display:flex;align-items:flex-start}
+.herbal-panel__frame{position:absolute;inset:16px;border:1px solid #cda878;border-radius:12px;pointer-events:none}
+.herbal-corner{position:absolute;width:38px;height:38px;fill:none;stroke:#cda878;stroke-width:1.3;background:#fffdf9}
+.herbal-corner--tl{top:-1px;left:-1px;border-top-left-radius:12px}.herbal-corner--tr{top:-1px;right:-1px;transform:rotate(90deg);border-top-left-radius:12px}.herbal-corner--bl{bottom:-1px;left:-1px;transform:rotate(-90deg);border-top-left-radius:12px}.herbal-corner--br{bottom:-1px;right:-1px;transform:rotate(180deg);border-top-left-radius:12px}
+.herbal-panel__content{width:100%;position:relative;min-width:0}
+:global(.dark .herbal-auth){background:#211b17;color:#f4eadd}
+:global(.dark .herbal-auth__background){filter:brightness(.4) saturate(.7)}
+:global(.dark .herbal-auth__veil){background:rgba(24,19,15,.28)}
+:global(.dark .herbal-brand__description){color:#d4c3ac}
+:global(.dark .herbal-motto h2){color:#f0bab3}
+:global(.dark .herbal-motto p){color:#edcfa5}
+:global(.dark .herbal-panel){background:rgba(39,31,26,.97)}
+:global(.dark .herbal-corner){background:#271f1a}
+:global(.dark .herbal-auth__toolbar >div){background:#30271f;border-color:#8d7053;color:#f4eadd}
+@media(min-width:1800px){.herbal-auth__main{grid-template-columns:minmax(0,1fr) 584px}.herbal-motto{max-width:900px;justify-self:center;width:100%}}
+@media(max-width:1199px){.herbal-auth{padding:28px}.herbal-brand{gap:12px}.herbal-brand__logo{width:52px;height:52px}.herbal-brand__platform{padding-left:16px}.herbal-brand__description{letter-spacing:.06em}.herbal-auth__main{gap:26px;grid-template-columns:minmax(0,1fr) minmax(360px,.9fr)}.herbal-motto p{font-size:clamp(25px,3vw,36px)}.herbal-panel{padding:50px 36px;min-height:600px}}
+@media(max-width:900px){.herbal-auth{padding:24px;}.herbal-brand__name{font-size:24px}.herbal-brand__description{display:none}.herbal-brand__title{font-size:17px}.herbal-auth__main{grid-template-columns:1fr;min-height:calc(100svh - 112px);padding:32px 0;}.herbal-motto{display:none}.herbal-panel{max-width:520px;min-height:560px;justify-self:center}.herbal-auth__background{object-position:35% center}.herbal-auth__veil{background:rgba(248,239,226,.48)}}
+@media(max-width:540px){.herbal-auth{padding:20px 16px 24px}.herbal-auth__header{align-items:flex-start;gap:10px}.herbal-brand{gap:10px;flex-wrap:wrap;max-width:calc(100% - 86px)}.herbal-brand__logo{width:42px;height:42px}.herbal-brand__name{font-size:22px}.herbal-brand__platform{border:0;padding:0;flex-basis:100%}.herbal-brand__title{font-size:14px;letter-spacing:.02em;margin:0}.herbal-auth__toolbar{min-width:76px}.herbal-panel{padding:50px 28px;min-height:510px;border-radius:16px}.herbal-panel__frame{inset:12px}.herbal-auth__main{padding-top:28px}.herbal-corner{width:30px;height:30px}}
+@media(max-height:760px) and (min-width:901px){.herbal-panel{min-height:530px;padding-top:48px;padding-bottom:48px}.herbal-auth__main{padding-top:22px}.herbal-motto{margin-top:20px}}
+</style>
+
diff --git a/src/layouts/auth.vue b/src/layouts/auth.vue
index 51dc083..a1fead3 100644
--- a/src/layouts/auth.vue
+++ b/src/layouts/auth.vue
@@ -1,15 +1,16 @@
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
-import { AuthPageLayout } from '@vben/layouts';
-import { preferences } from '@vben/preferences';
+import { preferences, usePreferences } from '@vben/preferences';
-import { $t } from '#/locales';
import { useCompanyConfigStore } from '#/store/company-config';
+import AuthenticationToolbar from '../packages/effects/layouts/src/authentication/toolbar.vue';
+import AuthScene from './auth-scene.vue';
declare const __APP_LOGO__: string;
const companyConfigStore = useCompanyConfigStore();
+const { isDark } = usePreferences();
onMounted(() => {
// 鐧诲綍椤靛睍绀� loginTitle锛氬惎鍔ㄥ姞杞借蛋鐨勬槸绾嚱鏁帮紙涓嶅~ store锛夛紝杩涘叆鐧诲綍椤垫椂鎷夊彇涓�娆�
@@ -18,22 +19,27 @@
const appName = computed(() => preferences.copyright.companyName);
// 鐧诲綍椤� logo 浼樺厛鍙栨暟鎹簱閰嶇疆锛坧references.logo.source锛夛紝鏈厤缃椂鍥為��缂栬瘧鏈熼粯璁� __APP_LOGO__
-const logo = computed(() => preferences.logo.source || __APP_LOGO__);
-const logoDark = computed(() => preferences.logo.sourceDark);
-// 鐧诲綍椤垫爣棰�/鎻忚堪浣跨敤 i18n 鍥哄畾鏂囨锛屼笉闅忓叕鍙搁厤缃彉鍖�
-const pageTitle = computed(() => $t('authentication.pageTitle'));
-const pageDescription = computed(() => $t('authentication.pageDesc'));
+const logo = computed(() => {
+ const source = (isDark.value && preferences.logo.sourceDark) || preferences.logo.source || __APP_LOGO__;
+ // 鏈厤缃叕鍙告爣蹇楁椂涓嶅睍绀烘鏋舵紨绀� Logo銆�
+ return source?.includes('@vbenjs/static-source') ? '' : source;
+});
+const pageTitle = computed(() => companyConfigStore.loginTitle || '婊嬭ˉ鍝佽涓氭暟瀛楀寲绠$悊骞冲彴');
+const pageDescription = computed(() => companyConfigStore.loginDescription || '浼犳壙涓尰鑽簿鍗� 路 鏈嶅姟鍋ュ悍缇庡ソ鐢熸椿');
</script>
<template>
- <AuthPageLayout
+ <AuthScene
:app-name="appName"
:logo="logo"
- :logo-dark="logoDark"
- :page-description="pageDescription"
- :page-title="pageTitle"
+ :platform-description="pageDescription"
+ :platform-title="pageTitle"
>
- <!-- 鑷畾涔夊伐鍏锋爮 -->
- <!-- <template #toolbar></template> -->
- </AuthPageLayout>
+ <template #toolbar><AuthenticationToolbar :toolbar-list="['language', 'theme']" /></template>
+ <RouterView v-slot="{ Component, route }">
+ <KeepAlive :include="['Login']">
+ <component :is="Component" :key="route.fullPath" />
+ </KeepAlive>
+ </RouterView>
+ </AuthScene>
</template>
diff --git a/src/main.ts b/src/main.ts
index b692f75..e60e320 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,5 @@
-import { initPreferences, preferencesManager } from './packages/preferences/src';
+import { watch } from 'vue';
+import { initPreferences, preferences, preferencesManager } from './packages/preferences/src';
import { unmountGlobalLoading } from './packages/utils/src';
import { overridesPreferences, preferencesExtension } from './preferences';
@@ -24,6 +25,28 @@
overrides: overridesPreferences,
});
+ // 棣栨鍗囩骇鍚屾鍝佺墝涓昏壊锛岄伩鍏嶆棫鐨勮摑鑹插亸濂介伄鐩栨湰娆′富棰樻洿鏂般��
+ // 鍗囩骇鍚庣敤鎴峰湪鍋忓ソ璁剧疆涓�夋嫨鐨勯鑹插拰鏄庢殫妯″紡浠嶄細淇濈暀銆�
+ document.documentElement.dataset.brandTheme = 'herbal';
+ const brandThemeKey = `${namespace}-herbal-theme-v1`;
+ try {
+ if (!localStorage.getItem(brandThemeKey)) {
+ preferencesManager.updatePreferences({
+ theme: { builtinType: 'custom', colorPrimary: '#a51d24' },
+ });
+ localStorage.setItem(brandThemeKey, '1');
+ }
+ } catch {
+ preferencesManager.updatePreferences({
+ theme: { builtinType: 'custom', colorPrimary: '#a51d24' },
+ });
+ }
+ watch(
+ () => preferences.theme.colorPrimary,
+ (color) => { document.documentElement.dataset.herbalPalette = String(color.toLowerCase() === '#a51d24'); },
+ { immediate: true, flush: 'sync' },
+ );
+
// 寮哄埗瑕嗙洊鍏徃 logo 鍜� favicon锛岀粫杩� localStorage 缂撳瓨
if (__APP_LOGO__) {
preferencesManager.updatePreferences({ logo: { source: __APP_LOGO__ } });
diff --git a/src/packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue b/src/packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue
index 8e34e07..3ee6e5d 100644
--- a/src/packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue
+++ b/src/packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue
@@ -42,16 +42,21 @@
<slot name="strengthText"> </slot>
</p>
</template>
- <div
+ <button
+ type="button"
+ :aria-label="show ? '闅愯棌瀵嗙爜' : '鏄剧ず瀵嗙爜'"
+ :aria-pressed="show"
+ :disabled="!!$attrs.disabled"
:class="{
'top-3': !!passwordStrength,
'top-1/2 -translate-y-1/2 items-center': !passwordStrength,
}"
class="text-foreground/60 hover:text-foreground absolute inset-y-0 right-0 flex cursor-pointer pr-3 text-lg leading-5"
@click="show = !show"
+ @keydown.enter.stop
>
<Eye v-if="show" class="size-4" />
<EyeOff v-else class="size-4" />
- </div>
+ </button>
</div>
</template>
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 0d7693a..1aec909 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -147,7 +147,6 @@
DV_MACHINERY_CODE: 'DV_MACHINERY_CODE',
DV_REPAIR_CODE: 'DV_REPAIR_CODE',
DV_SUBJECT_CODE: 'DV_SUBJECT_CODE',
- MD_CLIENT_CODE: 'MD_CLIENT_CODE',
MD_ITEM_TYPE_CODE: 'MD_ITEM_TYPE_CODE',
MD_ITEM_CODE: 'MD_ITEM_CODE',
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
@@ -168,6 +167,7 @@
QC_INDICATOR_CODE: 'QC_INDICATOR_CODE',
QC_INDICATOR_RESULT_CODE: 'QC_INDICATOR_RESULT_CODE',
QC_IQC_CODE: 'QC_IQC_CODE',
+ QC_IQC_VENDOR_BATCH_CODE: 'QC_IQC_VENDOR_BATCH_CODE',
QC_IPQC_CODE: 'QC_IPQC_CODE',
QC_OQC_CODE: 'QC_OQC_CODE',
QC_RQC_CODE: 'QC_RQC_CODE',
@@ -242,13 +242,6 @@
CONFIRMED: 1, // 宸茬‘璁�
FINISHED: 2, // 宸插畬鎴�
CANCELED: 3, // 宸插彇娑�
-} as const;
-
-/** MES 鐢熶骇宸ュ崟鐢熶骇鐘舵�佹灇涓撅紙瀵归綈鍚庣 MesProWorkOrderProductionStatusEnum锛� */
-export const MesProWorkOrderProductionStatusEnum = {
- WAITING: 0, // 寰呯敓浜�
- PRODUCING: 1, // 鐢熶骇涓�
- FINISHED: 2, // 鐢熶骇瀹屾垚
} as const;
/** MES 宸ュ崟绫诲瀷鏋氫妇 */
diff --git a/src/packages/effects/common-ui/src/ui/authentication/login.vue b/src/packages/effects/common-ui/src/ui/authentication/login.vue
index 3402dac..0dc459e 100644
--- a/src/packages/effects/common-ui/src/ui/authentication/login.vue
+++ b/src/packages/effects/common-ui/src/ui/authentication/login.vue
@@ -68,6 +68,7 @@
const rememberMe = ref(!!localUsername);
async function handleSubmit() {
+ if (props.loading) return;
const { valid } = await formApi.validate();
const values = await formApi.getValues();
if (valid) {
diff --git a/src/packages/effects/hooks/src/use-design-tokens.ts b/src/packages/effects/hooks/src/use-design-tokens.ts
index 909c1c5..65b1909 100644
--- a/src/packages/effects/hooks/src/use-design-tokens.ts
+++ b/src/packages/effects/hooks/src/use-design-tokens.ts
@@ -24,6 +24,7 @@
colorPrimary: '',
colorSuccess: '',
colorTextBase: '',
+ colorTextLightSolid: '',
colorWarning: '',
zIndexPopupBase: 2000, // 璋冩暣鍩虹寮瑰眰灞傜骇锛岄伩鍏嶄笅鎷夌瓑缁勪欢琚脊绐楁垨鑰呮渶澶у寲鐘舵�佷笅鐨勮〃鏍奸伄鎸�
});
@@ -48,7 +49,7 @@
tokens.colorTextBase = getCssVariableValue('--foreground');
- getCssVariableValue('--primary-foreground');
+ tokens.colorTextLightSolid = getCssVariableValue('--primary-foreground');
tokens.colorBorderSecondary = tokens.colorBorder =
getCssVariableValue('--border');
diff --git a/src/packages/effects/request/src/request-client/preset-interceptors.ts b/src/packages/effects/request/src/request-client/preset-interceptors.ts
index 97bb8cb..7e168e2 100644
--- a/src/packages/effects/request/src/request-client/preset-interceptors.ts
+++ b/src/packages/effects/request/src/request-client/preset-interceptors.ts
@@ -52,7 +52,7 @@
formatToken,
}: {
client: RequestClient;
- doReAuthenticate: () => Promise<void>;
+ doReAuthenticate: (error?: any) => Promise<void>;
doRefreshToken: () => Promise<string>;
enableRefreshToken: boolean;
formatToken: (token: string) => null | string;
@@ -67,13 +67,14 @@
// 鍒ゆ柇鏄惁鍚敤浜� refreshToken 鍔熻兘
// 濡傛灉娌℃湁鍚敤鎴栬�呭凡缁忔槸閲嶈瘯璇锋眰浜嗭紝鐩存帴璺宠浆鍒伴噸鏂扮櫥褰�
if (!enableRefreshToken || config.__isRetryRequest) {
- await doReAuthenticate();
+ await doReAuthenticate(error);
throw error;
}
// 濡傛灉姝e湪鍒锋柊 token锛屽垯灏嗚姹傚姞鍏ラ槦鍒楋紝绛夊緟鍒锋柊瀹屾垚
if (client.isRefreshing) {
- return new Promise((resolve) => {
- client.refreshTokenQueue.push((newToken: string) => {
+ return new Promise((resolve, reject) => {
+ client.refreshTokenQueue.push((newToken: string, refreshError?: unknown) => {
+ if (refreshError) { reject(refreshError); return; }
config.headers.Authorization = formatToken(newToken);
resolve(client.request(config.url, { ...config }));
});
@@ -96,10 +97,10 @@
return client.request(error.config.url, { ...error.config });
} catch (refreshError) {
// 濡傛灉鍒锋柊 token 澶辫触锛屽鐞嗛敊璇紙濡傚己鍒剁櫥鍑烘垨璺宠浆鐧诲綍椤甸潰锛�
- client.refreshTokenQueue.forEach((callback) => callback(''));
+ client.refreshTokenQueue.forEach((callback) => callback('', refreshError));
client.refreshTokenQueue = [];
console.error('Refresh token failed, please login again.');
- await doReAuthenticate();
+ await doReAuthenticate(error);
throw refreshError;
} finally {
diff --git a/src/packages/effects/request/src/request-client/request-client.ts b/src/packages/effects/request/src/request-client/request-client.ts
index c1c8f30..382784b 100644
--- a/src/packages/effects/request/src/request-client/request-client.ts
+++ b/src/packages/effects/request/src/request-client/request-client.ts
@@ -47,7 +47,7 @@
public isRefreshing = false;
public postSSE: SSE['postSSE'];
// 鍒锋柊token闃熷垪
- public refreshTokenQueue: ((token: string) => void)[] = [];
+ public refreshTokenQueue: ((token: string, error?: unknown) => void)[] = [];
public requestSSE: SSE['requestSSE'];
public upload: FileUploader['upload'];
diff --git a/src/preferences.ts b/src/preferences.ts
index 4da3a4e..c33de43 100644
--- a/src/preferences.ts
+++ b/src/preferences.ts
@@ -20,6 +20,10 @@
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
+ theme: {
+ builtinType: 'custom',
+ colorPrimary: '#a51d24',
+ },
app: {
/** 鍚庣璺敱妯″紡 */
accessMode: 'backend',
diff --git a/src/store/auth.ts b/src/store/auth.ts
index 15fd19e..c2b1dd6 100644
--- a/src/store/auth.ts
+++ b/src/store/auth.ts
@@ -29,6 +29,8 @@
const router = useRouter();
const loginLoading = ref(false);
+ let sessionVersion = 0;
+ let logoutTask: Promise<void> | null = null;
/**
* 寮傛澶勭悊鐧诲綍鎿嶄綔
@@ -44,6 +46,7 @@
) {
// 寮傛澶勭悊鐢ㄦ埛鐧诲綍鎿嶄綔骞惰幏鍙� accessToken
let userInfo: null | UserInfo = null;
+ const version = ++sessionVersion;
try {
let loginResult: AuthApi.LoginResult;
loginLoading.value = true;
@@ -65,6 +68,7 @@
}
}
const { accessToken, refreshToken } = loginResult;
+ if (version !== sessionVersion) return { userInfo: null };
// 濡傛灉鎴愬姛鑾峰彇鍒� accessToken
if (accessToken) {
@@ -78,6 +82,7 @@
// // getAccessCodesApi(),
// ]);
const fetchUserInfoResult = await fetchUserInfo();
+ if (version !== sessionVersion) return { userInfo: null };
userInfo = fetchUserInfoResult.user;
@@ -98,8 +103,11 @@
});
}
}
+ } catch (error) {
+ if (version === sessionVersion) throw error;
+ return { userInfo: null };
} finally {
- loginLoading.value = false;
+ if (version === sessionVersion) loginLoading.value = false;
}
return {
@@ -107,16 +115,28 @@
};
}
- async function logout(redirect: boolean = true) {
+ function logout(redirect: boolean = true): Promise<void> {
+ if (logoutTask) return logoutTask;
+ const accessToken = accessStore.accessToken;
+ const tenant = { tenantId: accessStore.tenantId, visitTenantId: accessStore.visitTenantId };
+ const fullPath = router.currentRoute.value.fullPath;
+ ++sessionVersion;
+ // 鏈嶅姟鍣ㄦ敞閿�鐙珛鎵ц锛屾湰鍦版竻鐞嗕笌璺宠浆涓嶇瓑寰呯綉缁溿��
+ // 浣跨敤閫�鍑哄墠鐨� token 鍜岀鎴峰揩鐓э紝閬垮厤娓呯悊鍚庝涪澶辨敞閿�涓婁笅鏂囥��
+ if (accessToken) void logoutApi(accessToken, tenant).catch(() => {});
+ logoutTask = clearSessionAndNavigate(redirect, fullPath).finally(() => { logoutTask = null; });
+ return logoutTask;
+ }
+
+ async function clearSessionAndNavigate(redirect: boolean, fullPath: string) {
try {
- const accessToken = accessStore.accessToken as string;
- if (accessToken) {
- await logoutApi(accessToken);
- }
- } catch {
- // 涓嶅仛浠讳綍澶勭悊
+ resetAllStores();
+ } catch (error) {
+ console.error('Failed to reset business stores during logout', error);
}
- resetAllStores();
+ // 璁よ瘉娓呯悊涓嶈兘渚濊禆涓氬姟 store 鐨勯噸缃粨鏋溿��
+ accessStore.$reset();
+ userStore.$reset();
accessStore.setLoginExpired(false);
// 鍥炵櫥褰曢〉甯︿笂褰撳墠璺敱鍦板潃
@@ -124,18 +144,26 @@
path: LOGIN_PATH,
query: redirect
? {
- redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ redirect: encodeURIComponent(fullPath),
}
: {},
});
}
async function fetchUserInfo() {
+ const version = sessionVersion;
+ const accessToken = accessStore.accessToken;
// 鍔犺浇
// eslint-disable-next-line no-useless-assignment
let authPermissionInfo: AuthPermissionInfo | null = null;
authPermissionInfo = await getAuthPermissionInfoApi();
+ if (version !== sessionVersion || accessToken !== accessStore.accessToken) {
+ throw new Error('鐧诲綍鐘舵�佸凡鍙樺寲锛屽拷鐣ユ棫浼氳瘽鐨勭敤鎴蜂俊鎭�');
+ }
await loadAuditPolicy();
+ if (version !== sessionVersion || accessToken !== accessStore.accessToken) {
+ throw new Error('鐧诲綍鐘舵�佸凡鍙樺寲锛屽拷鐣ユ棫浼氳瘽鐨勭敤鎴蜂俊鎭�');
+ }
// userStore
userStore.setUserInfo(authPermissionInfo.user);
userStore.setUserRoles(authPermissionInfo.roles);
diff --git a/src/styles/herbal-theme.css b/src/styles/herbal-theme.css
new file mode 100644
index 0000000..86ddaa2
--- /dev/null
+++ b/src/styles/herbal-theme.css
@@ -0,0 +1,59 @@
+/* 鍝佺墝涓�ц壊涓庝富鑹插垎绂伙細涓昏壊浠嶉�氳繃 Vben preferences 鐢熸垚鍏ㄩ儴鑹查樁銆� */
+html[data-brand-theme='herbal']:not(.dark) {
+ --background: 40 50% 99%;
+ --background-deep: 37 33% 95%;
+ --foreground: 27 16% 17%;
+ --card: 40 50% 99%;
+ --card-foreground: 27 16% 17%;
+ --popover: 40 50% 99%;
+ --popover-foreground: 27 16% 17%;
+ --muted: 35 25% 94%;
+ --muted-foreground: 28 10% 40%;
+ --secondary: 35 25% 94%;
+ --secondary-foreground: 27 16% 17%;
+ --accent: 33 35% 93%;
+ --accent-dark: 33 28% 90%;
+ --accent-darker: 33 24% 87%;
+ --accent-lighter: 38 45% 97%;
+ --accent-hover: 33 28% 90%;
+ --accent-foreground: 27 16% 17%;
+ --border: 33 25% 86%;
+ --input: 33 25% 86%;
+ --sidebar: 40 50% 99%;
+ --sidebar-deep: 37 35% 97%;
+ --menu: var(--sidebar);
+ --header: 40 50% 99%;
+}
+html[data-brand-theme='herbal'].dark {
+ --background: 26 16% 12%;
+ --background-deep: 25 18% 9%;
+ --foreground: 35 30% 90%;
+ --card: 26 15% 14%;
+ --card-foreground: 35 30% 90%;
+ --popover: 26 15% 14%;
+ --popover-foreground: 35 30% 90%;
+ --muted: 26 13% 19%;
+ --muted-foreground: 33 15% 68%;
+ --border: 27 15% 26%;
+ --input: 27 15% 26%;
+ --sidebar: 26 16% 12%;
+ --sidebar-deep: 25 18% 10%;
+ --menu: var(--sidebar);
+ --header: 26 16% 12%;
+}
+/* 鏆楄壊涓嬩娇鐢ㄦ槑浜湵鐮傝壊锛屼繚璇侀摼鎺ヤ笌閫変腑鎬佸彲璇伙紱鎸夐挳鎼厤娣辫壊鏂囧瓧銆� */
+html[data-brand-theme='herbal'][data-herbal-palette='true'].dark {
+ --primary: 357 64% 73% !important;
+ --primary-500: 357 64% 73% !important;
+ --primary-600: 357 69% 77% !important;
+ --primary-700: 357 70% 83% !important;
+ --primary-foreground: 27 25% 12%;
+}
+/* 鏀寔鍗婃繁鑹插鑸亸濂斤紝涓嶈娴呰壊鍝佺墝搴曡鐩栬鍋忓ソ銆� */
+html[data-brand-theme='herbal']:not(.dark) .dark {
+ --sidebar: 26 16% 12%;
+ --sidebar-deep: 25 18% 10%;
+ --menu: var(--sidebar);
+ --header: 26 16% 12%;
+}
+
diff --git a/src/utils/rangePickerProps.ts b/src/utils/rangePickerProps.ts
index 2ba4be2..4c6c244 100644
--- a/src/utils/rangePickerProps.ts
+++ b/src/utils/rangePickerProps.ts
@@ -66,3 +66,13 @@
},
};
}
+
+/** 鎸変笟鍔℃棩鏈熺瓫閫夛紝鎺ュ彛浠嶆帴鏀跺畬鏁存椂闂村瓧绗︿覆銆� */
+export function getDateRangePickerDefaultProps() {
+ return {
+ ...getRangePickerDefaultProps(),
+ format: 'YYYY-MM-DD',
+ placeholder: ['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡'],
+ showTime: false,
+ };
+}
diff --git a/src/views/_core/authentication/login.vue b/src/views/_core/authentication/login.vue
index 16e9f68..4e46c22 100644
--- a/src/views/_core/authentication/login.vue
+++ b/src/views/_core/authentication/login.vue
@@ -97,6 +97,8 @@
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
+ autocomplete: 'username',
+ 'aria-label': $t('authentication.username'),
},
fieldName: 'username',
label: $t('authentication.username'),
@@ -109,6 +111,8 @@
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.passwordTip'),
+ autocomplete: 'current-password',
+ 'aria-label': $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
@@ -122,7 +126,7 @@
</script>
<template>
- <div>
+ <div class="herbal-login">
<AuthenticationLogin
ref="loginRef"
:form-schema="formSchema"
@@ -134,7 +138,14 @@
:show-forget-password="false"
:show-doc-link="false"
@submit="handleLogin"
- />
+ >
+ <template #title>
+ <div class="herbal-login__heading">
+ <h1>{{ $t('authentication.welcomeBack') }}<svg class="herbal-login__seal" viewBox="0 0 20 30" aria-hidden="true"><rect x="1" y="1" width="18" height="28" rx="2"/><path d="M5 5h10v8H5V5Zm0 12h10v8H5v-8Zm5-12v8m0 4v8"/></svg></h1>
+ <p>{{ $t('authentication.loginSubtitle') }}</p>
+ </div>
+ </template>
+ </AuthenticationLogin>
<Verification
ref="verifyRef"
v-if="captchaEnable"
@@ -147,3 +158,24 @@
/>
</div>
</template>
+
+<style scoped>
+.herbal-login__heading{margin-bottom:32px}
+.herbal-login__heading h1{font-size:clamp(28px,2.4vw,36px);font-weight:700;line-height:1.4;letter-spacing:-.02em;margin:0 0 12px;color:#29251f;display:flex;align-items:center;gap:14px}
+.herbal-login__heading p{font-size:15px;line-height:1.7;color:#756d63;margin:0}
+.herbal-login__seal{width:20px;height:30px;flex-shrink:0;color:#a51d24;fill:none;stroke:currentColor;stroke-width:1.2}
+.herbal-login :deep(input:not([type='checkbox'])){min-height:56px;border-radius:9px;border:1px solid #ddd6cc;background:rgba(255,255,255,.82);padding:12px 16px;font-size:16px;color:#30281f}
+.herbal-login :deep(input::placeholder){color:#7a7167;opacity:1}
+.herbal-login :deep(input:focus-visible){outline:2px solid #a51d24;outline-offset:3px}
+.herbal-login :deep(input[autocomplete='current-password']){padding-right:44px}
+.herbal-login :deep(button[aria-label='login']){min-height:58px;margin-top:8px;border-radius:9px;background:#a51d24;color:#fff;font-size:19px;font-weight:600;letter-spacing:.12em;box-shadow:0 5px 12px rgba(137,24,30,.12)}
+.herbal-login :deep(button[aria-label='login']:hover){background:#87171d}
+.herbal-login :deep(button[aria-label='login']:focus-visible){outline:2px solid #a51d24;outline-offset:4px}
+.herbal-login :deep(button[aria-label='login']:disabled){opacity:.65;cursor:wait}
+:global(.dark .herbal-login__heading h1){color:#f5eee4}
+:global(.dark .herbal-login__heading p){color:#d1c1ae}
+:global(.dark .herbal-login__seal){color:#eea5a4}
+:global(.dark .herbal-login input:not([type='checkbox'])){background:#302720;border-color:#6f5b48;color:#f5eee4}
+:global(.dark .herbal-login input::placeholder){color:#c2b29e}
+@media(max-width:540px){.herbal-login__heading h1{font-size:28px}.herbal-login__heading p{font-size:14px}.herbal-login :deep(input:not([type='checkbox'])){min-height:52px}}
+</style>
diff --git a/src/views/bi/dashboard/index.vue b/src/views/bi/dashboard/index.vue
index 98072c2..87299c4 100644
--- a/src/views/bi/dashboard/index.vue
+++ b/src/views/bi/dashboard/index.vue
@@ -12,6 +12,7 @@
import ChartCard from './modules/ChartCard.vue';
import ClockWidget from './modules/ClockWidget.vue';
+import RealtimeTable from './modules/RealtimeTable.vue';
import { DASHBOARD_META, getDashboardCode, getDefaultCharts } from './data';
defineOptions({ name: 'BiDashboard' });
@@ -24,26 +25,38 @@
const loading = ref(true);
const lastUpdateTime = ref('');
let dataRefreshTimer: ReturnType<typeof setInterval> | undefined;
+let requestSequence = 0;
+let fetching = false;
+let disposed = false;
async function loadData(background = false) {
- if (!meta.value) return;
+ if (!meta.value || disposed || (background && fetching)) return;
+ const sequence = ++requestSequence;
+ const code = meta.value.code;
+ fetching = true;
if (!background && dataRefreshTimer) clearInterval(dataRefreshTimer);
try {
if (!background) loading.value = true;
- const data = await getDashboardData(meta.value.code);
+ const data = await getDashboardData(code);
+ if (disposed || sequence !== requestSequence) return;
charts.value = data.length > 0 ? data : getDefaultCharts();
lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN', { hour12: false });
} catch {
- if (!background) charts.value = getDefaultCharts();
+ if (!background && !disposed && sequence === requestSequence) charts.value = getDefaultCharts();
} finally {
- loading.value = false;
- await nextTick();
- resizeAllCharts();
- if (!background) {
- const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0);
- if (intervals.length) dataRefreshTimer = setInterval(() => {
- if (!loading.value && document.visibilityState === 'visible') loadData(true);
- }, Math.max(15, Math.min(...intervals)) * 1000);
+ if (!disposed && sequence === requestSequence) {
+ fetching = false;
+ loading.value = false;
+ await nextTick();
+ if (!disposed && sequence === requestSequence) {
+ resizeAllCharts();
+ if (!background) {
+ const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0);
+ if (intervals.length) dataRefreshTimer = setInterval(() => {
+ if (!loading.value && document.visibilityState === 'visible') loadData(true);
+ }, Math.max(15, Math.min(...intervals)) * 1000);
+ }
+ }
}
}
}
@@ -161,11 +174,6 @@
return val;
}
-// 澶у睆琛ㄦ牸銆屽疄鏃舵粴鍔ㄣ�嶏細鏁版嵁澶嶅埗涓�浠斤紝閰嶅悎 CSS marquee 鍋氭棤缂濈旱鍚戞粴鍔�
-function duplicateRows(rows?: Record<string, unknown>[]): Record<string, unknown>[] {
- return rows ? [...rows, ...rows] : [];
-}
-
// ======== 鐪嬫澘涓婚鑹� ========
const dashboardAccentColors: Record<string, string> = {
'purchase-sales': '#6366f1',
@@ -205,6 +213,8 @@
loadData();
});
onBeforeUnmount(() => {
+ disposed = true;
+ requestSequence++;
if (dataRefreshTimer) clearInterval(dataRefreshTimer);
document.removeEventListener('fullscreenchange', onFullscreenChange);
resizeObserver?.disconnect();
@@ -311,30 +321,11 @@
<span class="table-header-title">{{ chart.name }}</span>
<span class="table-badge">瀹炴椂婊氬姩</span>
</div>
- <div v-if="chart.data && chart.data.length > 0" class="table-wrap">
- <table class="data-table">
- <thead>
- <tr>
- <th
- v-for="key in Object.keys(chart.data[0] || {})"
- :key="key"
- >{{ key }}</th>
- </tr>
- </thead>
- <tbody class="marquee-tbody">
- <tr
- v-for="(row, ri) in duplicateRows(chart.data)"
- :key="ri"
- :style="{ animationDelay: `${ri * 50}ms` }"
- >
- <td
- v-for="key in Object.keys(chart.data[0] || {})"
- :key="key"
- >{{ getCellValue(row, key) }}</td>
- </tr>
- </tbody>
- </table>
- </div>
+ <RealtimeTable
+ v-if="chart.data && chart.data.length > 0"
+ :rows="chart.data"
+ :format-cell="getCellValue"
+ />
<Empty v-else description="鏆傛棤鏁版嵁" />
</div>
</template>
@@ -612,71 +603,6 @@
padding: 2px 8px; border-radius: 10px;
background: rgba(0,255,136,0.08); color: var(--green);
border: 1px solid rgba(0,255,136,0.15);
-}
-
-.table-wrap {
- overflow-x: auto;
- overflow-y: hidden;
- max-height: 320px;
- border-radius: 8px;
- background: rgba(4, 14, 36, 0.55);
- backdrop-filter: blur(6px);
- -webkit-backdrop-filter: blur(6px);
- border: 1px solid rgba(0, 229, 255, 0.08);
-}
-
-.data-table {
- width: 100%; border-collapse: collapse; font-size: 12px;
-}
-
-.data-table thead {
- position: sticky;
- top: 0;
- z-index: 2;
-}
-
-.data-table thead th {
- background: rgba(6, 18, 42, 0.8);
- color: var(--text-secondary);
- font-weight: 600; font-size: 10px;
- text-transform: uppercase; letter-spacing: 0.8px;
- padding: 10px 14px; text-align: left;
- border-bottom: 1px solid rgba(0, 229, 255, 0.12);
- white-space: nowrap;
-}
-
-.data-table tbody td {
- padding: 9px 14px; color: var(--text-primary);
- border-bottom: 1px solid rgba(0, 229, 255, 0.04);
- white-space: nowrap;
-}
-
-.data-table tbody tr {
- transition: all 0.25s;
- opacity: 0;
- animation: row-fade-in 0.45s ease forwards;
-}
-
-.data-table tbody tr:hover {
- background: rgba(0, 229, 255, 0.06);
- box-shadow: inset 0 0 20px rgba(0, 229, 255, 0.03);
-}
-
-@keyframes row-fade-in {
- from { opacity: 0; transform: translateY(6px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-/* 澶у睆琛ㄦ牸銆屽疄鏃舵粴鍔ㄣ�嶏細tbody 澶嶅埗涓�浠藉悗 translateY(-50%) 鏃犵紳寰幆 */
-.marquee-tbody {
- animation: table-marquee 15s linear infinite;
-}
-.marquee-tbody:hover {
- animation-play-state: paused;
-}
-@keyframes table-marquee {
- from { transform: translateY(0); }
- to { transform: translateY(-50%); }
}
/* ======== 绌虹姸鎬� ======== */
diff --git a/src/views/bi/dashboard/modules/RealtimeTable.vue b/src/views/bi/dashboard/modules/RealtimeTable.vue
new file mode 100644
index 0000000..ec9131d
--- /dev/null
+++ b/src/views/bi/dashboard/modules/RealtimeTable.vue
@@ -0,0 +1,164 @@
+<script setup lang="ts">
+import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
+
+const props = withDefaults(defineProps<{
+ rows: Record<string, unknown>[];
+ formatCell?: (row: Record<string, unknown>, column: string) => unknown;
+ maxHeight?: number;
+ speed?: number;
+}>(), { maxHeight: 260, speed: 24 });
+
+const columns = computed(() => Object.keys(props.rows[0] ?? {}));
+const viewport = ref<HTMLElement>();
+const track = ref<HTMLElement>();
+const original = ref<HTMLElement>();
+const scrolling = ref(false);
+const hovered = ref(false);
+const focused = ref(false);
+const reducedMotion = ref(false);
+let animation: Animation | undefined;
+let observer: ResizeObserver | undefined;
+let motionQuery: MediaQueryList | undefined;
+let frame = 0;
+let distance = 0;
+let duration = 0;
+let active = true;
+let disposed = false;
+let measuring = false;
+
+function syncPlayback() {
+ if (!animation) return;
+ if (hovered.value || focused.value || document.hidden || !active) animation.pause();
+ else animation.play();
+}
+
+async function measure() {
+ if (disposed || measuring || !viewport.value || !original.value || !track.value) return;
+ measuring = true;
+ try {
+ const height = original.value.offsetHeight;
+ const shouldScroll = !reducedMotion.value && height > viewport.value.clientHeight + 1;
+ const nextDuration = height / Math.max(1, props.speed) * 1000;
+ if (shouldScroll && animation && height === distance && duration === nextDuration) return;
+ const offset = distance && duration
+ ? (Number(animation?.currentTime ?? 0) % duration) / duration * distance : 0;
+ animation?.cancel();
+ animation = undefined;
+ scrolling.value = shouldScroll;
+ distance = height;
+ duration = nextDuration;
+ await nextTick();
+ if (disposed || !shouldScroll || !track.value) return;
+ // Animate a normal block, not tbody. A second identical table fills the loop seam.
+ animation = track.value.animate([
+ { transform: 'translateY(0)' },
+ { transform: `translateY(-${height}px)` },
+ ], { duration, iterations: Infinity, easing: 'linear' });
+ animation.currentTime = (offset % height) / height * duration;
+ syncPlayback();
+ } finally {
+ measuring = false;
+ }
+}
+
+function scheduleMeasure() {
+ cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(() => { void measure(); });
+}
+
+function onMotionChange() {
+ reducedMotion.value = motionQuery?.matches ?? false;
+ scheduleMeasure();
+}
+
+function onKeydown(event: KeyboardEvent) {
+ if (!animation || !scrolling.value) return;
+ const step = event.key === 'ArrowDown' ? 40 : event.key === 'ArrowUp' ? -40 : 0;
+ if (!step) return;
+ event.preventDefault();
+ const nextTime = Number(animation.currentTime ?? 0) + step / Math.max(1, props.speed) * 1000;
+ animation.currentTime = ((nextTime % duration) + duration) % duration;
+}
+
+watch(() => [props.rows, props.maxHeight, props.speed], scheduleMeasure, { flush: 'post' });
+watch([hovered, focused], syncPlayback);
+onMounted(() => {
+ motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+ motionQuery.addEventListener('change', onMotionChange);
+ onMotionChange();
+ observer = new ResizeObserver(scheduleMeasure);
+ if (viewport.value) observer.observe(viewport.value);
+ if (original.value) observer.observe(original.value);
+ document.addEventListener('visibilitychange', syncPlayback);
+});
+onActivated(() => { active = true; scheduleMeasure(); syncPlayback(); });
+onDeactivated(() => { active = false; syncPlayback(); });
+onBeforeUnmount(() => {
+ disposed = true;
+ cancelAnimationFrame(frame);
+ animation?.cancel();
+ observer?.disconnect();
+ motionQuery?.removeEventListener('change', onMotionChange);
+ document.removeEventListener('visibilitychange', syncPlayback);
+});
+</script>
+
+<template>
+ <div
+ class="realtime-table"
+ role="region"
+ aria-label="瀹炴椂鏁版嵁琛紝鑱氱劍鎴栨偓鍋滄殏鍋滄粴鍔紝鏂瑰悜閿�愯鏌ョ湅"
+ tabindex="0"
+ :style="{ '--table-height': `${maxHeight}px`, '--table-width': `${columns.length * 120}px` }"
+ @mouseenter="hovered = true"
+ @mouseleave="hovered = false"
+ @focusin="focused = true"
+ @focusout="focused = false"
+ @keydown="onKeydown"
+ >
+ <div class="table-content">
+ <table class="header-table">
+ <thead><tr><th v-for="column in columns" :key="column" scope="col">{{ column }}</th></tr></thead>
+ </table>
+ <div ref="viewport" class="table-viewport" :class="{ 'is-scrolling': scrolling }">
+ <div ref="track" class="table-track" :class="{ 'is-scrolling': scrolling }">
+ <div ref="original">
+ <table>
+ <thead class="sr-only"><tr><th v-for="column in columns" :key="column" scope="col">{{ column }}</th></tr></thead>
+ <tbody>
+ <tr v-for="(row, index) in rows" :key="index">
+ <td v-for="column in columns" :key="column" :title="String(formatCell ? formatCell(row, column) ?? '' : row[column] ?? '')">
+ {{ formatCell ? formatCell(row, column) : row[column] }}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <table v-if="scrolling" aria-hidden="true">
+ <tbody>
+ <tr v-for="(row, index) in rows" :key="index">
+ <td v-for="column in columns" :key="column">
+ {{ formatCell ? formatCell(row, column) : row[column] }}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.realtime-table { overflow-x: auto; border: 1px solid rgba(0, 229, 255, 0.08); border-radius: 8px; background: rgba(4, 14, 36, 0.55); }
+.realtime-table:focus-visible { outline: 2px solid var(--cyan, #00e5ff); outline-offset: 2px; }
+.table-content { min-width: var(--table-width); }
+table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 12px; }
+th { padding: 10px 14px; background: rgb(6, 18, 42); color: var(--text-secondary, #b9c4dc); font-size: 10px; font-weight: 600; text-align: left; border-bottom: 1px solid rgba(0, 229, 255, 0.12); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+td { padding: 9px 14px; color: var(--text-primary, #ebf0fc); border-bottom: 1px solid rgba(0, 229, 255, 0.04); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+tbody tr:hover { background: rgba(0, 229, 255, 0.06); }
+.table-viewport { max-height: var(--table-height); overflow-y: auto; scrollbar-width: thin; }
+.table-viewport.is-scrolling { overflow-y: hidden; }
+.table-track.is-scrolling { will-change: transform; }
+.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); }
+</style>
diff --git a/src/views/bi/warehouse/index.vue b/src/views/bi/warehouse/index.vue
index dca30fd..120caaa 100644
--- a/src/views/bi/warehouse/index.vue
+++ b/src/views/bi/warehouse/index.vue
@@ -17,6 +17,7 @@
} from '#/api/bi/warehouseCoordinate';
import ClockWidget from '../dashboard/modules/ClockWidget.vue';
+import RealtimeTable from '../dashboard/modules/RealtimeTable.vue';
import {
getChinaMapOptions,
getWarehouseAreaLineOptions,
@@ -30,19 +31,29 @@
const charts = ref<BiDashboardApi.ChartItem[]>([]);
const loading = ref(true);
const lastUpdateTime = ref('');
+let fetching = false;
+let disposed = false;
-async function loadData() {
+async function loadData(background = false) {
+ if (fetching || disposed) return false;
+ fetching = true;
try {
- loading.value = true;
+ if (!background) loading.value = true;
const data = await getDashboardData('warehouse');
+ if (disposed) return false;
charts.value = data.length > 0 ? data : [];
lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN', { hour12: false });
+ return true;
} catch {
- charts.value = [];
+ if (!background && !disposed) charts.value = [];
+ return false;
} finally {
- loading.value = false;
- await nextTick();
- resizeAllCharts();
+ fetching = false;
+ if (!disposed) {
+ loading.value = false;
+ await nextTick();
+ if (!disposed) resizeAllCharts();
+ }
}
}
@@ -233,10 +244,7 @@
// ======== 琛ㄦ牸鏁版嵁 ========
const tableData = computed(() => tableChart.value?.data || []);
-const tableColumns = computed(() => {
- if (tableData.value.length === 0) return [];
- return Object.keys(tableData.value[0] || {});
-});
+
// ======== 搴撳瓨棰勮 ========
const warningList = computed(() => {
@@ -428,7 +436,8 @@
function setupTimer() {
clearTimer();
refreshTimer = setInterval(async () => {
- await loadData();
+ if (document.hidden || editMode.value || disposed) return;
+ if (!(await loadData(true))) return;
await nextTick();
await renderAllCharts();
if (editMode.value) await nextTick().then(() => attachMapListeners());
@@ -497,13 +506,16 @@
resizeObserver = new ResizeObserver(() => resizeAllCharts());
if (dashboardRef.value) resizeObserver.observe(dashboardRef.value);
await loadCoordinates();
+ if (disposed) return;
await loadData();
+ if (disposed) return;
await nextTick();
await renderAllCharts();
setupTimer();
});
onBeforeUnmount(() => {
+ disposed = true;
document.removeEventListener('fullscreenchange', onFullscreenChange);
resizeObserver?.disconnect();
clearTimer();
@@ -720,26 +732,11 @@
鏈�杩戝叆搴撹褰�
<span class="wh-table-badge">瀹炴椂婊氬姩</span>
</div>
- <div v-if="tableData.length > 0" class="wh-table-wrap">
- <table class="wh-table">
- <thead>
- <tr>
- <th v-for="col in tableColumns" :key="col">{{ col }}</th>
- </tr>
- </thead>
- <tbody>
- <tr
- v-for="(row, ri) in tableData"
- :key="ri"
- :style="{ animationDelay: `${ri * 50}ms` }"
- >
- <td v-for="col in tableColumns" :key="col">
- {{ getCellValue(row, col) }}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
+ <RealtimeTable
+ v-if="tableData.length > 0"
+ :rows="tableData"
+ :format-cell="getCellValue"
+ />
<Empty v-else description="鏆傛棤鏁版嵁" />
</div>
</template>
@@ -1187,50 +1184,6 @@
padding: 2px 8px; border-radius: 10px;
background: rgba(0,255,136,0.08); color: var(--green);
border: 1px solid rgba(0,255,136,0.15);
-}
-
-.wh-table-wrap {
- overflow-x: auto; border-radius: 8px;
- background: rgba(4, 14, 36, 0.55);
- backdrop-filter: blur(6px);
- -webkit-backdrop-filter: blur(6px);
- border: 1px solid rgba(0, 229, 255, 0.08);
-}
-
-.wh-table {
- width: 100%; border-collapse: collapse; font-size: 12px;
-}
-
-.wh-table thead th {
- background: rgba(6, 18, 42, 0.8);
- color: var(--text-secondary);
- font-weight: 600; font-size: 10px;
- text-transform: uppercase; letter-spacing: 0.8px;
- padding: 10px 14px; text-align: left;
- border-bottom: 1px solid rgba(0,229,255,0.12);
- white-space: nowrap;
-}
-
-.wh-table tbody td {
- padding: 9px 14px; color: var(--text-primary);
- border-bottom: 1px solid rgba(0, 229, 255, 0.04);
- white-space: nowrap;
-}
-
-.wh-table tbody tr {
- transition: all 0.25s;
- opacity: 0;
- animation: row-fade-in 0.45s ease forwards;
-}
-
-.wh-table tbody tr:hover {
- background: rgba(0, 229, 255, 0.06);
- box-shadow: inset 0 0 20px rgba(0, 229, 255, 0.03);
-}
-
-@keyframes row-fade-in {
- from { opacity: 0; transform: translateY(6px); }
- to { opacity: 1; transform: translateY(0); }
}
/* ======== 鍏ㄥ睆 ======== */
diff --git a/src/views/crm/invoice/data.ts b/src/views/crm/invoice/data.ts
index a5db7b2..8cb2524 100644
--- a/src/views/crm/invoice/data.ts
+++ b/src/views/crm/invoice/data.ts
@@ -194,6 +194,15 @@
},
},
{
+ fieldName: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ彂绁ㄥ彿鐮侊紝鏀寔妯$硦鏌ヨ',
+ allowClear: true,
+ },
+ },
+ {
fieldName: 'customerId',
label: '瀹㈡埛',
component: markRaw(CrmCustomerSelect),
diff --git a/src/views/mes/dv/repair/modules/form.vue b/src/views/mes/dv/repair/modules/form.vue
index 214e069..ba28ebe 100644
--- a/src/views/mes/dv/repair/modules/form.vue
+++ b/src/views/mes/dv/repair/modules/form.vue
@@ -4,6 +4,7 @@
import type { MesDvRepairApi } from '#/api/mes/dv/repair';
import { computed, ref } from 'vue';
+import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui';
import { MesDvRepairResultEnum, MesDvRepairStatusEnum } from '@vben/constants';
@@ -23,6 +24,8 @@
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
+import SpareRecords from '../../spare/modules/records.vue';
+const { hasAccessByCodes } = useAccess();
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
@@ -210,6 +213,13 @@
:disabled="isLineReadonly"
:repair-id="formData.id"
/>
+ <SpareRecords
+ v-if="formData?.id && hasAccessByCodes(['mes:dv-spare:record'])"
+ :key="formData.id"
+ :repair-id="formData.id"
+ :repair-code="formData.code"
+ :can-issue="[0, 1].includes(formData.status ?? -1)"
+ />
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
diff --git a/src/views/mes/dv/spare-record/index.vue b/src/views/mes/dv/spare-record/index.vue
new file mode 100644
index 0000000..0966af3
--- /dev/null
+++ b/src/views/mes/dv/spare-record/index.vue
@@ -0,0 +1,6 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+import Records from '../spare/modules/records.vue';
+</script>
+<template><Page><Records /></Page></template>
+
diff --git a/src/views/mes/dv/spare/index.vue b/src/views/mes/dv/spare/index.vue
new file mode 100644
index 0000000..fdc250c
--- /dev/null
+++ b/src/views/mes/dv/spare/index.vue
@@ -0,0 +1,64 @@
+<script setup lang="ts">
+import type { SparePart } from '#/api/mes/dv/spare';
+import { onMounted, reactive, ref } from 'vue';
+import { Page, useVbenModal } from '@vben/common-ui';
+import { Alert, Button, Checkbox, Form, Input, Popconfirm, Select, Space, Table, Tag } from 'ant-design-vue';
+import { deleteSpare, getSparePage } from '#/api/mes/dv/spare';
+import SpareForm from './modules/form.vue';
+import Action from './modules/action.vue';
+import Records from './modules/records.vue';
+const rows=ref<SparePart[]>([]),loading=ref(false),error=ref('');
+const page=ref(1),size=ref(10),total=ref(0),selected=ref<SparePart>();
+const filters=reactive<{code?:string;name?:string;status?:number;shortageOnly:boolean}>({shortageOnly:false});
+const [FormModal,formApi]=useVbenModal({connectedComponent:SpareForm,destroyOnClose:true});
+const [ActionModal,actionApi]=useVbenModal({connectedComponent:Action,destroyOnClose:true});
+const [RecordsModal,recordsApi]=useVbenModal({showConfirmButton:false});
+let seq=0;
+const columns=[{title:'澶囦欢缂栫爜',dataIndex:'code',width:140},{title:'澶囦欢鍚嶇О',dataIndex:'name',width:180},
+ {title:'瑙勬牸鍨嬪彿',dataIndex:'specification',width:160},{title:'鍗曚綅',dataIndex:'unit',width:80},
+ {title:'鍙敤搴撳瓨',dataIndex:'stockQuantity',key:'stock',width:130},{title:'瀹夊叏搴撳瓨',dataIndex:'safetyQuantity',width:110},
+ {title:'瀛樻斁浣嶇疆',dataIndex:'location',width:150},{title:'鐘舵��',dataIndex:'status',key:'status',width:90},
+ {title:'鎿嶄綔',key:'action',width:290,fixed:'right' as const}];
+async function load(reset=false) {
+ if(reset)page.value=1;const current=++seq;loading.value=true;error.value='';
+ try{const result=await getSparePage({pageNo:page.value,pageSize:size.value,...filters});if(current===seq){rows.value=result.list;total.value=result.total;}}
+ catch{if(current===seq){rows.value=[];total.value=0;error.value='澶囦欢鍔犺浇澶辫触锛岃鐐瑰嚮鏌ヨ閲嶈瘯';}}
+ finally{if(current===seq)loading.value=false;}
+}
+function reset(){Object.assign(filters,{code:undefined,name:undefined,status:undefined,shortageOnly:false});void load(true);}
+function changePage(p:{current?:number;pageSize?:number}){page.value=p.current||1;size.value=p.pageSize||10;void load();}
+function showForm(part?:SparePart){formApi.setData({id:part?.id}).open();}
+function action(type:'issue'|'receive',part:SparePart){actionApi.setData({type,part}).open();}
+function history(part:SparePart){selected.value=part;recordsApi.open();}
+async function remove(part:SparePart){await deleteSpare(part.id);await load(true);}
+onMounted(()=>load());
+</script>
+<template>
+ <Page>
+ <FormModal @success="load(true)" /><ActionModal @success="load(true)" />
+ <RecordsModal :title="selected ? selected.name+' 路 搴撳瓨涓庨鐢ㄨ褰�' : '搴撳瓨涓庨鐢ㄨ褰�'" class="w-full lg:w-4/5"><Records v-if="selected" :key="selected.id" :spare-part-id="selected.id" /></RecordsModal>
+ <div class="mb-4 flex flex-wrap items-center justify-between gap-3"><h2 class="text-lg font-semibold">澶囦欢鍙拌处</h2><Button v-access:code="['mes:dv-spare:create']" type="primary" @click="showForm()">鏂板澶囦欢</Button></div>
+ <Form layout="inline" class="mb-4 flex flex-wrap gap-y-3">
+ <Form.Item label="澶囦欢缂栫爜"><Input v-model:value="filters.code" placeholder="瀹屾暣澶囦欢缂栫爜" allow-clear @press-enter="load(true)" /></Form.Item>
+ <Form.Item label="澶囦欢鍚嶇О"><Input v-model:value="filters.name" placeholder="杈撳叆澶囦欢鍚嶇О" allow-clear @press-enter="load(true)" /></Form.Item>
+ <Form.Item label="鐘舵��"><Select v-model:value="filters.status" class="w-28" allow-clear :options="[{label:'鍚敤',value:0},{label:'鍋滅敤',value:1}]" /></Form.Item>
+ <Form.Item><Checkbox v-model:checked="filters.shortageOnly">浠呯湅浣庝簬瀹夊叏搴撳瓨</Checkbox></Form.Item>
+ <Form.Item><Space><Button type="primary" :loading="loading" @click="load(true)">鏌ヨ</Button><Button @click="reset">閲嶇疆</Button></Space></Form.Item>
+ </Form>
+ <Alert v-if="error" class="mb-3" type="error" :message="error" />
+ <Table :columns="columns" :data-source="rows" :loading="loading" row-key="id" :scroll="{x:1330}" :pagination="{current:page,pageSize:size,total,showSizeChanger:true}" @change="changePage">
+ <template #bodyCell="{column,record}">
+ <template v-if="column.key==='stock'"><span>{{ record.stockQuantity }}</span><Tag v-if="Number(record.stockQuantity)<Number(record.safetyQuantity)" color="warning" class="ml-2">涓嶈冻</Tag></template>
+ <template v-else-if="column.key==='status'"><Tag :color="record.status===0?'success':'default'">{{ record.status===0?'鍚敤':'鍋滅敤' }}</Tag></template>
+ <template v-else-if="column.key==='action'"><Space :size="0" wrap>
+ <Button v-access:code="['mes:dv-spare:update']" type="link" @click="showForm(record as SparePart)">缂栬緫</Button>
+ <Button v-access:code="['mes:dv-spare:receive']" type="link" :disabled="record.status!==0" @click="action('receive',record as SparePart)">鍏ュ簱</Button>
+ <Button v-access:code="['mes:dv-spare:issue']" type="link" :disabled="record.status!==0 || Number(record.stockQuantity)<=0" @click="action('issue',record as SparePart)">棰嗙敤</Button>
+ <Button v-access:code="['mes:dv-spare:record']" type="link" @click="history(record as SparePart)">璁板綍</Button>
+ <Popconfirm title="纭鍒犻櫎澶囦欢锛熶粎鏃犲簱瀛樹笖鏃犳祦姘寸殑澶囦欢鍙垹闄ゃ��" @confirm="remove(record as SparePart)"><Button v-access:code="['mes:dv-spare:delete']" type="link" danger>鍒犻櫎</Button></Popconfirm>
+ </Space></template>
+ </template>
+ </Table>
+ </Page>
+</template>
+
diff --git a/src/views/mes/dv/spare/modules/action.vue b/src/views/mes/dv/spare/modules/action.vue
new file mode 100644
index 0000000..fb77318
--- /dev/null
+++ b/src/views/mes/dv/spare/modules/action.vue
@@ -0,0 +1,95 @@
+<script setup lang="ts">
+import type { SpareMovement, SparePart, SpareAction } from '#/api/mes/dv/spare';
+import type { FormInstance } from 'ant-design-vue';
+import { computed, reactive, ref } from 'vue';
+import { useVbenModal } from '@vben/common-ui';
+import { Alert, Form, Input, InputNumber, Select } from 'ant-design-vue';
+import { getSparePage, spareAction } from '#/api/mes/dv/spare';
+import { getRepairPage } from '#/api/mes/dv/repair';
+import { getSimpleUserList } from '#/api/system/user';
+const emit = defineEmits(['success']);
+const formRef = ref<FormInstance>();
+const type = ref<'issue'|'receive'|'return'>('issue');
+const saving = ref(false);
+const loadError = ref('');
+const original = ref<SpareMovement>();
+const parts = ref<SparePart[]>([]);
+const repairs = ref<{label:string;value:number}[]>([]);
+const users = ref<{label:string;value:number}[]>([]);
+const model = reactive<SpareAction>({ requestKey: '', quantity: 1, remark: '' });
+const fixedPart = ref(false), fixedRepair = ref(false);
+let partSeq = 0, repairSeq = 0, openSeq = 0;
+const title = computed(() => ({issue:'缁翠慨棰嗙敤澶囦欢',receive:'澶囦欢鍏ュ簱',return:'閫�鍥炴湭浣跨敤澶囦欢'})[type.value]);
+const chosenPart = computed(() => parts.value.find(p => p.id === model.sparePartId));
+const maxQuantity = computed(() => type.value === 'return' ? Number(original.value?.returnableQuantity ?? 0) : type.value === 'issue' ? Number(chosenPart.value?.stockQuantity ?? 0) : undefined);
+const required = [{required:true,message:'璇峰~鍐欐椤�'}];
+async function searchParts(name='') {
+ const seq = ++partSeq;
+ try { const result = await getSparePage({pageNo:1,pageSize:50,status:0,name}); if(seq === partSeq) parts.value=result.list; }
+ catch { if(seq===partSeq) loadError.value='澶囦欢鍔犺浇澶辫触锛岃閲嶆柊鎵撳紑寮圭獥鍚庨噸璇�'; }
+}
+async function searchRepairs(code='') {
+ const seq=++repairSeq;
+ try { const result=await getRepairPage({pageNo:1,pageSize:50,code} as Parameters<typeof getRepairPage>[0]);
+ if(seq===repairSeq) repairs.value=result.list.filter(r=>r.status===0||r.status===1).map(r=>({value:r.id!,label:`${r.code} 路 ${r.machineryName || r.name || ''}`}));
+ } catch { if(seq===repairSeq) loadError.value='缁翠慨鍗曞姞杞藉け璐ワ紝璇烽噸鏂版墦寮�寮圭獥鍚庨噸璇�'; }
+}
+const [Modal, modalApi] = useVbenModal({
+ onBeforeClose:()=>!saving.value,
+ async onOpenChange(open) {
+ const seq=++openSeq;
+ if(!open) { partSeq++; repairSeq++; return; }
+ const data=modalApi.getData<{type:'issue'|'receive'|'return';part?:SparePart;repairId?:number;repairCode?:string;original?:SpareMovement}>();
+ type.value=data.type; original.value=data.original; loadError.value='';
+ fixedPart.value=!!data.part||data.type==='return'; fixedRepair.value=!!data.repairId;
+ Object.assign(model,{requestKey:crypto.randomUUID(),sparePartId:data.part?.id,repairId:data.repairId,originalId:data.original?.id,receiverUserId:undefined,quantity:1,remark:''});
+ parts.value=data.part?[data.part]:[]; repairs.value=[]; users.value=[]; formRef.value?.clearValidate();
+ if(data.repairId) repairs.value=[{value:data.repairId,label:data.repairCode || String(data.repairId)}];
+ modalApi.lock();
+ try {
+ const jobs:Promise<unknown>[]=[];
+ if(data.type!=='return'&&!data.part) jobs.push(searchParts());
+ if(data.type==='issue'&&!data.repairId) jobs.push(searchRepairs());
+ if(data.type==='issue') jobs.push(getSimpleUserList().then(list=>{if(seq===openSeq)users.value=list.filter(u=>u.id!==undefined).map(u=>({value:u.id!,label:u.nickname}));}));
+ await Promise.all(jobs);
+ } catch { if(seq===openSeq) loadError.value='鍩虹鏁版嵁鍔犺浇澶辫触锛岃閲嶆柊鎵撳紑寮圭獥鍚庨噸璇�'; }
+ finally { if(seq===openSeq) modalApi.unlock(); }
+ },
+ async onConfirm() {
+ if(saving.value||loadError.value)return;
+ await formRef.value?.validate();
+ saving.value=true;modalApi.lock();
+ try { await spareAction(type.value,{...model}); emit('success');saving.value=false;await modalApi.close(); }
+ finally { saving.value=false;modalApi.unlock(); }
+ },
+});
+const quantityRules = [{required:true,message:'璇疯緭鍏ユ暟閲�'}, {validator:async (_:unknown,value:number)=>{
+ if(!Number.isFinite(value)||value<=0)throw new Error('鏁伴噺蹇呴』澶т簬0');
+ if(maxQuantity.value!==undefined && value>maxQuantity.value) throw new Error(type.value==='return'?'瓒呰繃鍙��鍥炴暟閲�':'瓒呰繃褰撳墠鍙敤搴撳瓨');
+}}];
+</script>
+<template>
+ <Modal :title="title" class="w-full sm:w-[600px]">
+ <Alert v-if="loadError" :message="loadError" type="error" class="mb-4" />
+ <Alert v-else class="mb-4" type="info" :message="type === 'issue' ? '棰嗙敤绔嬪嵆鎵e噺搴撳瓨锛屽苟鍏宠仈褰撳墠缁翠慨鍗曘�傝鏍稿澶囦欢銆侀鐢ㄤ汉鍜屾暟閲忋��' : type === 'return' ? '浠呴��鍥炴湭浣跨敤鐨勫浠讹紝绱閫�鍥炰笉鑳借秴杩囧師棰嗙敤鏁伴噺銆�' : '鍏ュ簱澧炲姞搴撳瓨骞朵繚鐣欒褰曪紝璇峰~鍐欒ˉ璐ф潵婧愭垨鍗曟嵁缂栧彿銆�'" />
+ <Form ref="formRef" :model="model" layout="vertical">
+ <Form.Item v-if="type === 'return'" label="鍘熼鐢ㄨ褰�">
+ {{ original?.repairCode }} 路 {{ original?.sparePartName }} 路 鍙��鍥� {{ original?.returnableQuantity }} {{ original?.unit }}
+ </Form.Item>
+ <Form.Item v-else label="澶囦欢" name="sparePartId" :rules="required">
+ <Select v-model:value="model.sparePartId" show-search :disabled="fixedPart || saving" :filter-option="false" placeholder="杈撳叆澶囦欢鍚嶇О鏌ユ壘" :options="parts.map(p=>({value:p.id,label:p.code+' 路 '+p.name+' 路 搴撳瓨 '+p.stockQuantity+' '+p.unit}))" @search="searchParts" />
+ </Form.Item>
+ <Form.Item v-if="type === 'issue'" label="鍏宠仈缁翠慨鍗�" name="repairId" :rules="required">
+ <Select v-model:value="model.repairId" :disabled="fixedRepair || saving" show-search :filter-option="false" :options="repairs" placeholder="杈撳叆缁翠慨鍗曠紪鍙锋煡鎵撅紙鑽夌鎴栫淮淇腑锛�" @search="searchRepairs" />
+ </Form.Item>
+ <Form.Item v-if="type === 'issue'" label="棰嗙敤浜�" name="receiverUserId" :rules="required">
+ <Select v-model:value="model.receiverUserId" show-search option-filter-prop="label" :options="users" placeholder="璇烽�夋嫨棰嗙敤浜�" :disabled="saving" />
+ </Form.Item>
+ <Form.Item :label="'鏁伴噺'+(chosenPart ? '锛�'+chosenPart.unit+'锛�' : '')" name="quantity" :rules="quantityRules">
+ <InputNumber v-model:value="model.quantity" :min="0.001" :precision="3" class="w-full" :disabled="saving" />
+ </Form.Item>
+ <Form.Item label="鎿嶄綔璇存槑" name="remark" :rules="required"><Input.TextArea v-model:value="model.remark" :maxlength="500" :rows="3" :disabled="saving" placeholder="濉啓棰嗙敤鐢ㄩ�斻�佸叆搴撴潵婧愭垨閫�鍥炲師鍥�" /></Form.Item>
+ </Form>
+ </Modal>
+</template>
+
diff --git a/src/views/mes/dv/spare/modules/form.vue b/src/views/mes/dv/spare/modules/form.vue
new file mode 100644
index 0000000..dcea6a0
--- /dev/null
+++ b/src/views/mes/dv/spare/modules/form.vue
@@ -0,0 +1,47 @@
+<script setup lang="ts">
+import type { SpareSave } from '#/api/mes/dv/spare';
+import type { FormInstance } from 'ant-design-vue';
+import { reactive, ref } from 'vue';
+import { useVbenModal } from '@vben/common-ui';
+import { Alert, Form, Input, InputNumber, Select } from 'ant-design-vue';
+import { createSpare, getSpare, updateSpare } from '#/api/mes/dv/spare';
+const emit = defineEmits(['success']);
+const formRef = ref<FormInstance>();
+const saving = ref(false);
+const model = reactive<SpareSave>({ code: '', name: '', unit: '浠�', initialQuantity: 0, safetyQuantity: 0, status: 0 });
+const required = [{ required: true, message: '璇峰~鍐欐椤�' }];
+const [Modal, modalApi] = useVbenModal({
+ onBeforeClose: () => !saving.value,
+ async onOpenChange(open) {
+ if (!open) return;
+ Object.assign(model, { id: undefined, code: '', name: '', specification: '', unit: '浠�', location: '', initialQuantity: 0, safetyQuantity: 0, status: 0, remark: '' });
+ formRef.value?.clearValidate();
+ const { id } = modalApi.getData<{ id?: number }>();
+ if (id) { modalApi.lock(); try { Object.assign(model, await getSpare(id)); } finally { modalApi.unlock(); } }
+ },
+ async onConfirm() {
+ if (saving.value) return;
+ await formRef.value?.validate();
+ saving.value = true; modalApi.lock();
+ try { await (model.id ? updateSpare(model) : createSpare(model)); emit('success'); saving.value = false; await modalApi.close(); }
+ finally { saving.value = false; modalApi.unlock(); }
+ },
+});
+</script>
+<template>
+ <Modal :title="model.id ? '缂栬緫澶囦欢' : '鏂板澶囦欢'" class="w-full sm:w-[640px]">
+ <Alert class="mb-4" type="info" :message="model.id ? '搴撳瓨閫氳繃鍏ュ簱銆侀鐢ㄥ拰閫�鍥炲彉鏇淬�傚凡鏈夋祦姘村悗涓嶅彲淇敼璁¢噺鍗曚綅銆�' : '鍒濆搴撳瓨鐧昏鍚庝細淇濈暀娴佹按锛屽悗缁ˉ璐ц浣跨敤鍏ュ簱銆�'" />
+ <Form ref="formRef" :model="model" layout="vertical" class="grid grid-cols-1 gap-x-4 sm:grid-cols-2">
+ <Form.Item label="澶囦欢缂栫爜" name="code" :rules="required"><Input v-model:value="model.code" :maxlength="64" /></Form.Item>
+ <Form.Item label="澶囦欢鍚嶇О" name="name" :rules="required"><Input v-model:value="model.name" :maxlength="128" /></Form.Item>
+ <Form.Item label="瑙勬牸鍨嬪彿" name="specification"><Input v-model:value="model.specification" :maxlength="255" /></Form.Item>
+ <Form.Item label="璁¢噺鍗曚綅" name="unit" :rules="required"><Input v-model:value="model.unit" :maxlength="32" /></Form.Item>
+ <Form.Item label="瀛樻斁浣嶇疆" name="location"><Input v-model:value="model.location" :maxlength="128" /></Form.Item>
+ <Form.Item label="瀹夊叏搴撳瓨" name="safetyQuantity" :rules="required"><InputNumber v-model:value="model.safetyQuantity" :min="0" :precision="3" class="w-full" /></Form.Item>
+ <Form.Item v-if="!model.id" label="鍒濆搴撳瓨" name="initialQuantity" :rules="required"><InputNumber v-model:value="model.initialQuantity" :min="0" :precision="3" class="w-full" /></Form.Item>
+ <Form.Item label="鐘舵��" name="status"><Select v-model:value="model.status" :options="[{label:'鍚敤',value:0},{label:'鍋滅敤',value:1}]" /></Form.Item>
+ <Form.Item label="澶囨敞" name="remark" class="sm:col-span-2"><Input.TextArea v-model:value="model.remark" :maxlength="500" :rows="3" /></Form.Item>
+ </Form>
+ </Modal>
+</template>
+
diff --git a/src/views/mes/dv/spare/modules/records.vue b/src/views/mes/dv/spare/modules/records.vue
new file mode 100644
index 0000000..e8f0816
--- /dev/null
+++ b/src/views/mes/dv/spare/modules/records.vue
@@ -0,0 +1,65 @@
+<script setup lang="ts">
+import type { SpareMovement } from '#/api/mes/dv/spare';
+import { onMounted, reactive, ref, watch } from 'vue';
+import { useVbenModal } from '@vben/common-ui';
+import { Alert, Button, Form, Input, Select, Space, Table } from 'ant-design-vue';
+import { getSpareMovements, movementLabels } from '#/api/mes/dv/spare';
+import Action from './action.vue';
+const props=defineProps<{repairId?:number;sparePartId?:number;canIssue?:boolean;repairCode?:string}>();
+const rows=ref<SpareMovement[]>([]),loading=ref(false),error=ref('');
+const page=ref(1),size=ref(10),total=ref(0);
+const filters=reactive<{repairCode?:string;type?:number}>({type:3});
+let seq=0;
+const [ActionModal,actionApi]=useVbenModal({connectedComponent:Action,destroyOnClose:true});
+const columns=[
+ {title:'鎿嶄綔鏃堕棿',dataIndex:'createTime',key:'createTime',width:175},
+ {title:'绫诲瀷',dataIndex:'type',key:'type',width:95},
+ {title:'澶囦欢缂栫爜',dataIndex:'sparePartCode',width:135},
+ {title:'澶囦欢鍚嶇О',dataIndex:'sparePartName',width:150},
+ {title:'瑙勬牸鍨嬪彿',dataIndex:'specification',width:130},
+ {title:'鏁伴噺 / 鍗曚綅',key:'quantity',width:110},
+ {title:'宸查��鍥�',dataIndex:'returnedQuantity',width:90},
+ {title:'鍙��鍥�',dataIndex:'returnableQuantity',width:90},
+ {title:'缁翠慨鍗曞彿',dataIndex:'repairCode',width:145},
+ {title:'璁惧',dataIndex:'machineryName',width:150},
+ {title:'棰嗙敤浜�',dataIndex:'receiverName',width:110},
+ {title:'鎿嶄綔璇存槑',dataIndex:'remark',width:220},
+ {title:'鎿嶄綔',key:'action',width:90,fixed:'right' as const},
+];
+async function load(reset=false) {
+ if(reset)page.value=1;
+ const current=++seq;loading.value=true;error.value='';
+ try {const result=await getSpareMovements({pageNo:page.value,pageSize:size.value,...filters,repairId:props.repairId,sparePartId:props.sparePartId});
+ if(current===seq){rows.value=result.list;total.value=result.total;}}
+ catch {if(current===seq){rows.value=[];total.value=0;error.value='璁板綍鍔犺浇澶辫触锛岃鐐瑰嚮鏌ヨ閲嶈瘯';}}
+ finally {if(current===seq)loading.value=false;}
+}
+function changePage(p:{current?:number;pageSize?:number}){page.value=p.current||1;size.value=p.pageSize||10;void load();}
+function issue(){actionApi.setData({type:'issue',repairId:props.repairId,repairCode:props.repairCode}).open();}
+function returnPart(record:SpareMovement){actionApi.setData({type:'return',original:record}).open();}
+watch(()=>[props.repairId,props.sparePartId],()=>load(true));
+onMounted(()=>load());
+defineExpose({refresh:()=>load(true)});
+</script>
+<template>
+ <div class="mt-4">
+ <ActionModal @success="load(true)" />
+ <div class="mb-3 flex flex-wrap items-center justify-between gap-3"><h3 class="text-base font-semibold">澶囦欢棰嗙敤璁板綍</h3>
+ <Button v-if="canIssue" v-access:code="['mes:dv-spare:issue']" type="primary" @click="issue">棰嗙敤澶囦欢</Button>
+ </div>
+ <Form layout="inline" class="mb-4 flex flex-wrap gap-y-3">
+ <Form.Item v-if="!repairId" label="缁翠慨鍗曞彿"><Input v-model:value="filters.repairCode" allow-clear placeholder="瀹屾暣缁翠慨鍗曞彿" @press-enter="load(true)" /></Form.Item>
+ <Form.Item label="璁板綍绫诲瀷"><Select v-model:value="filters.type" allow-clear class="w-36" :options="Object.entries(movementLabels).map(([value,label])=>({label,value:Number(value)}))" /></Form.Item>
+ <Form.Item><Space><Button :loading="loading" @click="load(true)">鏌ヨ</Button><Button @click="filters.repairCode=undefined;filters.type=3;load(true)">閲嶇疆</Button></Space></Form.Item>
+ </Form>
+ <Alert v-if="error" type="error" :message="error" class="mb-3" />
+ <Table :columns="columns" :data-source="rows" :loading="loading" row-key="id" :scroll="{x:1790}" :pagination="{current:page,pageSize:size,total,showSizeChanger:true}" @change="changePage">
+ <template #bodyCell="{column,record}">
+ <template v-if="column.key==='type'">{{ movementLabels[record.type] }}</template>
+ <template v-else-if="column.key==='quantity'">{{ record.quantity }} {{ record.unit }}</template>
+ <template v-else-if="column.key==='action'"><Button v-if="record.type===3 && Number(record.returnableQuantity)>0" v-access:code="['mes:dv-spare:return']" type="link" @click="returnPart(record as SpareMovement)">閫�鍥�</Button><span v-else>鈥�</span></template>
+ </template>
+ </Table>
+ </div>
+</template>
+
diff --git a/src/views/mes/md/client/components/select.vue b/src/views/mes/md/client/components/select.vue
index 3fcbc11..352c558 100644
--- a/src/views/mes/md/client/components/select.vue
+++ b/src/views/mes/md/client/components/select.vue
@@ -113,7 +113,6 @@
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
- <div>缂栫爜锛歿{ selectedItem.code || '-' }}</div>
<div>鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
<div>绠�绉帮細{{ selectedItem.nickname || '-' }}</div>
<div>鐢佃瘽锛歿{ selectedItem.telephone || '-' }}</div>
diff --git a/src/views/mes/md/client/data.ts b/src/views/mes/md/client/data.ts
index 45df0e7..5204f62 100644
--- a/src/views/mes/md/client/data.ts
+++ b/src/views/mes/md/client/data.ts
@@ -8,15 +8,6 @@
export function useClientSelectGridFormSchema(): VbenFormSchema[] {
return [
{
- fieldName: 'code',
- label: '瀹㈡埛缂栫爜',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ鎴风紪鐮�',
- },
- },
- {
fieldName: 'name',
label: '瀹㈡埛鍚嶇О',
component: 'Input',
@@ -52,11 +43,6 @@
): VxeTableGridOptions<MesMdClientApi.Client>['columns'] {
return [
{ type: multiple ? 'checkbox' : 'radio', width: 50 },
- {
- field: 'code',
- title: '瀹㈡埛缂栫爜',
- minWidth: 160,
- },
{
field: 'name',
title: '瀹㈡埛鍚嶇О',
diff --git a/src/views/mes/pd/project/modules/document-audit-form.vue b/src/views/mes/pd/project/modules/document-audit-form.vue
index 93f0dc0..75c42f0 100644
--- a/src/views/mes/pd/project/modules/document-audit-form.vue
+++ b/src/views/mes/pd/project/modules/document-audit-form.vue
@@ -23,7 +23,8 @@
{ fieldName: 'documentVersion', label: '璧勬枡鐗堟湰', component: 'Input', componentProps: { disabled: true } },
{ fieldName: 'reviewerNickname', label: '瀹℃牳浜�', component: 'Input', componentProps: { disabled: true } },
{
- fieldName: 'auditStatus',
+ // 瀛楁鍚嶄笉鑳界敤 auditStatus锛氳嚜鍔ㄥ鎵规ā寮忎笅浼氳 adapter/form 鐨� isAuditField 鏁存潯杩囨护鎺夛紝瀵艰嚧瀹℃牳浜烘棤娉曢�夋嫨
+ fieldName: 'auditResult',
label: '瀹℃牳缁撴灉',
component: 'Select',
componentProps: {
@@ -47,7 +48,7 @@
modalApi.lock();
try {
const raw = await formApi.getValues();
- await updatePdDocumentAudit({ id: raw.id, auditStatus: raw.auditStatus, remark: raw.remark, auditTime: Date.now() });
+ await updatePdDocumentAudit({ id: raw.id, auditStatus: raw.auditResult, remark: raw.remark, auditTime: Date.now() });
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
await modalApi.close();
@@ -59,9 +60,8 @@
if (!isOpen) return;
const data = modalApi.getData<MesPdDocumentAuditApi.DocumentAudit>();
modalApi.setState({ title: '璧勬枡瀹℃牳' });
- const formData = { ...data };
- delete formData.auditStatus;
- await formApi.setValues(formData);
+ // 姣忔鎵撳紑閮芥竻绌哄鏍哥粨鏋滐紝閬垮厤澶嶇敤涓婁竴琛屽凡閫夌殑鍊�
+ await formApi.setValues({ ...data, auditResult: undefined });
},
});
</script>
diff --git a/src/views/mes/pd/project/modules/document-audit-list.vue b/src/views/mes/pd/project/modules/document-audit-list.vue
index 609dde5..64fd6e1 100644
--- a/src/views/mes/pd/project/modules/document-audit-list.vue
+++ b/src/views/mes/pd/project/modules/document-audit-list.vue
@@ -49,6 +49,7 @@
{ field: 'auditNo', title: '瀹℃牳鍗曞彿', minWidth: 140 },
{ field: 'documentName', title: '璧勬枡鍚嶇О', minWidth: 160 },
{ field: 'documentVersion', title: '璧勬枡鐗堟湰', minWidth: 90 },
+ { field: 'fileUrl', title: '鏂囦欢', minWidth: 140, slots: { default: 'file' } },
{ field: 'reviewerNickname', title: '瀹℃牳浜�', minWidth: 100 },
{ field: 'auditStatus', title: '瀹℃牳鐘舵��', minWidth: 100, cellRender: { name: 'CellDict', props: { type: DICT_TYPE.MES_PD_DOCUMENT_AUDIT_STATUS } } },
{ field: 'auditTime', title: '瀹℃牳鏃堕棿', minWidth: 160, formatter: 'formatDateTime' },
@@ -78,6 +79,12 @@
<template>
<AuditFormModal @success="handleRefresh" />
<Grid table-title="璧勬枡瀹℃牳">
+ <template #file="{ row }">
+ <a v-if="row.fileUrl" :href="row.fileUrl" target="_blank" class="text-primary">
+ {{ row.fileName || '鏌ョ湅鏂囦欢' }}
+ </a>
+ <span v-else class="text-gray-400">-</span>
+ </template>
<template #actions="{ row }">
<TableAction
:actions="[
diff --git a/src/views/mes/pro/card/index.vue b/src/views/mes/pro/card/index.vue
index b643351..b955ed4 100644
--- a/src/views/mes/pro/card/index.vue
+++ b/src/views/mes/pro/card/index.vue
@@ -3,7 +3,7 @@
import type { MesProCardApi } from '#/api/mes/pro/card';
import { Page, useVbenModal } from '@vben/common-ui';
-import { MesProCardStatusEnum } from '@vben/constants';
+import { BarcodeBizTypeEnum, MesProCardStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -185,7 +185,7 @@
<PrinterLabel
:biz-code="row.code"
:biz-id="row.id"
- biz-type="PROCARD"
+ :biz-type="BarcodeBizTypeEnum.PROCARD"
/>
</template>
</Grid>
diff --git a/src/views/mes/pro/card/modules/form.vue b/src/views/mes/pro/card/modules/form.vue
index 024aa9e..932a0a6 100644
--- a/src/views/mes/pro/card/modules/form.vue
+++ b/src/views/mes/pro/card/modules/form.vue
@@ -19,7 +19,10 @@
updateCard,
} from '#/api/mes/pro/card';
import { $t } from '#/locales';
-import { BarcodeDetail } from '#/views/wls/barcode/components';
+import {
+ BarcodeDetail,
+ PrinterLabel,
+} from '#/views/wls/barcode/components';
import { useFormSchema } from '../data';
import ProcessList from './process-list.vue';
@@ -76,6 +79,7 @@
formData.value.id,
BarcodeBizTypeEnum.PROCARD,
formData.value.code,
+ formData.value.itemName || formData.value.code,
);
}
@@ -207,6 +211,14 @@
>
鏌ョ湅鏉$爜
</Button>
+ <PrinterLabel
+ v-if="formType === 'detail' && formData?.id"
+ :biz-id="formData.id"
+ :biz-type="BarcodeBizTypeEnum.PROCARD"
+ :biz-code="formData.code"
+ :biz-name="formData.itemName || formData.code"
+ title="娴佽浆鍗�"
+ />
</div>
</template>
</Modal>
diff --git a/src/views/mes/pro/mps/data.ts b/src/views/mes/pro/mps/data.ts
index c4b2d5f..ba79f62 100644
--- a/src/views/mes/pro/mps/data.ts
+++ b/src/views/mes/pro/mps/data.ts
@@ -250,11 +250,6 @@
formatter: 'formatAmount3',
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- width: 120,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
minWidth: 120,
diff --git a/src/views/mes/pro/mps/modules/form.vue b/src/views/mes/pro/mps/modules/form.vue
index 7490a8d..303db37 100644
--- a/src/views/mes/pro/mps/modules/form.vue
+++ b/src/views/mes/pro/mps/modules/form.vue
@@ -144,9 +144,6 @@
<Descriptions.Item label="宸茬敓浜ф暟閲�">
{{ formData.quantityProduced }}
</Descriptions.Item>
- <Descriptions.Item label="瀹㈡埛缂栫爜">
- {{ formData.clientCode }}
- </Descriptions.Item>
<Descriptions.Item label="瀹㈡埛鍚嶇О">
{{ formData.clientName }}
</Descriptions.Item>
diff --git a/src/views/mes/pro/route/data.ts b/src/views/mes/pro/route/data.ts
index ba20842..f4bf910 100644
--- a/src/views/mes/pro/route/data.ts
+++ b/src/views/mes/pro/route/data.ts
@@ -230,6 +230,19 @@
},
},
{
+ fieldName: 'yieldRate',
+ label: '鏀剁巼',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0.0001,
+ max: 1,
+ step: 0.01,
+ precision: 4,
+ placeholder: '浜у嚭/鎶曞叆锛屽 0.98',
+ },
+ },
+ {
fieldName: 'linkType',
label: '涓庝笅閬撳伐搴忓叧绯�',
component: 'Select',
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index 7dae255..0739c03 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -86,6 +86,7 @@
/** 寰呮帓浜ц鍗曞垪琛ㄧ殑瀛楁 */
export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] {
return [
+ { type: 'checkbox', width: 48, fixed: 'left' },
{
field: 'code',
title: '璁㈠崟缂栫爜',
@@ -143,15 +144,6 @@
},
},
{
- field: 'productionStatus',
- title: '鐢熶骇鐘舵��',
- width: 100,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS },
- },
- },
- {
field: 'quantity',
title: '璁㈠崟鏁伴噺',
width: 100,
@@ -173,11 +165,6 @@
slots: { default: 'progressPercent' },
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- width: 120,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
width: 120,
@@ -191,7 +178,7 @@
{
title: '鎿嶄綔',
- width: 160,
+ width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
diff --git a/src/views/mes/pro/task/index.vue b/src/views/mes/pro/task/index.vue
index ba6ff48..9621fdc 100644
--- a/src/views/mes/pro/task/index.vue
+++ b/src/views/mes/pro/task/index.vue
@@ -7,12 +7,11 @@
import { Page, useVbenModal } from '@vben/common-ui';
import {
- MesProWorkOrderProductionStatusEnum,
MesProWorkOrderStatusEnum,
MesProWorkOrderTypeEnum,
} from '@vben/constants';
-import { Button, Card, Progress, Tabs } from 'ant-design-vue';
+import { Button, Card, Progress, Tabs, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getGanttTaskList } from '#/api/mes/pro/task';
@@ -22,6 +21,7 @@
import { GanttChart } from './components';
import { useGridColumns, useGridFormSchema } from './data';
import ScheduleForm from './modules/schedule-form.vue';
+import AutoSchedule from './modules/auto-schedule.vue';
const router = useRouter();
const ganttTasks = ref<any[]>([]); // 鐢樼壒鍥鹃瑙堟暟鎹�
@@ -35,9 +35,31 @@
destroyOnClose: true,
});
+const [AutoScheduleModal, autoScheduleModalApi] = useVbenModal({
+ connectedComponent: AutoSchedule,
+ destroyOnClose: true,
+});
+
+function canAutoSchedule(row: MesProWorkOrderApi.WorkOrder) {
+ return row.status === MesProWorkOrderStatusEnum.CONFIRMED
+ && row.type === MesProWorkOrderTypeEnum.SELF
+ && !isCompletedWorkOrder(row)
+ && (row.quantity ?? 0) > 0
+ && (row.quantityScheduled ?? 0) < (row.quantity ?? 0);
+}
+
+function handleAutoSchedule(row?: MesProWorkOrderApi.WorkOrder) {
+ const rows: MesProWorkOrderApi.WorkOrder[] = row ? [row] : gridApi.grid.getCheckboxRecords();
+ if (!rows.length) return message.warning('璇峰厛鍕鹃�夊緟鎺掍骇宸ュ崟');
+ if (rows.length > 30) return message.warning('涓�娆℃渶澶氶�夋嫨30涓伐鍗曪紝璇峰垎鎵规帓浜�');
+ if (rows.some((order) => !canAutoSchedule(order))) return message.warning('璇烽�夋嫨宸茬‘璁や笖浠嶉渶鎺掍骇鐨勮嚜琛岀敓浜у伐鍗�');
+ autoScheduleModalApi.setData({ workOrderIds: rows.map((order) => order.id!) }).open();
+}
+
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
+ gridApi.grid.clearCheckboxRow();
gridApi.query();
loadGanttPreview();
}
@@ -72,12 +94,9 @@
router.push({ name: 'MesProTaskGanttEdit' });
}
-/** 鍒ゆ柇璁㈠崟鏄惁涓衡�滃凡瀹屾垚绫诲瀷鈥濓細璁㈠崟鐘舵��=宸插畬鎴� 鎴� 鐢熶骇鐘舵��=鐢熶骇瀹屾垚 */
+/** 鍒ゆ柇璁㈠崟鏄惁涓衡�滃凡瀹屾垚绫诲瀷鈥濓細璁㈠崟鐘舵��=宸插畬鎴� */
function isCompletedWorkOrder(row: MesProWorkOrderApi.WorkOrder): boolean {
- return (
- row.status === MesProWorkOrderStatusEnum.FINISHED ||
- row.productionStatus === MesProWorkOrderProductionStatusEnum.FINISHED
- );
+ return row.status === MesProWorkOrderStatusEnum.FINISHED;
}
/**
@@ -103,6 +122,11 @@
gridOptions: {
columns: useGridColumns(),
keepSource: true,
+ checkboxConfig: {
+ checkStrictly: true,
+ showHeader: true,
+ checkMethod: ({ row }: { row: MesProWorkOrderApi.WorkOrder }) => canAutoSchedule(row),
+ },
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
@@ -154,6 +178,7 @@
<template>
<Page auto-content-height>
<ScheduleModal @success="handleRefresh" />
+ <AutoScheduleModal @success="handleRefresh" />
<!-- 鎺掍骇鐢樼壒鍥鹃瑙� -->
<Card title="鎺掍骇鐢樼壒鍥�" class="mb-4">
<template #extra>
@@ -177,8 +202,14 @@
<TableAction
:actions="[
{
- label: '鐢樼壒鍥剧紪杈�',
+ label: '涓�閿帓浜�',
type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-task:create'],
+ onClick: () => handleAutoSchedule(),
+ },
+ {
+ label: '鐢樼壒鍥剧紪杈�',
icon: ACTION_ICON.EDIT,
onClick: handleGanttEdit,
},
@@ -200,6 +231,13 @@
<TableAction
:actions="[
{
+ label: '涓�閿帓浜�',
+ type: 'link',
+ auth: ['mes:pro-task:create'],
+ ifShow: canAutoSchedule(row),
+ onClick: () => handleAutoSchedule(row),
+ },
+ {
label: '鎺掍骇缁存姢',
type: 'link',
auth: ['mes:pro-task:create'],
diff --git a/src/views/mes/pro/task/modules/auto-schedule.vue b/src/views/mes/pro/task/modules/auto-schedule.vue
new file mode 100644
index 0000000..69d81dd
--- /dev/null
+++ b/src/views/mes/pro/task/modules/auto-schedule.vue
@@ -0,0 +1,180 @@
+<script setup lang="ts">
+import type { BatchSchedulePreview, BatchScheduleRequest } from '#/api/mes/pro/task';
+import { computed, ref, watch } from 'vue';
+import { useVbenModal } from '@vben/common-ui';
+import { Alert, Button, Checkbox, DatePicker, Descriptions, Table, Tag, message } from 'ant-design-vue';
+import dayjs from 'dayjs';
+import { executeBatchSchedule, previewBatchSchedule } from '#/api/mes/pro/task';
+
+const emit = defineEmits(['success']);
+const ids = ref<number[]>([]);
+function defaultStartTime() {
+ let value = dayjs().add(5, 'minute').startOf('minute');
+ if (value.hour() >= 18) value = value.add(1, 'day').hour(9).minute(0).second(0);
+ else if (value.hour() < 9) value = value.hour(9).minute(0).second(0);
+ while (value.day() === 0 || value.day() === 6) value = value.add(1, 'day').hour(9).minute(0).second(0);
+ return value;
+}
+const startTime = ref(defaultStartTime());
+const preview = ref<BatchSchedulePreview>();
+const loading = ref(false);
+const executing = ref(false);
+const acceptLate = ref(false);
+const errorText = ref('');
+let sequence = 0;
+let open = false;
+const canConfirm = computed(() => !!preview.value?.canExecute && !loading.value && !executing.value
+ && (!preview.value.warnings.length || acceptLate.value));
+
+const [Modal, modalApi] = useVbenModal({
+ confirmText: '纭鎺掍骇',
+ confirmDisabled: true,
+ onBeforeClose: () => !executing.value,
+ onConfirm: execute,
+ async onOpenChange(isOpen) {
+ open = isOpen;
+ sequence++;
+ if (!isOpen) {
+ preview.value = undefined;
+ ids.value = [];
+ loading.value = false;
+ errorText.value = '';
+ return;
+ }
+ ids.value = modalApi.getData<{ workOrderIds: number[] }>().workOrderIds;
+ startTime.value = defaultStartTime();
+ await calculate();
+ },
+});
+
+watch(canConfirm, (value) => modalApi.setState({ confirmDisabled: !value }));
+watch(startTime, () => {
+ sequence++;
+ preview.value = undefined;
+ acceptLate.value = false;
+ loading.value = false;
+ errorText.value = '';
+}, { flush: 'sync' });
+
+function request(): BatchScheduleRequest {
+ return { workOrderIds: [...ids.value], startTime: startTime.value.format('YYYY-MM-DD HH:mm:ss') };
+}
+
+async function calculate() {
+ if (executing.value || !startTime.value || !ids.value.length) return;
+ const current = ++sequence;
+ preview.value = undefined;
+ acceptLate.value = false;
+ errorText.value = '';
+ loading.value = true;
+ try {
+ const result = await previewBatchSchedule(request());
+ if (open && current === sequence) preview.value = result;
+ } catch (error) {
+ if (open && current === sequence) errorText.value = error instanceof Error ? error.message : '棰勮澶辫触锛岃绋嶅悗閲嶈瘯';
+ } finally {
+ if (current === sequence) loading.value = false;
+ }
+}
+
+async function execute() {
+ if (!canConfirm.value || !preview.value) return;
+ executing.value = true;
+ errorText.value = '';
+ modalApi.lock();
+ try {
+ const result = await executeBatchSchedule({ ...request(), previewHash: preview.value.previewHash,
+ previewTime: preview.value.previewTime, allowLate: acceptLate.value });
+ message.success(`鎺掍骇鎴愬姛锛�${ids.value.length} 涓伐鍗曪紝鐢熸垚 ${result.taskIds.length} 涓换鍔);
+ executing.value = false;
+ await modalApi.close();
+ emit('success');
+ } catch (error) {
+ errorText.value = error instanceof Error ? error.message : '鎵ц缁撴灉鏈‘璁わ紝璇峰埛鏂板垪琛ㄥ苟閲嶆柊棰勮';
+ // 璇锋眰澶辫触鎴栫粨鏋滄湭纭鍚庡繀椤婚噸绠楋紱鏃ч瑙堜笉鑳界洿鎺ュ啀娆℃彁浜ゃ��
+ preview.value = undefined;
+ acceptLate.value = false;
+ emit('success'); // 鍒锋柊宸ュ崟鍜岀敇鐗瑰浘锛屾牳瀵硅秴鏃惰姹傛槸鍚﹀凡缁忔彁浜ゃ��
+ } finally {
+ executing.value = false;
+ modalApi.unlock();
+ }
+}
+
+const orderColumns = [
+ { title: '宸ュ崟缂栫爜', dataIndex: 'workOrderCode', width: 170 },
+ { title: '鐢熶骇鏁伴噺', dataIndex: 'quantity', width: 100 },
+ { title: '鏂板浠诲姟', dataIndex: 'taskCount', width: 90 },
+ { title: '闇�姹傛棩鏈�', dataIndex: 'requestDate', width: 120 },
+ { title: '棰勮瀹屾垚', dataIndex: 'endTime', width: 175 },
+ { title: '鏍¢獙缁撴灉', dataIndex: 'error', width: 300 },
+];
+const taskColumns = [
+ { title: '宸ュ崟', dataIndex: 'workOrderCode', width: 160 },
+ { title: '宸ュ簭', dataIndex: 'processName', width: 130 },
+ { title: '宸ヤ綔绔�', dataIndex: 'workstationName', width: 150 },
+ { title: '璁惧', dataIndex: 'machineryNames', width: 180 },
+ { title: '鏈鏁伴噺', dataIndex: 'quantity', width: 100 },
+ { title: '灏忔椂浜ц兘', dataIndex: 'hourlyCapacity', width: 100 },
+ { title: '宸ユ椂(h)', dataIndex: 'duration', width: 85 },
+ { title: '寮�宸ユ椂闂�', dataIndex: 'startTime', width: 180 },
+ { title: '瀹屾垚鏃堕棿', dataIndex: 'endTime', width: 180 },
+];
+</script>
+
+<template>
+ <Modal title="涓�閿帓浜�" class="w-[min(96vw,1200px)]">
+ <div class="space-y-5 px-1">
+ <Alert type="info" show-icon message="鍏堟牎楠屼笌棰勮锛屽啀纭鎺掍骇"
+ description="鎸変氦鏈熶紭鍏堝強宸ュ簭椤哄簭瀹夋帓宸ヤ綔绔欎笌璁惧锛涙瘡澶�09:00鈥�18:00锛屾帓闄ゅ懆鏈拰宸查厤缃妭鍋囨棩锛岄潪宸ヤ綔鏃堕棿鑷姩椤哄欢銆傚凡閮ㄥ垎鎺掍骇鐨勫伐鍗曞彧琛ラ綈鍓╀綑鏁伴噺锛屾渶澶�30涓伐鍗曘��200涓柊浠诲姟銆�" />
+ <div class="flex flex-wrap items-end gap-3">
+ <label class="flex flex-col gap-2">
+ <span class="font-medium">鏈�鏃╁紑宸ユ椂闂�</span>
+ <DatePicker v-model:value="startTime" show-time :allow-clear="false" :disabled="executing"
+ format="YYYY-MM-DD HH:mm:ss" :disabled-date="(date) => date.endOf('day').isBefore(dayjs())" />
+ </label>
+ <Button :loading="loading" :disabled="executing" @click="calculate">
+ {{ preview ? '閲嶆柊璁$畻棰勮' : '鐢熸垚鎺掍骇棰勮' }}
+ </Button>
+ <span class="text-sm text-muted-foreground">宸查�夋嫨 {{ ids.length }} 涓伐鍗�</span>
+ </div>
+ <Alert v-if="errorText" type="error" show-icon :message="errorText" />
+ <template v-if="preview">
+ <Descriptions :column="{ xs: 1, sm: 3 }" size="small" bordered>
+ <Descriptions.Item label="宸ュ崟鏁�">{{ preview.orders.length }}</Descriptions.Item>
+ <Descriptions.Item label="鏂板浠诲姟鏁�">{{ preview.tasks.length }}</Descriptions.Item>
+ <Descriptions.Item label="棰勮鏈夋晥鏈�">15鍒嗛挓锛屾彁浜ゆ椂閲嶆柊鏍¢獙</Descriptions.Item>
+ </Descriptions>
+ <Alert v-if="!preview.canExecute" type="error" show-icon message="閮ㄥ垎宸ュ崟鏈�氳繃鏍¢獙锛屾湰鎵规涓嶈兘鎻愪氦"
+ description="璇锋牴鎹笅鏂规牎楠岀粨鏋滀慨姝e伐鍗曘�佽澶囨垨宸叉湁浠诲姟鐨勯厤缃紝鍐嶉噸鏂拌绠楅瑙堛��" />
+ <div>
+ <h3 class="mb-2 font-medium">宸ュ崟鏍¢獙</h3>
+ <Table :columns="orderColumns" :data-source="preview.orders" row-key="workOrderId"
+ :pagination="false" size="small" :scroll="{ x: 1055 }">
+ <template #bodyCell="{ column, record, text }">
+ <template v-if="column.dataIndex === 'error'">
+ <span v-if="record.error" class="text-destructive">{{ record.error }}</span>
+ <Tag v-else :color="record.late ? 'orange' : 'green'">{{ record.late ? '鏈夊欢鏈熼闄�' : '閫氳繃' }}</Tag>
+ </template>
+ <template v-else-if="column.dataIndex === 'requestDate'">{{ text ? String(text).slice(0, 10) : '鏈寚瀹�' }}</template>
+ </template>
+ </Table>
+ </div>
+ <div v-if="preview.tasks.length">
+ <h3 class="mb-2 font-medium">浠诲姟瀹夋帓</h3>
+ <Table :columns="taskColumns" :data-source="preview.tasks"
+ :row-key="(row) => `${row.workOrderId}-${row.processId}`" :pagination="{ pageSize: 10, showSizeChanger: false }"
+ size="small" :scroll="{ x: 1365 }" />
+ </div>
+ <Alert v-if="preview.warnings.length" type="warning" show-icon message="璇锋牳瀵逛氦鏈熼闄�">
+ <template #description>
+ <p v-for="warning in preview.warnings" :key="warning">{{ warning }}</p>
+ <Checkbox v-model:checked="acceptLate" :disabled="executing" class="mt-2">
+ 鎴戝凡鏍稿骞舵帴鍙椾互涓婁氦鏈熷欢鏈熼闄�
+ </Checkbox>
+ </template>
+ </Alert>
+ </template>
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/workorder-center/index.vue b/src/views/mes/pro/workorder-center/index.vue
index 99ada7d..c8f8f0a 100644
--- a/src/views/mes/pro/workorder-center/index.vue
+++ b/src/views/mes/pro/workorder-center/index.vue
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesProWorkOrderApi, TaskInfo } from '#/api/mes/pro/workorder';
+import type { MesProWorkOrderApi, TaskInfo, WorkOrderProgress } from '#/api/mes/pro/workorder';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
@@ -16,19 +16,24 @@
getWorkOrderProgress,
checkAndAutoFinishWorkOrder,
} from '#/api/mes/pro/workorder';
-import { previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
import { useGridColumns, useGridFormSchema, useTaskGridColumns } from './data';
import QuickFeedbackForm from './modules/quick-feedback.vue';
-import SchedulePreviewModal from './modules/schedule-preview.vue';
+import AutoSchedule from '../task/modules/auto-schedule.vue';
defineOptions({ name: 'WorkorderCenter' });
-const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>();
+const archiveVisible = ref(false);
+const archiveProductCode = ref('');
+
+function handleArchive(row: MesProWorkOrderApi.WorkOrder) {
+ archiveProductCode.value = row.productCode ?? '';
+ archiveVisible.value = true;
+}
const router = useRouter();
-const progressData = ref<MesProWorkOrderApi.WorkOrderProgress | null>(null);
+const progressData = ref<WorkOrderProgress | null>(null);
const activeProcessKeys = ref<string[]>([]);
const expandedRowId = ref<number | null>(null);
@@ -38,7 +43,7 @@
});
const [SchedulePreviewModalWrap, schedulePreviewModalApi] = useVbenModal({
- connectedComponent: SchedulePreviewModal,
+ connectedComponent: AutoSchedule,
destroyOnClose: true,
});
@@ -107,27 +112,9 @@
.open();
}
-/** 涓�閿帓浜ч瑙� */
-async function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
- try {
- const preview = await previewAutoGenerate({ workOrderId: row.id! });
- schedulePreviewModalApi
- .setData({ workOrder: row, preview })
- .open();
- } catch {
- message.error('鑾峰彇鎺掍骇棰勮澶辫触');
- }
-}
-
-/** 鎵ц涓�閿帓浜� */
-async function handleScheduleExecute(row: MesProWorkOrderApi.WorkOrder) {
- try {
- const result = await autoGenerateTasks({ workOrderId: row.id! });
- message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
- handleRefresh();
- } catch {
- message.error('鎺掍骇澶辫触');
- }
+/** 涓庣敓浜ф帓浜у叡鐢ㄦ牎楠屻�侀瑙堝拰鎵ц娴佺▼銆� */
+function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
+ schedulePreviewModalApi.setData({ workOrderIds: [row.id!] }).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -193,26 +180,16 @@
<TableAction
:actions="[
{
- label: '鎺掍骇棰勮',
- type: 'link',
- auth: ['mes:pro-task:create'],
- ifShow: row.quantityScheduled < row.quantity,
- onClick: handleSchedulePreview.bind(null, row),
- },
- {
label: '涓�閿帓浜�',
type: 'link',
auth: ['mes:pro-task:create'],
- ifShow: row.quantityScheduled < row.quantity,
- popConfirm: {
- title: '纭涓�閿帓浜э紵灏嗘寜鐓у伐鑹鸿矾绾胯嚜鍔ㄧ敓鎴愪换鍔°��',
- confirm: handleScheduleExecute.bind(null, row),
- },
+ ifShow: (row.quantityScheduled ?? 0) < (row.quantity ?? 0),
+ onClick: handleSchedulePreview.bind(null, row),
},
{
label: '褰掓。',
type: 'link',
- onClick: () => archiveDetailRef.value?.open(row.productCode),
+ onClick: () => handleArchive(row),
},
]"
/>
@@ -316,6 +293,6 @@
</Collapse.Panel>
</Collapse>
</Card>
- <ArchiveDetail ref="archiveDetailRef" />
+ <ArchiveDetail :visible="archiveVisible" :product-code="archiveProductCode" @update:visible="archiveVisible = $event" />
</Page>
</template>
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index 0547ca2..4cf593e 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -455,11 +455,6 @@
width: 100,
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- width: 120,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
width: 120,
@@ -477,15 +472,6 @@
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
- },
- },
- {
- field: 'productionStatus',
- title: '鐢熶骇鐘舵��',
- width: 100,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS },
},
},
{
@@ -622,11 +608,6 @@
field: 'quantity',
title: '璁㈠崟鏁伴噺',
width: 100,
- },
- {
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- width: 120,
},
{
field: 'clientName',
diff --git a/src/views/mes/pro/workorder/index.vue b/src/views/mes/pro/workorder/index.vue
index 6c3ef86..1547165 100644
--- a/src/views/mes/pro/workorder/index.vue
+++ b/src/views/mes/pro/workorder/index.vue
@@ -68,7 +68,7 @@
/** 瀹屾垚璁㈠崟鍓嶆牎楠� */
function handleCheckFinish(row: MesProWorkOrderApi.WorkOrder) {
- if (row.productionStatus !== 2) {
+ if (row.status !== MesProWorkOrderStatusEnum.FINISHED) {
Modal.confirm({
title: '鎿嶄綔纭',
content: '璇ヨ鍗曟湭瀹屾垚锛屾槸鍚︾‘璁よ缁х画瀹屾垚锛�',
diff --git a/src/views/mes/process-design/route/data.ts b/src/views/mes/process-design/route/data.ts
index 6d0e614..5471982 100644
--- a/src/views/mes/process-design/route/data.ts
+++ b/src/views/mes/process-design/route/data.ts
@@ -202,6 +202,21 @@
rules: "selectRequired",
},
{
+ fieldName: "yieldRate",
+ label: "宸ュ簭鏀剁巼",
+ component: "InputNumber",
+ help: "浜у嚭閲� 梅 鎶曞叆閲忋�傚~鍐� 0.98 琛ㄧず 98%锛�1 琛ㄧず鏃犳崯鑰楋紱鏈~鍐欐寜 100% 鍙備笌鎺掍骇璁$畻銆�",
+ componentProps: {
+ class: "!w-full",
+ min: 0.0001,
+ max: 1,
+ step: 0.01,
+ precision: 4,
+ placeholder: "濡� 0.98锛�98%锛�",
+ },
+ rules: z.number().min(0.0001).max(1).nullish(),
+ },
+ {
fieldName: "linkType",
label: "涓庝笅閬撳伐搴忓叧绯�",
component: "Select",
diff --git a/src/views/mes/process-design/route/modules/workbench.vue b/src/views/mes/process-design/route/modules/workbench.vue
index 7959f3b..7941290 100644
--- a/src/views/mes/process-design/route/modules/workbench.vue
+++ b/src/views/mes/process-design/route/modules/workbench.vue
@@ -406,6 +406,9 @@
</div>
<div class="mt-1 flex flex-wrap gap-x-5 gap-y-1 text-sm text-gray-500">
<span>搴忓彿锛歿{ selectedProcess.sort }}</span>
+ <span>
+ 宸ュ簭鏀剁巼锛歿{ selectedProcess.yieldRate == null ? '鏈缃紙鎸� 100%锛�' : `${Number((selectedProcess.yieldRate * 100).toFixed(2))}%` }}
+ </span>
<span>鍑嗗鏃堕棿锛歿{ selectedProcess.prepareTime ?? 0 }} 鍒�</span>
<span>绛夊緟鏃堕棿锛歿{ selectedProcess.waitTime ?? 0 }} 鍒�</span>
<span v-if="selectedProcess.linkType != null">
@@ -542,4 +545,4 @@
</div>
</div>
</div>
-</template>
\ No newline at end of file
+</template>
diff --git a/src/views/mes/qc/batchtrace/data.ts b/src/views/mes/qc/batchtrace/data.ts
index 4af7ebe..92ff6dc 100644
--- a/src/views/mes/qc/batchtrace/data.ts
+++ b/src/views/mes/qc/batchtrace/data.ts
@@ -56,15 +56,6 @@
placeholder: '璇疯緭鍏ラ攢鍞鍗曠紪鍙�',
},
},
- {
- fieldName: 'purchaseOrderCode',
- label: '閲囪喘璁㈠崟缂栧彿',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ラ噰璐鍗曠紪鍙�',
- },
- },
];
}
@@ -97,19 +88,9 @@
width: 80,
},
{
- field: 'vendorCode',
- title: '渚涘簲鍟嗙紪鐮�',
- minWidth: 140,
- },
- {
field: 'vendorName',
title: '渚涘簲鍟嗗悕绉�',
minWidth: 160,
- },
- {
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- minWidth: 140,
},
{
field: 'clientName',
@@ -119,11 +100,6 @@
{
field: 'salesOrderCode',
title: '閿�鍞鍗曠紪鍙�',
- minWidth: 160,
- },
- {
- field: 'purchaseOrderCode',
- title: '閲囪喘璁㈠崟缂栧彿',
minWidth: 160,
},
{
@@ -192,24 +168,12 @@
span: 3,
},
{
- field: 'purchaseOrderCode',
- label: '閲囪喘璁㈠崟缂栧彿',
- },
- {
- field: 'vendorCode',
- label: '渚涘簲鍟嗙紪鐮�',
- },
- {
field: 'vendorName',
label: '渚涘簲鍟嗗悕绉�',
},
{
field: 'salesOrderCode',
label: '閿�鍞鍗曠紪鍙�',
- },
- {
- field: 'clientCode',
- label: '瀹㈡埛缂栫爜',
},
{
field: 'clientName',
@@ -243,16 +207,6 @@
title: '鍏ュ簱鏃ユ湡',
minWidth: 160,
formatter: 'formatDate',
- },
- {
- field: 'purchaseOrderCode',
- title: '閲囪喘璁㈠崟缂栧彿',
- minWidth: 160,
- },
- {
- field: 'vendorCode',
- title: '渚涘簲鍟嗙紪鐮�',
- minWidth: 140,
},
{
field: 'vendorName',
diff --git a/src/views/mes/qc/indicatorresult/components/data.ts b/src/views/mes/qc/indicatorresult/components/data.ts
index 694a01c..f16f449 100644
--- a/src/views/mes/qc/indicatorresult/components/data.ts
+++ b/src/views/mes/qc/indicatorresult/components/data.ts
@@ -11,11 +11,12 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
/** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'create' | 'update';
+export type FormType = 'create' | 'update' | 'detail';
/** 鏂板/淇敼妫�娴嬬粨鏋滅殑琛ㄥ崟 */
export function useQcIndicatorResultFormSchema(
formApi?: VbenFormApi,
+ readonly = false,
): VbenFormSchema[] {
return [
{
@@ -34,7 +35,7 @@
placeholder: '璇疯緭鍏ユ牱鍝佺紪鍙�',
},
rules: 'required',
- suffix: () =>
+ suffix: readonly ? undefined : () =>
h(
Button,
{
@@ -48,14 +49,6 @@
},
{ default: () => '鐢熸垚' },
),
- },
- {
- fieldName: 'sn',
- label: '鐗╄祫 SN',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ョ墿璧� SN',
- },
},
{
fieldName: 'remark',
@@ -79,18 +72,13 @@
width: 200,
},
{
- field: 'sn',
- title: '鐗╄祫SN',
- minWidth: 200,
- },
- {
field: 'remark',
title: '澶囨敞',
minWidth: 200,
},
{
title: '鎿嶄綔',
- width: 150,
+ width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
diff --git a/src/views/mes/qc/indicatorresult/components/form.vue b/src/views/mes/qc/indicatorresult/components/form.vue
index c6481b2..3984da5 100644
--- a/src/views/mes/qc/indicatorresult/components/form.vue
+++ b/src/views/mes/qc/indicatorresult/components/form.vue
@@ -40,11 +40,14 @@
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
+const isDetail = computed(() => formType.value === 'detail');
const items = ref<MesQcIndicatorResultApi.IndicatorResultDetail[]>([]);
const ctxData = ref<CtxData>();
const getTitle = computed(() =>
- formType.value === 'update'
+ isDetail.value
+ ? '妫�娴嬬粨鏋滆鎯�'
+ : formType.value === 'update'
? $t('ui.actionTitle.edit', ['妫�楠岀粨鏋�'])
: $t('ui.actionTitle.create', ['妫�楠岀粨鏋�']),
);
@@ -73,9 +76,21 @@
: [];
}
+function getDisplayValue(item: MesQcIndicatorResultApi.IndicatorResultDetail) {
+ if (item.value === undefined || item.value === null || item.value === '') {
+ return '鏈~鍐�';
+ }
+ if (item.valueType === MesQcResultValueType.DICT) {
+ return getValueOptions(item.valueSpecification).find(
+ (option) => option.value === String(item.value),
+ )?.label ?? item.value;
+ }
+ return item.value;
+}
+
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
- if (!ctxData.value) {
+ if (!ctxData.value || isDetail.value) {
return;
}
const { valid } = await formApi.validate();
@@ -130,11 +145,18 @@
items.value = [];
return;
}
- formApi.setState({ schema: useQcIndicatorResultFormSchema(formApi) });
// 鍔犺浇鏁版嵁
const data = modalApi.getData<CtxData>();
ctxData.value = data;
formType.value = data.formType;
+ formApi.setState({
+ schema: useQcIndicatorResultFormSchema(formApi, isDetail.value),
+ });
+ formApi.setDisabled(isDetail.value);
+ modalApi.setState({
+ showConfirmButton: !isDetail.value,
+ cancelText: isDetail.value ? '鍏抽棴' : $t('common.cancel'),
+ });
modalApi.lock();
try {
const detail = await getIndicatorResultDetail(
@@ -148,7 +170,9 @@
valueNumber:
(item.valueType === MesQcResultValueType.FLOAT ||
item.valueType === MesQcResultValueType.INTEGER) &&
- item.value !== null
+ item.value != null &&
+ String(item.value).trim() !== '' &&
+ Number.isFinite(Number(item.value))
? Number(item.value)
: undefined,
}));
@@ -157,7 +181,6 @@
id: detail.id,
code: detail.code,
remark: detail.remark,
- sn: detail.sn,
});
} finally {
modalApi.unlock();
@@ -181,8 +204,14 @@
<AForm.Item
:label="`妫�娴嬮」${index + 1}锛�${item.indicatorName ?? ''}`"
>
+ <div v-if="isDetail" class="whitespace-pre-wrap break-words py-1">
+ {{ getDisplayValue(item) }}
+ <div v-if="item.remark" class="mt-1 text-sm text-muted-foreground">
+ 澶囨敞锛歿{ item.remark }}
+ </div>
+ </div>
<InputNumber
- v-if="
+ v-else-if="
item.valueType === MesQcResultValueType.FLOAT ||
item.valueType === MesQcResultValueType.INTEGER
"
diff --git a/src/views/mes/qc/indicatorresult/components/list.vue b/src/views/mes/qc/indicatorresult/components/list.vue
index 48e11bb..2b4433d 100644
--- a/src/views/mes/qc/indicatorresult/components/list.vue
+++ b/src/views/mes/qc/indicatorresult/components/list.vue
@@ -55,6 +55,18 @@
.open();
}
+/** 鏌ョ湅妫�娴嬬粨鏋滐紝璐ㄦ瀹屾垚鍚庝粛鍙墦寮�鍙璇︽儏銆� */
+function handleDetail(row: MesQcIndicatorResultApi.IndicatorResult) {
+ formModalApi
+ .setData({
+ formType: 'detail',
+ id: row.id,
+ qcId: props.qcId,
+ qcType: props.qcType,
+ })
+ .open();
+}
+
/** 鍒犻櫎妫�娴嬬粨鏋� */
async function handleDelete(row: MesQcIndicatorResultApi.IndicatorResult) {
const hideLoading = message.loading({
@@ -120,6 +132,12 @@
<TableAction
:actions="[
{
+ label: '鏌ョ湅璇︽儏',
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ onClick: handleDetail.bind(null, row),
+ },
+ {
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
diff --git a/src/views/mes/qc/ipqc/data.ts b/src/views/mes/qc/ipqc/data.ts
index 7233c70..a6a4399 100644
--- a/src/views/mes/qc/ipqc/data.ts
+++ b/src/views/mes/qc/ipqc/data.ts
@@ -17,7 +17,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getDateRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProTaskSelect } from '#/views/mes/pro/task/components';
@@ -437,11 +437,25 @@
},
},
{
+ fieldName: 'inspectorUserId',
+ label: '妫�娴嬩汉鍛�',
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ placeholder: '璇烽�夋嫨妫�娴嬩汉鍛�',
+ showSearch: true,
+ optionFilterProp: 'label',
+ valueField: 'id',
+ },
+ },
+ {
fieldName: 'inspectDate',
label: '妫�娴嬫棩鏈�',
component: 'RangePicker',
componentProps: {
- ...getRangePickerDefaultProps(),
+ ...getDateRangePickerDefaultProps(),
},
},
];
diff --git a/src/views/mes/qc/iqc/data.ts b/src/views/mes/qc/iqc/data.ts
index af978f7..3ad70e6 100644
--- a/src/views/mes/qc/iqc/data.ts
+++ b/src/views/mes/qc/iqc/data.ts
@@ -13,7 +13,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getDateRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
@@ -178,6 +178,21 @@
componentProps: {
placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规鍙�',
},
+ suffix: () =>
+ h(
+ Button,
+ {
+ disabled: formType === 'detail',
+ type: 'default',
+ onClick: async () => {
+ const code = await generateAutoCode(
+ MesAutoCodeRuleCode.QC_IQC_VENDOR_BATCH_CODE,
+ );
+ await formApi?.setFieldValue('vendorBatch', code);
+ },
+ },
+ { default: () => '鐢熸垚' },
+ ),
},
{
fieldName: 'receivedQuantity',
@@ -335,7 +350,7 @@
label: '鏉ユ枡鏃ユ湡',
component: 'RangePicker',
componentProps: {
- ...getRangePickerDefaultProps(),
+ ...getDateRangePickerDefaultProps(),
},
},
{
@@ -343,7 +358,7 @@
label: '妫�娴嬫棩鏈�',
component: 'RangePicker',
componentProps: {
- ...getRangePickerDefaultProps(),
+ ...getDateRangePickerDefaultProps(),
},
},
{
diff --git a/src/views/mes/qc/ledger/data.ts b/src/views/mes/qc/ledger/data.ts
index e090a6e..b6ff88c 100644
--- a/src/views/mes/qc/ledger/data.ts
+++ b/src/views/mes/qc/ledger/data.ts
@@ -8,7 +8,7 @@
import { getDictOptions } from '@vben/hooks';
import { getSimpleUserList } from '#/api/system/user';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getDateRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
@@ -29,7 +29,7 @@
label: '妫�娴嬫棩鏈�',
component: 'RangePicker',
componentProps: {
- ...getRangePickerDefaultProps(),
+ ...getDateRangePickerDefaultProps(),
},
},
{
diff --git a/src/views/mes/qc/oqc/data.ts b/src/views/mes/qc/oqc/data.ts
index f27a382..e3c4a4c 100644
--- a/src/views/mes/qc/oqc/data.ts
+++ b/src/views/mes/qc/oqc/data.ts
@@ -13,7 +13,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getDateRangePickerDefaultProps } from '#/utils';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
@@ -371,11 +371,25 @@
},
},
{
+ fieldName: 'inspectorUserId',
+ label: '妫�娴嬩汉鍛�',
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ placeholder: '璇烽�夋嫨妫�娴嬩汉鍛�',
+ showSearch: true,
+ optionFilterProp: 'label',
+ valueField: 'id',
+ },
+ },
+ {
fieldName: 'inspectDate',
label: '妫�娴嬫棩鏈�',
component: 'RangePicker',
componentProps: {
- ...getRangePickerDefaultProps(),
+ ...getDateRangePickerDefaultProps(),
},
},
];
diff --git a/src/views/mes/qc/rqc/data.ts b/src/views/mes/qc/rqc/data.ts
index b7ca107..0f724b5 100644
--- a/src/views/mes/qc/rqc/data.ts
+++ b/src/views/mes/qc/rqc/data.ts
@@ -17,7 +17,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getDateRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
/** 琛ㄥ崟绫诲瀷 */
@@ -364,7 +364,7 @@
label: '妫�娴嬫棩鏈�',
component: 'RangePicker',
componentProps: {
- ...getRangePickerDefaultProps(),
+ ...getDateRangePickerDefaultProps(),
},
},
];
diff --git a/src/views/wls/barcode/components/printer-label.vue b/src/views/wls/barcode/components/printer-label.vue
index 7fbeee5..a6df06c 100644
--- a/src/views/wls/barcode/components/printer-label.vue
+++ b/src/views/wls/barcode/components/printer-label.vue
@@ -1,30 +1,184 @@
<script lang="ts" setup>
+import type { MesWmBarcodeApi } from '#/api/mes/wm/barcode';
+
+import { nextTick, ref } from 'vue';
+
import { Button, message } from 'ant-design-vue';
+
+import { createBarcode, getBarcodeByBusiness } from '#/api/mes/wm/barcode';
+
+import WlsBarcode from './barcode.vue';
defineOptions({ name: 'WlsPrinterLabel' });
-withDefaults(
+/** 鏍囩涓婇檮鍔犲睍绀虹殑涓氬姟瀛楁锛堝彲閫夛紝榛樿涓嶅睍绀猴級 */
+interface LabelField {
+ label: string;
+ value?: number | string;
+}
+
+const props = withDefaults(
defineProps<{
bizCode?: string;
bizId?: number;
- bizType?: string;
+ /** 涓氬姟鍚嶇О锛屼綔涓烘爣绛句富淇℃伅灞曠ず鍦ㄦ潯鐮佷笅鏂� */
+ bizName?: string;
+ bizType?: number;
+ /** 鏍囩鏍囬 */
+ title?: string;
+ /** 鎸夐挳鏂囨 */
labelText?: string;
+ /** 闄勫姞瀛楁锛屼竴鑸笉闇�瑕佷紶 */
+ fields?: LabelField[];
}>(),
{
bizCode: undefined,
bizId: undefined,
+ bizName: undefined,
bizType: undefined,
+ title: '',
labelText: '鏍囩鎵撳嵃',
+ fields: () => [],
},
);
-function handlePrint() {
- message.warning('鏍囩鎵撳嵃鍔熻兘鏆傛湭瀹炵幇锛屾暚璇锋湡寰�');
+const barcodeRef = ref<InstanceType<typeof WlsBarcode>>();
+const barcode = ref<MesWmBarcodeApi.Barcode>();
+const renderKey = ref(0);
+const printing = ref(false);
+/** 鎵撳嵃绐楀彛锛氶渶鍦ㄧ敤鎴风偣鍑绘椂鍚屾鎵撳紑锛屽惁鍒欎細琚祻瑙堝櫒鎷︽埅 */
+const printWindow = ref<null | Window>(null);
+
+function escapeHtml(value: string) {
+ return value
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>');
+}
+
+/** 鏋勫缓鏍囩 HTML锛氭爣棰� + 鏉$爜鍥� + 鍚嶇О锛堝眳涓級锛岄檮鍔犲瓧娈靛彲閫� */
+function buildLabelHtml(base64: string) {
+ const nameHtml = props.bizName
+ ? `<div class="label-name">${escapeHtml(props.bizName)}</div>`
+ : '';
+ const infoHtml = props.fields
+ .filter((item) => item.value !== undefined && item.value !== '')
+ .map(
+ (item) =>
+ `<p><span class="info-label">${escapeHtml(item.label)}</span>${escapeHtml(String(item.value))}</p>`,
+ )
+ .join('');
+ return `<!DOCTYPE html>
+<html><head><meta charset="UTF-8"><title>${escapeHtml(props.title || '鎵撳嵃鏍囩')}</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:Arial,"Microsoft YaHei",sans-serif;display:flex;justify-content:center;padding:12px}
+.label{width:320px;padding:8px;text-align:center}
+.label-title{font-size:13px;color:#333;margin-bottom:6px}
+.label-barcode{width:200px;margin:0 auto}
+.label-barcode img{display:block;width:100%}
+.label-name{margin-top:8px;font-size:16px;font-weight:700;line-height:1.4;word-break:break-all}
+.label-info{margin-top:8px;font-size:12px;line-height:1.6;text-align:left;color:#333}
+.label-info p{margin:2px 0}
+.info-label{color:#666;margin-right:4px}
+.info-label::after{content:"锛�"}
+@page{margin:5mm}
+@media print{body{padding:0}.label{width:auto;max-width:100%}}
+</style>
+</head><body><div class="label">
+${props.title ? `<div class="label-title">${escapeHtml(props.title)}</div>` : ''}
+<div class="label-barcode"><img src="${base64}" alt="鏉$爜" /></div>
+${nameHtml}
+${infoHtml ? `<div class="label-info">${infoHtml}</div>` : ''}
+</div></body></html>`;
+}
+
+/** 鏉$爜鍥炬覆鏌撳畬鎴愶紝鎶婃爣绛惧唴瀹瑰啓鍏ュ凡鎵撳紑鐨勬墦鍗扮獥鍙e苟璋冭捣鎵撳嵃 */
+function handleBarcodeDone() {
+ const win = printWindow.value;
+ const base64 = barcodeRef.value?.getImageBase64();
+ printWindow.value = null;
+ if (!win || win.closed) {
+ return;
+ }
+ if (!base64) {
+ message.warning('鏉$爜娓叉煋澶辫触锛屾棤娉曟墦鍗版爣绛�');
+ win.close();
+ return;
+ }
+ win.document.open();
+ win.document.write(buildLabelHtml(base64));
+ win.document.close();
+ setTimeout(() => win.print(), 500);
+}
+
+/** 鎵撳嵃鏍囩锛氭棤鏉$爜鏃跺厛鐢熸垚锛屽啀娓叉煋鏉$爜鍥惧悗鎵撳嵃 */
+async function handlePrint() {
+ if (!props.bizType || !props.bizId) {
+ message.warning('缂哄皯涓氬姟绫诲瀷鎴栦笟鍔$紪鍙凤紝鏃犳硶鎵撳嵃鏍囩');
+ return;
+ }
+ if (printing.value) {
+ return;
+ }
+ // 鍦ㄧ敤鎴锋墜鍔垮唴鍚屾鎵撳紑绐楀彛锛岄伩鍏嶅脊绐楄鎷︽埅
+ const win = window.open('', '_blank');
+ if (!win) {
+ message.error('鏃犳硶鎵撳紑鎵撳嵃绐楀彛锛岃妫�鏌ユ祻瑙堝櫒璁剧疆');
+ return;
+ }
+ win.document.write(
+ '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Arial;padding:20px">姝e湪鐢熸垚鏍囩锛岃绋嶅�欌��</body></html>',
+ );
+ printWindow.value = win;
+ printing.value = true;
+ try {
+ let data = await getBarcodeByBusiness(props.bizType, props.bizId);
+ if (!data?.content) {
+ await createBarcode({
+ bizCode: props.bizCode || '',
+ bizId: props.bizId,
+ bizName: props.bizName || '',
+ bizType: props.bizType,
+ });
+ data = await getBarcodeByBusiness(props.bizType, props.bizId);
+ }
+ if (!data?.content) {
+ message.warning('鏉$爜鐢熸垚澶辫触锛屾棤娉曟墦鍗版爣绛�');
+ win.close();
+ printWindow.value = null;
+ return;
+ }
+ barcode.value = data;
+ renderKey.value += 1;
+ await nextTick();
+ } catch {
+ message.error('鎵撳嵃鏍囩澶辫触锛岃閲嶈瘯');
+ win.close();
+ printWindow.value = null;
+ } finally {
+ printing.value = false;
+ }
}
</script>
<template>
- <Button type="link" @click="handlePrint">
+ <Button :loading="printing" @click="handlePrint">
{{ labelText }}
</Button>
+ <!-- 闅愯棌鐨勬潯鐮佹覆鏌撳鍣紝娓叉煋瀹屾垚鍚庡彇鍑哄浘鐗囧啓鍏ユ墦鍗扮獥鍙� -->
+ <div
+ v-if="barcode?.content"
+ class="pointer-events-none fixed top-0 -left-[9999px] h-0 w-0 overflow-hidden"
+ >
+ <WlsBarcode
+ :key="renderKey"
+ ref="barcodeRef"
+ :content="barcode.content"
+ :format="barcode.format"
+ :height="150"
+ :width="280"
+ @done="handleBarcodeDone"
+ />
+ </div>
</template>
diff --git a/src/views/wls/batch/data.ts b/src/views/wls/batch/data.ts
index 9af407b..dfa2066 100644
--- a/src/views/wls/batch/data.ts
+++ b/src/views/wls/batch/data.ts
@@ -208,11 +208,6 @@
width: 120,
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- width: 110,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
width: 110,
diff --git a/src/views/wls/itemreceipt/modules/line-list.vue b/src/views/wls/itemreceipt/modules/line-list.vue
index 2e5a09b..90e014f 100644
--- a/src/views/wls/itemreceipt/modules/line-list.vue
+++ b/src/views/wls/itemreceipt/modules/line-list.vue
@@ -8,6 +8,7 @@
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
+import { BarcodeBizTypeEnum } from '@vben/constants';
import { message } from 'ant-design-vue';
@@ -232,7 +233,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="ITEM_BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</div>
</template>
diff --git a/src/views/wls/outsourcereceipt/modules/line-list.vue b/src/views/wls/outsourcereceipt/modules/line-list.vue
index 1dcf161..dbb790d 100644
--- a/src/views/wls/outsourcereceipt/modules/line-list.vue
+++ b/src/views/wls/outsourcereceipt/modules/line-list.vue
@@ -8,6 +8,7 @@
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
+import { BarcodeBizTypeEnum } from '@vben/constants';
import { message } from 'ant-design-vue';
@@ -227,7 +228,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</div>
</template>
diff --git a/src/views/wls/packages/data.ts b/src/views/wls/packages/data.ts
index f771d1c..7876069 100644
--- a/src/views/wls/packages/data.ts
+++ b/src/views/wls/packages/data.ts
@@ -299,11 +299,6 @@
minWidth: 120,
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- minWidth: 100,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
minWidth: 120,
@@ -390,11 +385,6 @@
field: 'invoiceCode',
title: '鍙戠エ缂栧彿',
minWidth: 120,
- },
- {
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- minWidth: 100,
},
{
field: 'clientName',
@@ -621,11 +611,6 @@
field: 'invoiceCode',
title: '鍙戠エ缂栧彿',
minWidth: 120,
- },
- {
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- minWidth: 100,
},
{
field: 'clientName',
diff --git a/src/views/wls/productreceipt/data.ts b/src/views/wls/productreceipt/data.ts
index 3b9188b..aee28f1 100644
--- a/src/views/wls/productreceipt/data.ts
+++ b/src/views/wls/productreceipt/data.ts
@@ -1,5 +1,6 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
@@ -10,6 +11,7 @@
import {
DICT_TYPE,
MesAutoCodeRuleCode,
+ MesProWorkOrderStatusEnum,
} from '@vben/constants';
import { Button } from 'ant-design-vue';
@@ -106,7 +108,9 @@
label: '鐢熶骇璁㈠崟',
component: markRaw(ProWorkOrderSelect),
componentProps: {
- checkMethod: (row: any) => row.productionStatus !== 0,
+ checkMethod: (row: MesProWorkOrderApi.WorkOrder) =>
+ row.status === MesProWorkOrderStatusEnum.CONFIRMED ||
+ row.status === MesProWorkOrderStatusEnum.FINISHED,
},
},
{
diff --git a/src/views/wls/productreceipt/modules/line-list.vue b/src/views/wls/productreceipt/modules/line-list.vue
index 26442fb..f384833 100644
--- a/src/views/wls/productreceipt/modules/line-list.vue
+++ b/src/views/wls/productreceipt/modules/line-list.vue
@@ -244,7 +244,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</div>
</template>
diff --git a/src/views/wls/productsales/data.ts b/src/views/wls/productsales/data.ts
index 9522d75..ddfc5a3 100644
--- a/src/views/wls/productsales/data.ts
+++ b/src/views/wls/productsales/data.ts
@@ -351,11 +351,6 @@
minWidth: 160,
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- minWidth: 120,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
minWidth: 120,
diff --git a/src/views/wls/returnissue/modules/line-list.vue b/src/views/wls/returnissue/modules/line-list.vue
index 14c7ccf..2a174d5 100644
--- a/src/views/wls/returnissue/modules/line-list.vue
+++ b/src/views/wls/returnissue/modules/line-list.vue
@@ -241,7 +241,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</div>
</template>
diff --git a/src/views/wls/returnsales/data.ts b/src/views/wls/returnsales/data.ts
index 7a3214b..25abc11 100644
--- a/src/views/wls/returnsales/data.ts
+++ b/src/views/wls/returnsales/data.ts
@@ -232,11 +232,6 @@
minWidth: 140,
},
{
- field: 'clientCode',
- title: '瀹㈡埛缂栫爜',
- minWidth: 120,
- },
- {
field: 'clientName',
title: '瀹㈡埛鍚嶇О',
minWidth: 150,
diff --git a/src/views/wls/returnsales/modules/line-list.vue b/src/views/wls/returnsales/modules/line-list.vue
index 9c635e2..a07a7b2 100644
--- a/src/views/wls/returnsales/modules/line-list.vue
+++ b/src/views/wls/returnsales/modules/line-list.vue
@@ -9,6 +9,7 @@
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
+import { BarcodeBizTypeEnum } from '@vben/constants';
import { message } from 'ant-design-vue';
@@ -242,7 +243,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</div>
</template>
diff --git a/src/views/wls/returnvendor/modules/line-list.vue b/src/views/wls/returnvendor/modules/line-list.vue
index 7f67f49..ffbdd92 100644
--- a/src/views/wls/returnvendor/modules/line-list.vue
+++ b/src/views/wls/returnvendor/modules/line-list.vue
@@ -8,6 +8,7 @@
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
+import { BarcodeBizTypeEnum } from '@vben/constants';
import { message } from 'ant-design-vue';
@@ -228,7 +229,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</div>
</template>
diff --git a/src/views/wls/transfer/modules/line-list.vue b/src/views/wls/transfer/modules/line-list.vue
index 05d8165..c7252ed 100644
--- a/src/views/wls/transfer/modules/line-list.vue
+++ b/src/views/wls/transfer/modules/line-list.vue
@@ -8,6 +8,7 @@
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
+import { BarcodeBizTypeEnum } from '@vben/constants';
import { message } from 'ant-design-vue';
@@ -220,7 +221,7 @@
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
- biz-type="BATCH"
+ :biz-type="BarcodeBizTypeEnum.BATCH"
/>
</template>
</Grid>
diff --git a/vite.config.ts b/vite.config.ts
index 6b61522..9382ae0 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -82,7 +82,7 @@
proxy: {
"/admin-api": {
changeOrigin: true,
- target: "http://localhost:48081",
+ target: "http://localhost:48080",
ws: true,
},
},
--
Gitblit v1.9.3