Forráskód Böngészése

fix:970仓储管理看板首先新需求

mlchen 1 hete
szülő
commit
7eed2e059f
25 módosított fájl, 1220 hozzáadás és 1002 törlés
  1. 80 0
      docs/仓储首页重构实施说明.md
  2. 83 0
      scripts/check-warehouse-dashboard.cjs
  3. 17 0
      src/api/warehouseManagement/dashboard.js
  4. 1 0
      src/api/warehouseManagement/warehouseDefinition.js
  5. 2 0
      src/router/index.js
  6. 42 8
      src/views/warehouseManagement/inboundRequests/components/search.vue
  7. 10 2
      src/views/warehouseManagement/inboundRequests/components/tgTable.vue
  8. 25 4
      src/views/warehouseManagement/inboundRequests/index.vue
  9. 3 614
      src/views/warehouseManagement/index/common.vue
  10. 132 0
      src/views/warehouseManagement/index/components/DashboardActions.vue
  11. 46 0
      src/views/warehouseManagement/index/components/DashboardChart.vue
  12. 89 0
      src/views/warehouseManagement/index/components/HomeOverview.vue
  13. 29 0
      src/views/warehouseManagement/index/dashboard-element.scss
  14. 33 0
      src/views/warehouseManagement/index/dashboard-layout.scss
  15. 17 0
      src/views/warehouseManagement/index/dashboard-screens.js
  16. 235 0
      src/views/warehouseManagement/index/dashboard-theme.scss
  17. 36 0
      src/views/warehouseManagement/index/dashboard-utils.js
  18. 211 0
      src/views/warehouseManagement/index/dashboard.vue
  19. 76 0
      src/views/warehouseManagement/index/detail.vue
  20. 6 20
      src/views/warehouseManagement/index/index.vue
  21. 3 348
      src/views/warehouseManagement/index/yuxin.vue
  22. 12 1
      src/views/warehouseManagement/stockManagement/add.vue
  23. 11 3
      src/views/warehouseManagement/warehouseApplication/index.vue
  24. 16 1
      src/views/warehouseManagement/warehouseDefinition/add.vue
  25. 5 1
      src/views/warehouseManagement/warehouseDefinition/details.vue

+ 80 - 0
docs/仓储首页重构实施说明.md

@@ -0,0 +1,80 @@
+# 仓储首页重构实施说明
+
+更新日期:2026-10-16。
+
+首页继续嵌套在 EleAdmin 页面内容区,保留原系统菜单、页头及标签栏。仅移除看板自身重复的顶部品牌/筛选栏和左侧导航,展示用户截图红框内的数据总览。看板主内容使用内容区可用宽度并随内容增长,根节点不得使用视口级 `fixed/inset/z-index` 或强制系统布局全屏。保留首页卡片下钻、明细筛选和返回首页入口;仓库/单位继续从现有路由参数及接口默认值读取。此约定以用户最新确认的“嵌套在页面里面、需要之前系统菜单”为准。
+
+## 页面与组件
+
+普通及宇信首页通过 `warehouseManagement/index/index.vue`、`common.vue`、`yuxin.vue` 嵌套进入 `dashboard.vue`。三个历史入口都保留原路径,避免新增“点击首页”才能打开的独立看板路由。入口组合首页、9个统计视图、待我审批和4个原业务视图,共15个视图;首页不保留快捷新建按钮,新增单据从出入库列表进入。
+
+| 文件 | 职责 |
+| --- | --- |
+| `index/dashboard.vue` | 路由筛选、真实请求、刷新生命周期、报表导出、业务导航及批量收货队列 |
+| `index/components/HomeOverview.vue` | 待办、指标、图表、预警与动态展示,事件上抛到入口 |
+| `index/components/DashboardChart.vue` | ECharts绘图、点击下钻、尺寸适配与实例销毁 |
+| `index/detail.vue`、`dashboard-screens.js` | 真实明细列、筛选、分页、操作和统计口径 |
+| `index/components/DashboardActions.vue` | 阈值、采购需求、处置、财务登记与逐单结果 |
+| `api/warehouseManagement/dashboard.js` | 统一鉴权请求和业务响应校验,身份保留字符串 |
+
+`dashboardView`取值:`home/ledger/dead/io/todo/age/alerts/turnover/locations/qc/flow/addin/addout/ioapp/inapp`。筛选保存在路由查询中,显式空`unit`表示不自动选默认单位。月份初值使用本地月份完整首末日,接口以服务端规则再次校验。
+
+首页以外的14个配套视图如下:
+
+| 视图 | 路由值 |
+| --- | --- |
+| 库存台账 | `ledger` |
+| 呆滞品分析 | `dead` |
+| 出入库动态 | `io` |
+| 待办单据 | `todo` |
+| 批次库龄 | `age` |
+| 安全库存预警 | `alerts` |
+| 周转分析 | `turnover` |
+| 库位地图 | `locations` |
+| 入库质检 | `qc` |
+| 待我审批 | `flow` |
+| 新增入库单 | `addin` |
+| 新增出库单 | `addout` |
+| 出入库申请 | `ioapp` |
+| 入总库申请 | `inapp` |
+
+## 数据口径与刷新
+
+- 使用`/wms/index/home-options`、`home-summary`、`home-detail`及`home-alert-config`,不保留原型示例数字。
+- 不同基本单位不相加。库存含待检与锁定;可用量扣锁并排除未放行。金额按可核实入库价估值,缺价不显示为零。
+- 呆滞认定要求首次入库及最近实际消耗均超过180天,占比使用呆滞估值与全部库存估值。日期或估值不足时展示原因。
+- `summary.capturedAt`显示为“服务端查询时间”;7日前日末实际采集时间由`comparisonReason`说明,不能将本次接口时间当作日结时间。
+- 总统计15分钟、预警5分钟、动态30秒刷新;后两类通过`section=alerts/activities`更新对应区块。请求序号及生命周期世代阻止旧响应覆盖新筛选;页面停用、销毁后清理定时器。
+- 明细真实分页,导出沿当前筛选逐页读取。空值保留未知语义,缺日期显示“日期待核实”。
+
+## 原业务链路
+
+- 待入/出库、入总库按本月整单待办打开,默认状态0;清除首页物料、单位筛选,避免与整单计数口径冲突。入总库原API在每次查询时固定`type=1`,重置筛选不会混入出库申请。
+- 批量收货从真实WMS申请列表选择,逐单打开原入库表单,完成原校验和包装明细组装后暂存请求,全部核对后调用`/wms/dashboard/action/batch-receive`。来源为`/wms/applystoragedetail/listByAppIds`;`extInfo.dashboardApplyDetailId`保留申请明细ID,采购来源`sourceReceiveProductId`原样保留。
+- 批量收货确认期间阻止重复请求;旧表单事件必须与当前`inId`匹配。成功项移除,失败项保留完整请求及原`requestKey`供重试。成功表示保存并提交原审批或质检,库存变动仍取决于原流程。
+- 库存盘点入口默认`type=inventory`;待审批使用真实当前任务分页及逐单审批返回值。业务身份禁止转成JavaScript数值。
+- 阈值按仓库与物料基本单位配置,要求`0≤最低≤安全≤最高`且精度4位;历史单位不一致时提示核对。采购需求保存为EOM草稿,需在原需求详情核对并提交。
+- 呆滞处置依次保存草稿、提交真实审批、执行原调拨或出库、登记独立财务凭证。驳回/撤销可重新提交;审批本身不变动库存。财务附件复用原上传组件。
+- 库位用途字段为`allocationPurpose=ordinary/repair/temporary/virtual`,启停字段为`enabled=0/1`;新货位普通且启用,历史空值保留待核实。
+
+## 跨系统与权限
+
+从当前用户授权菜单解析门户同源入口。WT接收`warehouseScope/warehouseId/month/taskId`后,先按真实任务ID和授权范围查询,再打开原审批容器。WT新增`stagnantDisposal/detailDialog.vue`读取真实处置申请,`submit.vue`复用现有审批意见、通过与驳回处理。EOM通过`requirementId`打开原采购需求详情。
+
+本地没有QMS前端项目,质检操作仅标“打开检验工作台”,传批次参数;未证明目标工作台消费这些参数,不宣称可精确打开质检报告。
+
+WMS/WT/EOM在`dashboard=1`时启用深色Element主题。WMS样式限制在页面内容区及看板专用弹层:`DashboardActions`使用`wms-dashboard-dialog`,其选择框使用`wms-dashboard-popper`;不得直接覆盖整个`body`下的Element组件。系统页头部门、角色输入框及选项面板保持原白色样式。接口仍由服务端校验授权仓库及业务权限,不以隐藏前端按钮代替鉴权。
+
+## 验证与验收边界
+
+最终定向检查:WMS 22个、WT 5个、EOM 3个改动的Vue/JS/SCSS文件,模板编译、Babel脚本解析及看板Sass编译均无失败;三个前端仓库`git diff --check`通过。WT处置审批另用Vue实际选项合并断言,确认无独立模板的子组件继承原渲染函数、审批方法、任务参数,且后端两个组件路径均指向本地现存文件。
+
+上述重构检查未执行完整前端构建、Maven、浏览器或设备验证,也未调用真实写接口。Sass仅出现现有旧JS API弃用提示。新增文档和断言脚本均未被忽略规则排除,未暂存或提交本次改动。
+
+2026-10-16展示范围修正:实际本地浏览器确认首页位于`.ele-admin-content`内,系统菜单、页头和标签栏可见;部门、角色输入框和展开的选项面板计算背景均为`rgb(255, 255, 255)`。卡片进入库存台账及返回首页已验证。修正后的布局、看板及操作弹层模板和脚本解析、三份看板SCSS编译、现有业务断言和`git diff --check`通过;系统布局文件无最终差异。本次未执行完整构建、设备验证或真实写操作。
+
+运行`node scripts/check-warehouse-dashboard.cjs`可重复验证月份边界、空值/零值、大整数身份、授权菜单路径、整单待办筛选、“其他”分组下钻、重复收货事件与失败项幂等重试;脚本直接加载现有组件方法,使用请求桩,不访问数据库。
+
+部署前须在目标环境联调真实响应、菜单路径、弹层布局、审批模型及权限、采购草稿、批量收货审批/质检、处置执行与财务附件。日末数据需等真实采集积累,缺失历史不能用样例或补造值替代。后端表结构与模型准备参见后端项目`docs/仓储管理首页重构-统计与数据核验.md`及影响地图。
+
+2026-10-16首页入口约定:本次将“白色字典背景”暂按“白色字体”处理,标题“仓储管理总览”和“下午好,xxx”使用白色字体,保留看板深色背景;库存总量、库存金额、库存分类“查看台账”及分类图点击统一跳转原库存台账`/warehouseManagement/stockLedger`,待入库与入库管理使用`/warehouseManagement/stockManagement`,待出库与出库管理使用`/warehouseManagement/outgoingManagement`,不进入首页自建台账或出入库列表。原系统入口不携带`dashboard=1`;`ledger`事件与`stockLedger`事件统一分流,避免其他统计页的“查看库存”再次进入自建台账。前文重构记录中的台账下钻约定以本条为准。原台账未读取首页分类查询参数,本次仅保证进入原页面,不宣称自动选中分类。

+ 83 - 0
scripts/check-warehouse-dashboard.cjs

@@ -0,0 +1,83 @@
+// 只读断言:直接加载现有组件方法,禁止发出真实业务请求。
+const assert = require('assert/strict');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+const root = path.resolve(__dirname, '..');
+const folder = path.join(root, 'src/views/warehouseManagement/index');
+const compiler = require(path.join(root, 'node_modules/vue-template-compiler'));
+function load(file, bindings = {}) {
+  let source = fs.readFileSync(path.join(folder, file), 'utf8');
+  if (file.endsWith('.vue')) source = compiler.parseComponent(source).script.content;
+  source = source.replace(/^import .*;$/gm, '').replace(/export default/, 'module.exports =').replace(/export (function|const)/g, '$1');
+  const context = { module: { exports: {} }, ...bindings };
+  vm.runInNewContext(source, context, { filename: file });
+  return { value: context.module.exports, context };
+}
+const { context: utils } = load('dashboard-utils.js');
+const { monthRange, cleanQuery, displayNumber, findPortalEntry } = utils;
+assert.equal(monthRange(new Date(2024, 1, 15)).endTime, '2024-02-29 23:59:59');
+assert.equal(monthRange(new Date(2026, 11, 31)).startTime, '2026-12-01 00:00:00');
+assert.equal(displayNumber(null), '—');
+assert.equal(displayNumber(''), '—');
+assert.equal(displayNumber(0), '0');
+assert.equal(cleanQuery({ id: '9223372036854775806', status: 0, scope: false, empty: '', missing: null }).id, '9223372036854775806');
+assert.equal(cleanQuery({ status: 0 }).status, '0');
+assert.equal(cleanQuery({ scope: false }).scope, 'false');
+const menus = [{ path: '/page-wt', children: [{ path: '/bpm', children: [{ path: 'todo', component: 'bpm/todo/index' }] }] }];
+assert.equal(findPortalEntry(menus, n => /bpm\/todo/.test(n.component || '')), '/page-wt/bpm/todo');
+assert.equal(findPortalEntry([{ path: '/page-eom', children: [{ path: '/purchasingManage/purchaseNeedManage', name: '采购' }] }], n => n.name === '采购'), '/page-eom/purchasingManage/purchaseNeedManage');
+assert.equal(findPortalEntry([{ path: 'https://example.com', name: '外部' }], () => true), '');
+assert.equal(findPortalEntry(menus, n => n.name === '无权限菜单'), '');
+let calls = 0;
+const { context: screens } = load('dashboard-screens.js');
+const { value: page } = load('dashboard.vue', {
+  HomeOverview: {}, DashboardDetail: {}, DashboardActions: {}, ...utils, screens: screens.screens,
+  dashboardAction: async () => { calls++; return [{ key: 'a', success: true }, { key: 'b', success: false }]; }
+});
+for (const entry of ['index.vue', 'common.vue', 'yuxin.vue']) {
+  const source = fs.readFileSync(path.join(folder, entry), 'utf8');
+  assert.match(source, /from ['"]\.\/dashboard\.vue['"]/);
+  assert.doesNotMatch(source, /from ['"]\.\/index\.vue['"]/);
+}
+const open = { filters: { unit: '吨', categoryId: '1', stockBatchId: '2' }, view: 'home', route(view, filters) { this.target = view; this.nextFilters = filters; } };
+page.methods.open.call(open, 'todo');
+assert.equal(open.nextFilters.type, 'inventory');
+page.methods.open.call(open, 'ioapp');
+assert.equal(open.nextFilters.status, '0');
+assert.equal(open.nextFilters.unit, '');
+assert.equal(open.nextFilters.categoryId, undefined);
+const receipt = { receiveQueue: [{ id: '1' }, { id: '2' }], preparedReceipts: [], receiving: false, $route: { query: { inId: '1' } }, nextReceipt() {} };
+page.methods.prepareReceipt.call(receipt, { sourceBizNo: 'R1' });
+page.methods.prepareReceipt.call(receipt, { sourceBizNo: 'R1' });
+assert.equal(receipt.preparedReceipts.length, 1, '旧表单重复事件不能准备到下一张申请');
+const { value: overview } = load('components/HomeOverview.vue', { DashboardChart: {}, displayNumber });
+const kpis = overview.computed.kpis.call({ summary: {}, unit: '吨' });
+assert.equal(kpis.find(item => item.key === 'stock').target, 'stockLedger');
+assert.equal(kpis.find(item => item.key === 'value').target, 'stockLedger');
+const todos = overview.computed.todoCards.call({ summary: {} });
+assert.equal(todos.find(item => item.key === 'in').target, 'stockManagement');
+assert.equal(todos.find(item => item.key === 'out').target, 'outgoingManagement');
+const legacy = { filters: { unit: '吨' }, $router: { push(location) { this.location = location; return Promise.resolve(); } } };
+for (const [view, expectedPath] of Object.entries({
+  ledger: '/warehouseManagement/stockLedger',
+  stockLedger: '/warehouseManagement/stockLedger',
+  stockManagement: '/warehouseManagement/stockManagement',
+  outgoingManagement: '/warehouseManagement/outgoingManagement'
+})) {
+  page.methods.open.call(legacy, view);
+  assert.equal(legacy.$router.location.path, expectedPath);
+  assert.equal(legacy.$router.location.query.dashboard, undefined, '原系统入口不应携带首页看板标记');
+}
+let navigation = 0;
+overview.methods.categorySelect.call({ $message: { info() {} }, $emit() { navigation++; } }, { id: 'other' });
+assert.equal(navigation, 0, '其他分组不能作为具体物料下钻');
+(async () => {
+  const state = { receiving: false, preparedReceipts: [{ requestKey: 'a' }, { requestKey: 'b' }], receiveQueue: [], filters: {}, $confirm: async () => {}, route() {}, $refs: { actions: { showResults() {} } }, refresh() {}, $message: { error(message) { throw new Error(message); } } };
+  await Promise.all([page.methods.submitReceipts.call(state), page.methods.submitReceipts.call(state)]);
+  assert.equal(calls, 1, '重复点击只能发出一次批量请求');
+  assert.equal(state.preparedReceipts.length, 1);
+  assert.equal(state.preparedReceipts[0].requestKey, 'b', '失败申请重试保留原幂等键');
+  assert.equal(state.receiving, false);
+  console.log('仓储看板业务断言通过:月份、空值、大整数身份、授权入口、待办范围、分组下钻、收货去重与失败重试。');
+})().catch(error => { console.error(error); process.exitCode = 1; });

+ 17 - 0
src/api/warehouseManagement/dashboard.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request';
+
+// 首页与业务操作沿用统一鉴权和大整数解析,业务身份保持字符串。
+async function dataOf(promise) {
+  const { data } = await promise;
+  if (data.code == 0) return data.data;
+  throw new Error(data.message || '请求未成功,请稍后重试');
+}
+export const homeOptions = (params) => dataOf(request.get('/wms/index/home-options', { params }));
+export const homeSummary = (params) => dataOf(request.get('/wms/index/home-summary', { params }));
+export const homeDetail = (params) => dataOf(request.get('/wms/index/home-detail', { params }));
+export const warehouseTasks = (data) => dataOf(request.post('/bpm/task/todo-page', data));
+export const dashboardAction = (path, data) => dataOf(request.post('/wms/dashboard/action/' + path, data));
+export const disposalList = (params) => dataOf(request.get('/wms/dashboard/action/disposal', { params }));
+export const disposalDetail = (id) => dataOf(request.get('/wms/dashboard/action/disposal/' + encodeURIComponent(id)));
+export const alertConfig = (params) => dataOf(request.get('/wms/index/home-alert-config', { params }));
+export const saveAlertConfig = (data) => dataOf(request.post('/wms/index/home-alert-config', data));

+ 1 - 0
src/api/warehouseManagement/warehouseDefinition.js

@@ -91,6 +91,7 @@ export default {
     if (res.data.code == 0) {
       return res.data.message;
     }
+    throw new Error(res.data.message || '货位状态保存失败');
   },
   //删除
   delete: async (id) => {

+ 2 - 0
src/router/index.js

@@ -9,6 +9,7 @@ import store from '@/store';
 import { getToken, setToken, getCurrentUser  } from '@/utils/token-util';
 import { routes, getMenuRoutes } from './routes';
 import { changeRole } from '@/api/layout/index';
+import '@/views/warehouseManagement/index/dashboard-element.scss';
 
 Vue.use(VueRouter);
 
@@ -72,6 +73,7 @@ router.beforeEach((to, from, next) => {
 });
 
 router.afterEach((to) => {
+  document.body.classList.toggle('wms-dashboard-theme', to.query.dashboard === '1');
   if (!to.path.includes(REDIRECT_PATH) && NProgress.isStarted()) {
     setTimeout(() => {
       NProgress.done(true);

+ 42 - 8
src/views/warehouseManagement/inboundRequests/components/search.vue

@@ -14,6 +14,21 @@
           <el-input clearable v-model="where.code" placeholder="请输入" />
         </el-form-item>
       </el-col>
+      <el-col v-bind="styleResponsive ? { lg: 6, md: 10 } : { span: 6 }">
+        <el-form-item label="申请日期:">
+          <el-date-picker v-model="where.time" type="daterange" value-format="yyyy-MM-dd"
+            range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" class="w100" />
+        </el-form-item>
+      </el-col>
+      <el-col v-bind="styleResponsive ? { lg: 6, md: 10 } : { span: 6 }">
+        <el-form-item label="状态:">
+          <el-select v-model="where.status" placeholder="全部" clearable class="w100">
+            <el-option :value="0" label="待处理" />
+            <el-option :value="1" label="已处理" />
+            <el-option :value="2" label="已撤销" />
+          </el-select>
+        </el-form-item>
+      </el-col>
 
       <el-col v-bind="styleResponsive ? { lg: 6, md: 10 } : { span: 6 }">
         <el-form-item label="物品编码:">
@@ -83,7 +98,9 @@ export default {
       workOrderCode: '',
       warehouseName: '',
       categoryName: '',
-      factoryId: ''
+      factoryId: '',
+      status: '',
+      time: []
     };
     return {
       // 表单数据
@@ -102,16 +119,33 @@ export default {
       return this.$store.state.theme.styleResponsive;
     }
   },
-  watch: {
-    activeName(val) {
-      console.log('val-------', val);
-      this.where = { ...this.defaultWhere };
-    }
-  },
   created() {
     this.getFactoryList();
   },
   methods: {
+    applyRouteFilters(query) {
+      const { factoryId, code, categoryCode, categoryName, startTime, endTime, status } = query;
+      const datePattern = /^\d{4}-\d{2}-\d{2}$/;
+      const start = typeof startTime === 'string' ? startTime.slice(0, 10) : '';
+      const end = typeof endTime === 'string' ? endTime.slice(0, 10) : '';
+      this.where = {
+        ...this.defaultWhere,
+        ...(typeof factoryId === 'string' && /^\d+$/.test(factoryId) ? { factoryId } : {}),
+        ...(typeof (code || query.keyword) === 'string' ? { code:code || query.keyword } : {}),
+        ...(query.warehouseId ? { warehouseId:query.warehouseId } : {}),
+        ...(query.categoryId ? { categoryId:query.categoryId } : {}),
+        ...(query.unit ? { unit:query.unit } : {}),
+        ...(typeof categoryCode === 'string' ? { categoryCode } : {}),
+        ...(typeof categoryName === 'string' ? { categoryName } : {}),
+        ...(['0', '1', '2'].includes(String(status)) ? { status: Number(status) } : {}),
+        time: datePattern.test(start) && datePattern.test(end) && start <= end ? [start, end] : []
+      };
+    },
+    query() {
+      const { time, ...where } = this.where;
+      return { ...where, ...(time && time.length === 2
+        ? { startTime: time[0] + ' 00:00:00', endTime: time[1] + ' 23:59:59' } : {}) };
+    },
     //获取工厂列表
     async getFactoryList() {
       const res = await warehouseDefinition.getFactoryarea({
@@ -123,7 +157,7 @@ export default {
     },
     /* 搜索 */
     search() {
-      this.$emit('search', this.where);
+      this.$emit('search', this.query());
     },
     /*  重置 */
     reset() {

+ 10 - 2
src/views/warehouseManagement/inboundRequests/components/tgTable.vue

@@ -7,8 +7,12 @@
       height="calc(100vh-300px)"
       :pageSize="20"
       :datasource="datasource"
+      :selection.sync="selection"
       cache-key="workOrderTable"
     >
+      <template v-slot:toolbar v-if="$route.query.dashboard === '1' && type === '1'">
+        <el-button type="primary" :disabled="!selection.length" @click="$emit('batch-receive', selection)">批量收货({{ selection.length }})</el-button>
+      </template>
       <template v-slot:code="{ row }">
         <el-link type="primary" :underline="false" @click="details(row)">{{
           row.code
@@ -68,6 +72,7 @@
         outputSceneState,
         sceneState,
         loading: false,
+        selection: [],
         statusOpt: [
           { label: '待处理', value: 0 },
           { label: '已处理', value: 1 },
@@ -79,6 +84,7 @@
       // 表格列配置
       columns() {
         return [
+          ...(this.$route.query.dashboard === '1' && this.type === '1' ? [{ type:'selection',width:55,align:'center',selectable:(row) => row.status === 0 }] : []),
           {
             columnKey: 'index',
             label: '序号',
@@ -184,6 +190,7 @@
           this.$router.push({
             path: '/warehouseManagement/stockManagement/add',
             query: {
+              ...(this.$route.query.dashboard === '1' ? { ...this.$route.query,dashboard:'1' } : {}),
               code: row.code,
               inId: row.id,
               type: '1',
@@ -195,6 +202,7 @@
           this.$router.push({
             path: '/warehouseManagement/outgoingManagement/add',
             query: {
+              ...(this.$route.query.dashboard === '1' ? { ...this.$route.query,dashboard:'1' } : {}),
               code: row.code,
               id: row.id,
               type: '2',
@@ -206,9 +214,9 @@
       },
       /* 刷新表格 */
       reload(where) {
-        console.log(where, '11111where');
+        this.selection = [];
         this.$nextTick(() => {
-          if (this.$refs.table1 && this.$refs.table1.loading) {
+          if (this.$refs.table1) {
             this.$refs.table1.reload({ page: 1, where });
           }
         });

+ 25 - 4
src/views/warehouseManagement/inboundRequests/index.vue

@@ -5,7 +5,7 @@
       </search>
       <el-tabs type="border-card" v-model="activeName" @tab-click="handleClick">
         <el-tab-pane label="入库" name="入库">
-          <tgTable v-if="activeName == '入库'" ref="tableRef" type="1"></tgTable>
+          <tgTable v-if="activeName == '入库'" ref="tableRef" type="1" @batch-receive="$emit('batch-receive', $event)"></tgTable>
         </el-tab-pane>
         <el-tab-pane label="出库" name="出库">
           <tgTable v-if="activeName == '出库'" ref="tableRef" type="2"></tgTable>
@@ -36,15 +36,36 @@ export default {
       loading: false
     };
   },
-  created() { },
+  created() {
+    this.applyDashboardFilters();
+  },
+  watch: {
+    '$route.query': {
+      handler() { this.applyDashboardFilters(); },
+      deep: true
+    }
+  },
   methods: {
+    // 首页下钻沿用申请列表支持的筛选条件。
+    applyDashboardFilters() {
+      const query = this.$route.query;
+      const direction = query.direction || query.tab;
+      this.activeName = ['out','出库','2'].includes(String(direction)) ? '出库' : '入库';
+      this.$nextTick(() => {
+        if (!this.$refs.searchRef) return;
+        this.$refs.searchRef.applyRouteFilters(query);
+        this.$refs.searchRef.search();
+      });
+    },
     handleClick(tab) {
       this.activeName = tab.name;
-      this.$refs.tableRef && this.$refs.tableRef.reload(tab.name);
+      this.$nextTick(() => {
+        if (this.$refs.tableRef) this.$refs.tableRef.reload(this.$refs.searchRef.query());
+      });
     },
     /* 刷新表格 */
     reload(where) {
-      this.$refs.tableRef.$refs.table1.reload({ where });
+      if (this.$refs.tableRef) this.$refs.tableRef.reload(where);
     }
   }
 };

+ 3 - 614
src/views/warehouseManagement/index/common.vue

@@ -1,616 +1,5 @@
-<template>
-  <div id="my_index">
-    <div class="viewBar">
-      <div v-for="item in list">
-        <div class="item">
-          <div class="title"
-            >{{ item.title }}<span>{{ item.unit }}</span></div
-          >
-          <div class="count">{{ item.count }}</div>
-        </div>
-        <div class="icon">
-          <img :src="item.icon" />
-        </div>
-      </div>
-    </div>
-    <div class="echart_box">
-      <div class="left">
-        <div>
-          <div class="title_box">库总量同比及趋势分析</div>
-          <div class="erchats1"></div>
-        </div>
-        <div>
-          <div class="title_box">物料库存周转趋势分析</div>
-          <div class="erchats2"></div>
-        </div>
-      </div>
-      <div class="right">
-        <div>
-          <div class="title_box">在库总量分析</div>
-          <div class="erchats3"></div>
-        </div>
-        <div>
-          <div class="title_box">呆滞品总量分析</div>
-          <div class="erchats4"></div>
-        </div>
-        <!-- <div>
-          <div class="title_box">在库总量分析</div>
-          <div class="erchats5"></div>
-        </div> -->
-      </div>
-    </div>
-  </div>
-</template>
-
+<template><warehouse-dashboard /></template>
 <script>
-  import * as echarts from 'echarts';
-  import indexApi from '@/api/main/index.js';
-  import { dateReg } from 'ele-admin/lib/utils/validate';
-  import { factorial } from 'mathjs';
-
-  export default {
-    data() {
-      return {
-        timeR:null,
-        list: [
-          {
-            title: '库存总量',
-            key: 'totalInventory',
-            count: '',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-          {
-            title: '库存周转天数',
-            unit: '(本月)',
-            key: 'materialTurnoverDays',
-            count: '',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-          // {
-          //   title: '物料库存周转率',
-          //   unit: '(本月)',
-          //   count: '',
-          //   key: 'materialTurnoverRate',
-          //   icon: require('../../../assets/index/Vector@2x.png')
-          // },
-          {
-            title: '呆滞品总量',
-            // unit: '(单位)',
-            count: '',
-            key: 'totalSlowMovingItems',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-          {
-            title: '产成品总量',
-            // unit: '(单位)',
-            count: '',
-            key: 'totalFinishedProducts',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-          {
-            title: '原材料总量',
-            // unit: '(单位)',
-            count: '',
-            key: 'totalRawMaterials',
-            icon: require('../../../assets/index/Vector@2x.png')
-          }
-        ]
-      };
-    },
-    mounted() {
-      // this.initCharts1();
-      // this.initCharts2();
-
-      this.init();
-      this.timeR = setInterval(() => {
-        this.init();
-      },86400000);
-    },
-    beforeDestroy(){
-      clearInterval(this.timeR)
-    },
-    methods: {
-      async init() {
-        let data = await indexApi.indexQuery();
-        let barData = await indexApi.queryList();
-
-        this.list.forEach((item) => {
-          item.count = data[item.key];
-        });
-
-        this.initCharts3(data.slowMovingItemsAnalysis);
-        this.initCircle1(data);
-        this.initCharts1(barData);
-        this.initCharts2(barData);
-      },
-
-      initCharts3(data) {
-        var myChart = echarts.init(document.querySelector('.erchats4'));
-        let x = data.map((item) => item.name);
-        let y = data.map((item) => item.num);
-        let option = {
-          tooltip: {
-            trigger: 'axis',
-            axisPointer: {
-              type: 'shadow'
-            }
-          },
-          grid: {
-            top: '10%',
-            left: '3%',
-            right: '4%',
-            bottom: '3%',
-            containLabel: true
-          },
-          xAxis: [
-            {
-              type: 'category',
-              data: x,
-              axisTick: {
-                alignWithLabel: true
-              }
-            }
-          ],
-          yAxis: [
-            {
-              type: 'value'
-            }
-          ],
-          series: [
-            {
-              name: '个',
-              type: 'bar',
-              barWidth: '30%',
-              itemStyle: {
-                color: '#20BE4B'
-              },
-              data: y
-            }
-          ]
-        };
-        myChart.setOption(option);
-      },
-      initCircle1(data) {
-        var myChart = echarts.init(document.querySelector('.erchats3'));
-        let arr = [
-          {
-            value: data.inspectedQuantity,
-            name: '已质检',
-            rate: data.inspectedPercentage
-          },
-          {
-            value: data.pendingInspectionQuantity,
-            name: '未质检',
-            rate: data.pendingInspectionPercentage
-          },
-          {
-            value: data.noInspectionRequiredQuantity,
-            name: '无需质检',
-            rate: data.noInspectionRequiredPercentage
-          }
-        ];
-        let option = {
-          tooltip: {
-            trigger: 'item'
-          },
-          graphic: {
-            //图形中间图片
-            elements: [
-              {
-                type: 'text', //通过不同top值可以设置上下显示
-                left: '25%',
-                top: '44%',
-                style: {
-                  text: '在库总量(台)',
-                  fill: '#000', //文字的颜色
-                  width: 30,
-                  height: 30,
-                  fontSize: 12,
-                  color: '#000',
-                  fontFamily: 'Microsoft YaHei'
-                }
-              },
-              {
-                type: 'text', //通过不同top值可以设置上下显示
-                left: '25%',
-                top: '53%',
-                style: {
-                  text: data.totalInventory,
-                  fill: '#000', //文字的颜色
-                  width: 30,
-                  height: 30,
-                  fontSize: 18,
-                  color: '#000',
-                  fontFamily: 'Microsoft YaHei'
-                }
-              }
-            ]
-          },
-          legend: {
-            top: 'center',
-            right: '6%',
-            // left:'right',
-            type: 'scroll',
-            orient: 'vertical',
-            itemGap: 15,
-            textStyle: {
-              rich: {
-                // 通过富文本rich给每个项设置样式,下面的oneone、twotwo、threethree可以理解为"每一列"的样式
-                oneone: {
-                  // 设置文字、数学、英语这一列的样式
-                  width: 80,
-                  color: '#000',
-                  fontSize: 12,
-                  fontWeight: 'bolder'
-                },
-                twotwo: {
-                  // 设置10分、20分、30分这一列的样式
-                  width: 35,
-                  color: '#000',
-                  fontSize: 12
-                },
-                threethree: {
-                  // 设置百分比这一列的样式
-                  width: 20,
-                  color: '#000',
-                  fontSize: 12
-                }
-              }
-            },
-            formatter: (name) => {
-              let data = arr.find((item) => item.name == name);
-              return `{oneone|${name}}{twotwo|${data?.value}}           {threethree|(${data?.rate}%)}`;
-            }
-          },
-          series: [
-            {
-              type: 'pie',
-              radius: ['60%', '75%'],
-              center: ['30%', '50%'], //图的位置,距离左跟上的位置
-              avoidLabelOverlap: false,
-              padAngle: 5,
-              label: {
-                show: false,
-                position: 'center'
-              },
-              emphasis: {
-                label: {
-                  show: false,
-                  fontSize: 40,
-                  fontWeight: 'bold'
-                }
-              },
-              labelLine: {
-                show: false
-              },
-              data: arr
-            }
-          ]
-        };
-        myChart.setOption(option);
-      },
-
-      initCharts1(barData) {
-        const colors = ['#5470C6', '#91CC75', '#EE6666'];
-        console.log(barData, 'barData');
-        var myChart = echarts.init(document.querySelector('.erchats1'));
-        myChart.setOption({
-          color: colors,
-          tooltip: {
-            trigger: 'axis',
-            axisPointer: {
-              type: 'cross'
-            }
-          },
-          grid: {
-            left: '5%',
-            right: '5%',
-            bottom: '8%'
-          },
-          legend: {},
-          xAxis: [
-            {
-              type: 'category',
-              axisTick: {
-                alignWithLabel: true
-              },
-              data: barData.map((item) => item.month)
-            }
-          ],
-          yAxis: [
-            {
-              type: 'value',
-              name: '',
-              position: 'right',
-              alignTicks: true,
-              axisLine: {
-                show: false
-              },
-              axisLabel: {
-                formatter: '{value}%'
-              }
-            },
-            // {
-            //   type: 'value',
-            //   name: '2024',
-            //   show: false,
-            //   position: 'right',
-            //   alignTicks: true,
-            //   offset: 80,
-            //   axisLine: {
-            //     show: false
-            //   },
-            //   axisLabel: {
-            //     formatter: '{value} ml'
-            //   }
-            // },
-            {
-              type: 'value',
-              name: '(单位:pcs)',
-              position: 'left',
-              alignTicks: true,
-              axisLine: {
-                show: false
-              },
-              axisLabel: {
-                formatter: '{value}'
-              }
-            }
-          ],
-          series: [
-            {
-              name: '去年同期库存总量',
-              type: 'bar',
-              barWidth: '20%',
-              itemStyle: {
-                color: '#3976F1'
-              },
-              yAxisIndex: 1,
-              data: barData.map((item) => item.lastYearTotalInventory)
-            },
-            {
-              name: '本年库存总量',
-              type: 'bar',
-              yAxisIndex: 1,
-              barWidth: '20%',
-              itemStyle: {
-                color: '#5DD07C'
-              },
-              data: barData.map((item) => item.totalInventory)
-            },
-            {
-              name: '趋势',
-              type: 'line',
-              itemStyle: {
-                color: '#FF9669'
-              },
-              data: barData.map((item) => item.inventoryTrendRate)
-            }
-          ]
-        });
-      },
-      initCharts2(barData) {
-        var myChart = echarts.init(document.querySelector('.erchats2'));
-        myChart.setOption({
-          tooltip: {
-            trigger: 'axis',
-            axisPointer: {
-              type: 'cross'
-            }
-          },
-          grid: {
-            left: '5%',
-            right: '5%',
-            bottom: '8%'
-          },
-          legend: {
-            // data: ['当年存货周转率', '去年存货周转率', '当年趋势']
-          },
-          xAxis: [
-            {
-              type: 'category',
-              axisTick: {
-                alignWithLabel: true
-              },
-              data: barData.map((item) => item.month)
-            }
-          ],
-          yAxis: [
-            {
-              type: 'value',
-              name: '',
-              position: 'right',
-              alignTicks: true,
-              axisLine: {
-                show: false
-              },
-              axisLabel: {
-                formatter: '{value}%'
-              }
-            },
-            // {
-            //   type: 'value',
-            //   name: '2024',
-            //   show: false,
-            //   position: 'right',
-            //   alignTicks: true,
-            //   offset: 80,
-            //   axisLine: {
-            //     show: false
-            //   },
-            //   axisLabel: {
-            //     formatter: '{value} ml'
-            //   }
-            // },
-            {
-              type: 'value',
-              name: '(单位:pcs)',
-              position: 'left',
-              alignTicks: true,
-              axisLine: {
-                show: false
-              },
-              axisLabel: {
-                formatter: '{value}'
-              }
-            }
-          ],
-          series: [
-            {
-              name: '本年存货周转率',
-              type: 'bar',
-              barWidth: '20%',
-              itemStyle: {
-                color: '#FF9669'
-              },
-              yAxisIndex: 1,
-              data: barData.map((item) => item.materialTurnoverRate)
-            },
-            {
-              name: '去年存货周转率',
-              type: 'bar',
-              yAxisIndex: 1,
-              barWidth: '20%',
-              yAxisIndex: 1,
-              itemStyle: {
-                color: '#26ADAD'
-              },
-              data: barData.map((item) => item.lastYearMaterialTurnoverRate)
-            },
-            {
-              name: '趋势',
-              type: 'line',
-              itemStyle: {
-                color: '#095BFF'
-              },
-              data: barData.map((item) => item.materialTurnoverTrend)
-            }
-          ]
-        });
-      }
-    }
-  };
+import WarehouseDashboard from './dashboard.vue';
+export default { name: 'WmsCommonHome', components: { WarehouseDashboard } };
 </script>
-
-<style lang="scss" scoped>
-  #my_index {
-    height: calc(100vh - 96px);
-    display: flex;
-    width: 100%;
-    padding: 10px;
-    flex-direction: column;
-    box-sizing: border-box;
-    background-color: #dbdbdb;
-    .viewBar {
-      height: 93px;
-      width: 100%;
-      display: flex;
-      margin-bottom: 10px;
-      > div {
-        flex: 1;
-        margin-left: 10px;
-        background: linear-gradient(to right, #2d80ee 100%, #e9f2ff 50%);
-        border-radius: 8px;
-        display: flex;
-        padding: 0 20px;
-        box-sizing: border-box;
-        .item {
-          flex: 1;
-          display: flex;
-          flex-direction: column;
-          .title {
-            flex: 1;
-            display: flex;
-            align-items: center;
-            font-size: 16px;
-            color: rgba(255, 255, 255, 1);
-            > span {
-              font-size: 12px;
-            }
-          }
-          .count {
-            flex: 1;
-            display: flex;
-            align-items: center;
-            font-size: 28px;
-            color: rgba(255, 255, 255, 1);
-          }
-        }
-        .icon {
-          width: 50px;
-          height: 100%;
-          display: flex;
-          align-items: center;
-          justify-content: center;
-          > img {
-            height: 50px;
-            width: 50px;
-          }
-        }
-      }
-      > div:first-child {
-        margin-left: 0;
-      }
-    }
-    .echart_box {
-      display: flex;
-      flex: 1;
-      .left {
-        flex: 5;
-        display: flex;
-        flex-direction: column;
-        margin-right: 10px;
-        height: 100%;
-        > div:first-child {
-          margin-bottom: 10px;
-        }
-        > div {
-          flex: 1;
-          background-color: #fff;
-          padding: 10px;
-          box-sizing: border-box;
-          border-radius: 6px;
-          display: flex;
-          flex-direction: column;
-          .title_box {
-            font-size: 14px;
-            padding-bottom: 10px;
-            box-sizing: border-box;
-            border-bottom: 1px solid #e6e8e8;
-          }
-          > div:last-child {
-            flex: 1;
-          }
-        }
-      }
-      .right {
-        flex: 3;
-        display: flex;
-        flex-direction: column;
-        height: 100%;
-        > div {
-          flex: 1;
-          padding: 10px;
-          box-sizing: border-box;
-          background-color: #fff;
-          margin-bottom: 10px;
-          border-radius: 6px;
-          display: flex;
-          flex-direction: column;
-          .title_box {
-            font-size: 14px;
-            padding-bottom: 10px;
-            box-sizing: border-box;
-            border-bottom: 1px solid #e6e8e8;
-          }
-          > div:last-child {
-            flex: 1;
-          }
-        }
-        > div:last-child {
-          margin-bottom: 0;
-        }
-      }
-    }
-  }
-</style>

+ 132 - 0
src/views/warehouseManagement/index/components/DashboardActions.vue

@@ -0,0 +1,132 @@
+<template>
+  <div>
+    <el-dialog class="wms-dashboard-dialog" :visible.sync="visible" :title="title" width="760px" append-to-body :close-on-click-modal="false" :before-close="close">
+      <div v-loading="loading">
+        <template v-if="kind === 'results'">
+          <p v-if="resultsNote" class="dashboard-note">{{ resultsNote }}</p>
+          <el-table :data="results"><el-table-column prop="key" label="单据 / 任务" /><el-table-column label="结果" width="100"><template slot-scope="{ row }"><span :class="row.success ? 'dashboard-success' : 'dashboard-error'">{{ row.success ? '成功' : '失败' }}</span></template></el-table-column><el-table-column prop="message" label="说明" /><el-table-column label="关联单据"><template slot-scope="{ row }">{{ (row.documentIds || []).join('、') }}</template></el-table-column></el-table>
+        </template>
+        <template v-else-if="kind === 'disposal-list'">
+          <p class="dashboard-note">处置审批不占用库存;执行后生成调拨或出库单据,财务确认单独登记。</p>
+          <el-table :data="records"><el-table-column prop="code" label="处置单号" min-width="150" /><el-table-column prop="categoryName" label="物料" /><el-table-column prop="quantity" label="数量" /><el-table-column label="状态"><template slot-scope="{ row }">{{ approvalLabels[row.approvalStatus] || '待核实' }} / {{ row.executionStatus === 1 ? '已执行' : '未执行' }}</template></el-table-column><el-table-column label="操作" width="200"><template slot-scope="{ row }"><el-link v-if="[0,3,4].includes(row.approvalStatus)" type="primary" @click="transition(row, 'submit')">提交审批</el-link> <el-link v-if="row.approvalStatus === 2 && row.executionStatus !== 1" type="primary" @click="transition(row, 'execute')">执行</el-link> <el-link v-if="row.documentId" type="primary" @click="$emit('business', 'disposal-document', row)">关联单据</el-link> <el-link v-if="row.executionStatus === 1" type="primary" @click="open('finance', row)">{{ row.financeStatus === 1 ? '财务记录' : '财务登记' }}</el-link></template></el-table-column></el-table>
+          <el-pagination :current-page.sync="page" :page-size="20" :total="total" layout="total, prev, pager, next" @current-change="loadDisposals" />
+        </template>
+        <el-form v-else label-width="110px" @submit.native.prevent>
+          <template v-if="kind === 'purchase' || kind === 'threshold'">
+            <el-form-item label="仓库" required><el-select popper-class="wms-dashboard-popper" v-model="form.warehouseId" filterable @change="scopeChanged"><el-option v-for="item in options.warehouses || []" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
+            <el-form-item label="物料" required><el-select popper-class="wms-dashboard-popper" v-model="form.categoryId" filterable @change="scopeChanged"><el-option v-for="item in options.categories || []" :key="item.id" :label="(item.code ? item.code + ' ' : '') + item.name" :value="item.id" /></el-select></el-form-item>
+            <el-form-item label="基本单位">{{ form.unit || '请选择物料' }}</el-form-item>
+          </template>
+          <template v-if="kind === 'threshold'">
+            <el-form-item label="最低库存" required><el-input-number v-model="form.minInventory" :min="0" :precision="4" /></el-form-item><el-form-item label="安全库存" required><el-input-number v-model="form.secureInventory" :min="0" :precision="4" /></el-form-item><el-form-item label="最高库存" required><el-input-number v-model="form.maxInventory" :min="0" :precision="4" /></el-form-item>
+            <p class="dashboard-note">最低库存 ≤ 安全库存 ≤ 最高库存;保存后按仓库与物料基本单位生效。</p>
+          </template>
+          <template v-if="kind === 'purchase'">
+            <el-form-item label="需求名称" required><el-input v-model.trim="form.requirementName" maxlength="100" /></el-form-item>
+            <el-form-item label="需求部门" required><el-select popper-class="wms-dashboard-popper" v-model="form.requireDeptId" filterable><el-option v-for="item in departments" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
+            <el-form-item label="需求类型" required><el-select popper-class="wms-dashboard-popper" v-model="form.sourceType"><el-option v-for="item in sources" :key="item.code" :label="item.label" :value="item.code" /></el-select></el-form-item>
+            <el-form-item label="采购数量" required><el-input-number v-model="form.quantity" :min="0.0001" :precision="4" /></el-form-item><el-form-item label="用途" required><el-input v-model.trim="form.useTo" maxlength="500" /></el-form-item>
+            <p v-if="savedPurchase" class="dashboard-note">需求 {{ savedPurchase.code }} {{ savedPurchase.reused ? '已存在' : '已保存为草稿' }},请进入采购需求页核对并提交审批。<el-link type="primary" @click="$emit('business', 'purchase-document', savedPurchase)">打开采购需求</el-link></p>
+          </template>
+          <template v-if="kind === 'disposal'">
+            <el-form-item label="物料 / 批次">{{ source.categoryName }} / {{ source.systemBatchNo || source.batchNo || '日期待核实' }}</el-form-item><el-form-item label="处置数量" required><el-input-number v-model="form.quantity" :min="0.0001" :max="Number(source.availableQuantity || source.quantity)" :precision="4" /> {{ source.unit }}</el-form-item>
+            <el-form-item label="处置方式" required><el-radio-group v-model="form.method"><el-radio label="TRANSFER">调拨</el-radio><el-radio label="SCRAP">报废出库</el-radio></el-radio-group></el-form-item><el-form-item v-if="form.method === 'TRANSFER'" label="目标仓库" required><el-select popper-class="wms-dashboard-popper" v-model="form.targetWarehouseId" filterable><el-option v-for="item in options.warehouses || []" :key="item.id" :label="item.name" :value="item.id" :disabled="String(item.id) === String(source.warehouseId)" /></el-select></el-form-item><el-form-item label="处置原因" required><el-input v-model.trim="form.reason" type="textarea" :rows="3" maxlength="1000" /></el-form-item>
+          </template>
+          <template v-if="kind === 'finance'">
+            <el-form-item label="处置单号">{{ source.code }}</el-form-item><el-form-item label="关联单据">{{ source.documentCode || source.documentId }}</el-form-item><el-form-item label="财务确认"><el-checkbox v-model="form.confirmed" :disabled="source.financeStatus === 1">已核对处置单及外部凭证</el-checkbox></el-form-item><el-form-item label="确认金额" required><el-input-number v-model="form.amount" :min="0" :precision="2" :disabled="source.financeStatus === 1" /></el-form-item><el-form-item label="外部凭证号" required><el-input v-model.trim="form.voucherNo" :disabled="source.financeStatus === 1" maxlength="100" /></el-form-item><el-form-item label="附件"><file-upload v-if="source.financeStatus !== 1" v-model="form.files" module="wms" /><span v-else>{{ (source.files || []).map(item => item.name || item.fileName).join('、') || '无附件' }}</span></el-form-item>
+          </template>
+        </el-form>
+      </div>
+      <span slot="footer"><el-button :disabled="loading" @click="close">关闭</el-button><el-button v-if="!['results','disposal-list'].includes(kind) && !(kind === 'finance' && source.financeStatus === 1) && !savedPurchase" type="primary" :loading="loading" @click="submit">{{ kind === 'purchase' ? '保存采购需求草稿' : kind === 'disposal' ? '保存处置草稿' : '保存' }}</el-button></span>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { dashboardAction, disposalList, disposalDetail, alertConfig, saveAlertConfig } from '@/api/warehouseManagement/dashboard';
+import warehouseDefinition from '@/api/warehouseManagement/warehouseDefinition';
+import { getByCode } from '@/api/system/dictionary-data';
+import FileUpload from '@/components/upload/fileUpload.vue';
+
+export default {
+  name: 'DashboardActions', components: { FileUpload },
+  props: { options: { type: Object, default: () => ({}) }, filters: { type: Object, default: () => ({}) } },
+  data() { return { visible:false,kind:'',loading:false,source:{},form:{},savedPurchase:null,departments:[],sources:[],records:[],total:0,page:1,results:[],resultsNote:'',configSequence:0,approvalLabels:{ 0:'草稿',1:'审批中',2:'已通过',3:'已驳回',4:'已撤销' } }; },
+  computed: { title() { return { purchase:'安全库存补货',threshold:'安全库存配置',disposal:'呆滞品处置申请','disposal-list':'处置申请与财务登记',finance:'财务登记',results:'逐单处理结果' }[this.kind]; } },
+  methods: {
+    async open(kind, row = {}) {
+      if (this.loading) return;
+      this.kind = kind; this.source = row; this.savedPurchase = null; this.visible = true; this.configSequence++;
+      this.form = { warehouseId:row.warehouseId || this.filters.warehouseId || '',categoryId:row.categoryId || this.filters.categoryId || '',unit:row.unit || '',quantity:row.gapQuantity > 0 ? row.gapQuantity : undefined,requirementName:'安全库存补货',useTo:'安全库存补货',sourceType:'1',method:'TRANSFER',reason:'',requestKey:'wms-disposal-' + Date.now() + '-' + Math.random().toString(36).slice(2) };
+      try {
+        if (kind === 'disposal-list') { this.page = 1; await this.loadDisposals(); }
+        if (kind === 'threshold') await this.scopeChanged();
+        if (kind === 'purchase') {
+          this.loading = true;
+          const [departments, dict] = await Promise.all([warehouseDefinition.tree(),getByCode('requirement_source_type')]);
+          this.departments = departments || []; this.sources = Object.values(dict.data || {}).map((item) => ({ code:Object.keys(item)[0],label:Object.values(item)[0] }));
+          const info = this.$store.state.user.info || {}; const ids = info.deptIds || info.deptId || [];
+          this.$set(this.form,'requireDeptId',Array.isArray(ids) ? ids[ids.length - 1] : ids); this.syncUnit();
+        }
+        if (kind === 'finance') {
+          this.loading = true; this.source = await disposalDetail(row.id);
+          this.form = { confirmed:this.source.financeStatus === 1,amount:this.source.financeAmount == null ? undefined : Number(this.source.financeAmount),voucherNo:this.source.voucherNo || '',files:this.source.files || [] };
+        }
+      } catch (e) { this.$message.error(e.message || '加载失败,请重试'); }
+      finally { this.loading = false; }
+    },
+    close(done) { if (this.loading) return; this.visible = false; this.configSequence++; if (typeof done === 'function') done(); },
+    syncUnit() { const item = (this.options.categories || []).find((c) => String(c.id) === String(this.form.categoryId)); this.$set(this.form,'unit',item ? item.unit : this.source.unit || ''); return item; },
+    async scopeChanged() {
+      this.syncUnit(); if (this.kind !== 'threshold') return;
+      ['minInventory','secureInventory','maxInventory'].forEach((key) => this.$set(this.form,key,undefined));
+      const sequence = ++this.configSequence;
+      if (!this.form.warehouseId || !this.form.categoryId) return;
+      this.loading = true;
+      try {
+        const data = await alertConfig({ warehouseId:this.form.warehouseId,categoryId:this.form.categoryId });
+        if (sequence === this.configSequence && data) {
+          if (data.unit !== this.form.unit) this.$message.warning('历史阈值单位与当前物料基本单位不一致,请核对后保存');
+          ['minInventory','secureInventory','maxInventory'].forEach((key) => { const value = data[key]; this.$set(this.form,key,value != null && value !== '' && Number.isFinite(Number(value)) ? Number(value) : undefined); });
+        }
+      }
+      catch (e) { if (sequence === this.configSequence) this.$message.error(e.message || '配置读取失败,请重试'); }
+      finally { if (sequence === this.configSequence) this.loading = false; }
+    },
+    async loadDisposals() {
+      this.loading = true;
+      try { const data = await disposalList({ month:(this.filters.startTime || '').slice(0,7),warehouseId:this.filters.warehouseId || undefined,pageNum:this.page,pageSize:20 }); this.records = data.list || []; this.total = Number(data.total || 0); }
+      catch (e) { this.$message.error(e.message || '处置列表加载失败'); }
+      finally { this.loading = false; }
+    },
+    async transition(row, action) {
+      if (this.loading) return;
+      try { await this.$confirm(action === 'execute' ? '确认执行处置并生成调拨或出库单据?' : '确认提交处置审批?','处置确认'); this.loading = true; await dashboardAction('disposal/' + row.id + '/' + action,{}); this.$message.success(action === 'execute' ? '处置已执行' : '已提交审批'); await this.loadDisposals(); this.$emit('success'); }
+      catch (e) { if (e !== 'cancel' && e !== 'close') this.$message.error(e.message || '操作失败,请重试'); }
+      finally { this.loading = false; }
+    },
+    async submit() {
+      if (this.loading) return; const f = this.form;
+      if (['purchase','threshold'].includes(this.kind) && (!f.warehouseId || !f.categoryId || !f.unit)) return this.$message.warning('请选择仓库及具有基本单位的物料');
+      if (this.kind === 'threshold' && ![f.minInventory,f.secureInventory,f.maxInventory].every((v) => typeof v === 'number' && Number.isFinite(v) && v >= 0)) return this.$message.warning('请完整填写非负库存阈值');
+      if (this.kind === 'threshold' && (f.minInventory > f.secureInventory || f.secureInventory > f.maxInventory)) return this.$message.warning('阈值需满足最低库存 ≤ 安全库存 ≤ 最高库存');
+      if (this.kind === 'purchase' && (!f.requirementName || !f.requireDeptId || !f.sourceType || !(f.quantity > 0) || !f.useTo)) return this.$message.warning('请完整填写采购需求');
+      if (this.kind === 'disposal' && (!(f.quantity > 0) || !f.reason || (f.method === 'TRANSFER' && !f.targetWarehouseId))) return this.$message.warning('请填写处置数量、方式、原因及目标仓库');
+      if (this.kind === 'finance' && (!f.confirmed || !(f.amount >= 0) || !f.voucherNo)) return this.$message.warning('请完成财务确认、金额及凭证号');
+      this.loading = true;
+      try {
+        if (this.kind === 'threshold') await saveAlertConfig({ warehouseId:f.warehouseId,categoryId:f.categoryId,unit:f.unit,minInventory:f.minInventory,secureInventory:f.secureInventory,maxInventory:f.maxInventory });
+        if (this.kind === 'purchase') {
+          const category = this.syncUnit(); const dept = this.departments.find((d) => String(d.id) === String(f.requireDeptId));
+          this.savedPurchase = await dashboardAction('purchase',{ warehouseId:f.warehouseId,categoryId:f.categoryId,requirement:{ requirementName:f.requirementName,sourceType:f.sourceType,requireDeptId:f.requireDeptId,requireDeptName:dept && dept.name,useTo:f.useTo,detailList:[{ productId:f.categoryId,productCode:category && category.code || this.source.categoryCode,productName:category && category.name || this.source.categoryName,totalCount:f.quantity,measuringUnit:f.unit }] } });
+        }
+        if (this.kind === 'disposal') await dashboardAction('disposal',{ requestKey:f.requestKey,stockBatchId:this.source.stockBatchId || this.source.id,quantity:f.quantity,method:f.method,targetWarehouseId:f.method === 'TRANSFER' ? f.targetWarehouseId : undefined,reason:f.reason });
+        if (this.kind === 'finance') await dashboardAction('disposal/' + this.source.id + '/finance',f);
+        this.$message.success(this.kind === 'disposal' ? '处置草稿已保存,请在处置申请中提交审批' : this.kind === 'purchase' ? '采购需求已保存,请核对并提交审批' : '保存成功');
+        if (this.kind !== 'purchase') this.visible = false; this.$emit('success');
+      } catch (e) { this.$message.error(e.message || '保存失败,请重试'); }
+      finally { this.loading = false; }
+    },
+    showResults(results, note = '') { this.results = results || []; this.resultsNote = note; this.kind = 'results'; this.visible = true; }
+  }
+};
+</script>

+ 46 - 0
src/views/warehouseManagement/index/components/DashboardChart.vue

@@ -0,0 +1,46 @@
+<template><div ref="chart" class="dashboard-chart" role="img" :aria-label="chartLabel"></div></template>
+<script>
+import * as echarts from 'echarts';
+export default {
+  name: 'WarehouseDashboardChart',
+  props: { kind: String, rows: { type: Array, default: () => [] }, unit: { type: String, default: '' } },
+  computed: { chartLabel() { return { trend: '近三十天出入库趋势', category: '库存分类分布', aging: '批次库龄结构' }[this.kind]; } },
+  watch: { rows: { deep: true, handler() { this.render(); } }, unit() { this.render(); } },
+  mounted() {
+    this.chart = echarts.init(this.$refs.chart);
+    this.chart.on('click', (event) => { if (this.rows[event.dataIndex]) this.$emit('select', this.rows[event.dataIndex]); });
+    this.resize = () => this.chart && this.chart.resize();
+    window.addEventListener('resize', this.resize);
+    if (window.ResizeObserver) { this.observer = new ResizeObserver(this.resize); this.observer.observe(this.$refs.chart); }
+    this.render();
+  },
+  activated() { this.$nextTick(() => this.resize && this.resize()); },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resize);
+    if (this.observer) this.observer.disconnect();
+    if (this.chart) this.chart.dispose();
+    this.chart = null;
+  },
+  methods: {
+    render() {
+      if (!this.chart) return;
+      const color = ['#38C8F5', '#F0A03A', '#9B7DF7', '#3BD48A', '#F25E5E', '#7E93AD', '#2E8BE8', '#D9C96C'];
+      const axis = { axisLine: { lineStyle: { color: '#345573' } }, axisLabel: { color: '#9FB8D4', fontSize: 11 }, splitLine: { lineStyle: { color: 'rgba(80,150,220,.12)' } } };
+      let option;
+      if (this.kind === 'trend') {
+        option = { color, tooltip: { trigger: 'axis' }, grid: { top: 22, right: 20, bottom: 28, left: 55 }, xAxis: { ...axis, type: 'category', data: this.rows.map((r) => r.date) }, yAxis: { ...axis, type: 'value', name: this.unit, nameTextStyle: { color: '#9FB8D4' } }, series: [
+          { name: '入库量', type: 'bar', data: this.rows.map((r) => r.inbound), barMaxWidth: 10, itemStyle:{ color:new echarts.graphic.LinearGradient(0,0,0,1,[{ offset:0,color:'#38C8F5' },{ offset:1,color:'rgba(46,139,232,.25)' }]) } },
+          { name: '出库量', type: 'bar', data: this.rows.map((r) => r.outbound), barMaxWidth: 10 },
+          { name: '净趋势', type: 'line', smooth: true, symbol: 'none', lineStyle:{ type:'dashed' }, data: this.rows.map((r) => r.net) }
+        ] };
+      } else if (this.kind === 'category') {
+        option = { color, tooltip: { trigger: 'item' }, legend: { type: 'scroll', orient: 'vertical', right: 0, top: 'center', textStyle: { color: '#9FB8D4', fontSize: 11 } }, series: [{ type: 'pie', radius: ['48%', '72%'], center: ['35%', '50%'], label: { show: false }, itemStyle: { borderColor: '#0A1830', borderWidth: 3 }, data: this.rows.map((r) => ({ name: r.name, value: r.quantity })) }] };
+      } else {
+        option = { color, tooltip: { trigger: 'axis' }, grid: { top: 12, right: 28, bottom: 25, left: 100 }, xAxis: { ...axis, type: 'value' }, yAxis: { ...axis, type: 'category', inverse: true, data: this.rows.map((r) => r.label || r.name) }, series: [{ type: 'bar', barMaxWidth: 18, data: this.rows.map((r, i) => ({ value: r.quantity, itemStyle: { color: color[i % color.length], borderRadius: [0, 4, 4, 0] } })) }] };
+      }
+      this.chart.setOption(option, true);
+    }
+  }
+};
+</script>
+<style scoped>.dashboard-chart { width:100%; height:220px; }</style>

+ 89 - 0
src/views/warehouseManagement/index/components/HomeOverview.vue

@@ -0,0 +1,89 @@
+<template>
+<div class="view active">
+          <div class="screen-band">
+            <div class="en">Warehouse Data Center</div>
+            <h1>仓储管理总览</h1>
+            <div class="meta"><span>统计月份:<b>{{ monthLabel }}</b></span><span>服务端查询时间:<b>{{ summary.capturedAt || '尚未更新' }}</b></span></div>
+          </div>
+          <div class="greet"><div><h2>{{ greeting }}{{ userName ? ',' + userName : '' }}</h2><p>今天是 {{ todayLabel }} · 数据更新于 {{ updatedAt || '尚未更新' }}(统计每 15 分钟、预警每 5 分钟、动态每 30 秒刷新)</p></div></div>
+          <div v-if="error" class="data-note bad" role="alert">{{ error }} <button type="button" @click="$emit('refresh')">重试</button></div>
+          <div class="data-note">估值覆盖率 {{ display(summary.valuationCoverage) }}% · 库龄来源覆盖率 {{ display(summary.ageCoverage) }}% · {{ summary.comparisonReason || '日末采集历史待核实' }}。批次估值按可核实入库价计算。</div>
+          <div class="todo-bar">
+            <div v-for="(item, i) in todoCards" :key="item.key" class="todo-item" role="button" tabindex="0" @click="$emit('open', item.target, item.filters || {})" @keydown.enter="$emit('open', item.target, item.filters || {})">
+              <div class="todo-icon" :class="'icon-' + item.icon"><span>{{ item.symbol }}</span></div>
+              <div><div class="todo-num">{{ display(item.value) }} <span v-if="i < 2" class="badge-today">本月</span><span v-if="item.overdue" class="badge-overdue">{{ item.overdue }} 单超时</span></div><div class="todo-label">{{ item.label }}</div></div>
+              <span class="todo-arrow">›</span>
+            </div>
+          </div>
+          <div class="kpi-row">
+            <div v-for="(item, i) in kpis" :key="item.key" class="card kpi corner" role="button" tabindex="0" @click="$emit('open', item.target, item.filters || {})" @keydown.enter="$emit('open', item.target, item.filters || {})">
+              <span class="idx">{{ ('0' + (i + 1)).slice(-2) }}</span><span class="kpi-go">查看明细 ›</span>
+              <div class="lb">{{ item.label }} <span class="q" :title="item.info">?</span></div>
+              <div class="val" :style="{ color: item.color }">{{ display(item.value) }}<small v-if="item.value != null">{{ item.unit }}</small></div>
+              <div class="delta dim" :title="item.reason">{{ item.reason || '数据待核实' }}</div>
+              <div v-if="item.progress != null" class="bar-mini"><i :style="{ width: item.progress + '%', background: item.color, color: item.color }"></i></div>
+            </div>
+          </div>
+          <div class="grid-2-1">
+            <section class="card corner"><div class="card-hd"><div><h3>出入库动态</h3><div class="sub">近 30 天滚动 · 净趋势=当日入库−当日出库</div></div><span class="link-group"><a class="link" href="#" @click.prevent="$emit('open', 'stockManagement')">进入入库管理 ›</a><a class="link" href="#" @click.prevent="$emit('open', 'outgoingManagement')">进入出库管理 ›</a></span></div><div class="chart-body"><div class="legend"><span><i style="background:#38C8F5"></i>入库量</span><span><i style="background:#F0A03A"></i>出库量</span><span><i style="background:#9B7DF7;height:2px"></i>净趋势</span></div><dashboard-chart v-if="trend.length" kind="trend" :rows="trend" :unit="unit" @select="trendSelect" /><div v-else class="empty-chart">{{ trendReason || '暂无已完成出入库流水' }}</div></div></section>
+            <section class="card corner"><div class="card-hd"><div><h3>库存分类分布</h3><div class="sub">按物料分类 TOP8(前 7 + 其他兜底)· 按基本单位</div></div><a class="link" href="#" @click.prevent="$emit('open', 'ledger')">查看台账 ›</a></div><div class="chart-body"><dashboard-chart v-if="categories.length" kind="category" :rows="categories" :unit="unit" @select="categorySelect" /><div v-else class="empty-chart">{{ categoryReason || '当前筛选无可追溯库存' }}</div></div></section>
+          </div>
+          <div class="grid-3">
+            <section class="card corner"><div class="card-hd"><div><h3>库龄结构</h3><div class="sub">在库批次按库龄分段 · 与呆滞口径联动</div></div><a class="link" href="#" @click.prevent="$emit('open', 'age')">明细 ›</a></div><div class="chart-body"><dashboard-chart v-if="aging.length" kind="aging" :rows="aging" :unit="unit" @select="ageSelect" /><div v-else class="empty-panel">{{ agingReason || '日期待核实,暂无完整库龄结构' }}</div></div></section>
+            <section class="card corner"><div class="card-hd"><div><h3>安全库存预警</h3><div class="sub">低于安全库存 / 超储 · 准实时(≤5 分钟)</div></div><a class="link" href="#" @click.prevent="$emit('open', 'alerts')">全部预警 ›</a></div><div v-if="alerts.length"><div v-for="item in alerts" :key="item.id" class="alert-item" role="button" tabindex="0" @click="$emit('open', 'alerts', { categoryId: item.categoryId, warehouseId: item.warehouseId })" @keydown.enter="$emit('open', 'alerts', { categoryId: item.categoryId, warehouseId: item.warehouseId })"><span class="dot" :style="{ background: item.color || '#F0A03A', color: item.color || '#F0A03A' }"></span><b>{{ item.name }}</b><span class="meta">{{ item.message }}</span></div></div><div v-else class="empty-panel">{{ alertReason || '暂无可核对的预警数据' }}</div></section>
+            <section class="card corner"><div class="card-hd"><div><h3>最近作业动态</h3><div class="sub">出入库 / 调拨 / 盘点流水</div></div><a class="link" href="#" @click.prevent="$emit('open', 'io')">全部动态 ›</a></div><div v-if="activities.length" class="feed"><div v-for="item in activities" :key="item.id" class="feed-item" role="button" tabindex="0" @click="$emit('business', 'io', item)" @keydown.enter="$emit('business', 'io', item)"><span class="tm">{{ item.time }}</span><span><b>{{ item.title }}</b> · {{ item.description }}</span></div></div><div v-else class="empty-panel">{{ activityReason || '暂无可核对的作业动态' }}</div></section>
+          </div>
+        </div>
+</template>
+<script>
+import DashboardChart from './DashboardChart.vue';
+import { displayNumber } from '../dashboard-utils';
+
+export default {
+  name: 'WarehouseHomeOverview',
+  components: { DashboardChart },
+  props: { summary: { type: Object, default: () => ({}) }, unit: { type: String, default: '' }, updatedAt: { type: String, default: '' }, error: { type: String, default: '' } },
+  computed: {
+    trend() { return this.summary.trend || []; },
+    categories() { return this.summary.categories || []; },
+    aging() { return this.summary.aging || []; },
+    alerts() { return this.summary.alerts || []; },
+    activities() { return this.summary.activities || []; },
+    trendReason() { return this.summary.trendReason || this.summary.unitReason || this.summary.periodReason || ''; },
+    categoryReason() { return this.summary.categoryReason || this.summary.unitReason || ''; },
+    agingReason() { return this.summary.agingReason || this.summary.unitReason || ''; },
+    alertReason() { return this.summary.alertReason || ''; },
+    activityReason() { return this.summary.activityReason || ''; },
+
+    userName() { return (this.$store.state.user.info || {}).nickname || (this.$store.state.user.info || {}).name || ''; },
+    monthLabel() { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit' }).format(new Date()); },
+    todayLabel() { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long' }).format(new Date()); },
+    greeting() { const hour = new Date().getHours(); return hour < 11 ? '上午好' : hour < 18 ? '下午好' : '晚上好'; },
+    todoCards() {
+      return [
+        { key: 'in', icon: 'in', symbol: '↓', label: '待入库单', value: this.summary.pendingInbound, target: 'stockManagement', overdue: this.summary.overdueInbound },
+        { key: 'out', icon: 'out', symbol: '↑', label: '待出库单', value: this.summary.pendingOutbound, target: 'outgoingManagement', overdue: this.summary.overdueOutbound },
+        { key: 'check', icon: 'check', symbol: '✓', label: '盘点任务进行中', value: this.summary.pendingInventory, target: 'todo', overdue: this.summary.overdueInventory },
+        { key: 'approve', icon: 'approve', symbol: '▣', label: '待我审批', value: this.summary.pendingApproval, overdue: this.summary.overdueApproval, target: 'flow' }
+      ];
+    },
+    kpis() {
+      const s = this.summary;
+      return [
+        { key: 'stock', label: '库存总量', value: s.inventoryQuantity, unit: this.unit, reason: s.inventoryReason, info: '当前在库数量,含待检及锁定;不同基本单位分别展示', color: '#38C8F5', target: 'stockLedger' },
+        { key: 'value', label: '库存金额', value: s.inventoryValue, unit: '元', reason: s.valuationReason, info: '批次实际计量数量或重量乘可核实入库单价;未定价不作为零金额', color: '#9B7DF7', target: 'stockLedger' },
+        { key: 'turnover', label: '库存周转天数', value: s.turnoverDays, unit: '天', reason: s.turnoverReason, info: '本月截至昨日的平均库存与实际消耗估算,历史不足时无法计算', color: '#3BD48A', target: 'turnover' },
+        { key: 'stagnant', label: '呆滞品占比', value: s.stagnantRate, unit: '%', reason: s.stagnantReason, info: '首次入库及最近实际消耗均超过 180 天的批次估值除以全部库存估值;估值或日期不完整时无法核实', color: '#F0A03A', progress: s.stagnantRate, target: 'dead' },
+        { key: 'location', label: '库位利用率', value: s.locationUtilization, unit: '%', reason: s.locationReason, info: '已占用的有效库位数量除以启用的普通库位数量', color: '#38C8F5', progress: s.locationUtilization, target: 'locations' },
+        { key: 'quality', label: '质检通过率', value: s.qualityPassRate, unit: '%', reason: s.qualityReason, info: '本月已检批次中合格批次占比,让步计分母但不计合格', color: '#3BD48A', progress: s.qualityPassRate, target: 'qc' }
+      ];
+    }
+  },
+  methods: {
+    display: displayNumber,
+    trendSelect(row) { this.$emit('open', 'io', { startTime: row.date + ' 00:00:00', endTime: row.date + ' 23:59:59' }); },
+    categorySelect(row) { if (row.id === 'other' || !row.id && !row.categoryId) return this.$message.info('其他物料为合并分组,请在库存台账选择具体物料'); this.$emit('open', 'ledger', { categoryId: row.id || row.categoryId }); },
+    ageSelect(row) { this.$emit('open', 'age', { age: row.key }); }
+  }
+};
+</script>

+ 29 - 0
src/views/warehouseManagement/index/dashboard-element.scss

@@ -0,0 +1,29 @@
+/* 深色样式仅覆盖看板内容和专用弹层,保留系统页头、菜单及其下拉框原样。 */
+body.wms-dashboard-theme .ele-admin-content,
+.wms-home,
+.wms-dashboard-dialog,
+.wms-dashboard-popper {
+  &.el-select-dropdown,.el-card,.el-dialog,.el-message-box,.el-select-dropdown,.el-picker-panel,.el-cascader-panel,.el-popover,.el-dropdown-menu,.el-autocomplete-suggestion { background: #0A1A30; color: #EAF4FF; border-color: rgba(70,160,230,.28); }
+  .el-card__header,.el-dialog__header,.el-message-box__header { border-color: rgba(70,160,230,.28); }
+  .el-dialog__title,.el-message-box__title,.el-dialog__body,.el-message-box__content,.el-form-item__label,.el-checkbox__label,.el-radio__label { color: #EAF4FF; }
+  .el-input__inner,.el-textarea__inner,.el-range-input { background: #071528; color: #EAF4FF; border-color: rgba(70,160,230,.35); }
+  .el-input.is-disabled .el-input__inner,.el-textarea.is-disabled .el-textarea__inner { background: #10243A; color: #9FB8D4; }
+  .el-select-dropdown__item,.el-dropdown-menu__item,.el-cascader-node,.el-date-table td { color: #9FB8D4; }
+  .el-select-dropdown__item.hover,.el-select-dropdown__item:hover,.el-dropdown-menu__item:hover,.el-cascader-node:hover,.el-date-table td.available:hover { background: #173655; color: #38C8F5; }
+  .el-table,.el-table__expanded-cell,.el-table tr { background: #0A1A30; color: #EAF4FF; }
+  .el-table th.el-table__cell,.el-table th { background: #102A48; color: #6FA6D8; }
+  .el-table td,.el-table th.is-leaf,.el-table--border,.el-table--group { border-color: rgba(70,160,230,.28); }
+  .el-table--enable-row-hover .el-table__body tr:hover > td,.el-table__body tr.current-row > td { background: #173655; }
+  .el-table::before,.el-table--border::after { background: rgba(70,160,230,.28); }
+  .el-tabs--border-card,.el-tabs--border-card > .el-tabs__header,.el-tabs__nav-wrap::after { background: #0A1A30; border-color: rgba(70,160,230,.28); }
+  .el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active { background: #173655; color: #38C8F5; border-color: rgba(70,160,230,.28); }
+  .el-tabs__item,.el-pagination,.el-pagination__total,.el-pagination__jump,.el-upload-list__item-name { color: #9FB8D4; }
+  .el-pagination button,.el-pager li { background: #102A48; color: #9FB8D4; }
+  .el-pager li.active { color: #38C8F5; }
+  .el-button--default { background: #102A48; color: #EAF4FF; border-color: rgba(70,160,230,.35); }
+  .el-button--primary { background: #2E8BE8; border-color: #38C8F5; }
+  .el-input-number__increase,.el-input-number__decrease { background: #102A48; color: #9FB8D4; border-color: rgba(70,160,230,.28); }
+  .el-loading-mask { background: rgba(6,15,31,.75); }
+  .dashboard-note { color: #9FB8D4; line-height: 1.8; margin: 0 0 14px; }
+  .dashboard-success { color: #3BD48A; } .dashboard-error { color: #F25E5E; }
+}

+ 33 - 0
src/views/warehouseManagement/index/dashboard-layout.scss

@@ -0,0 +1,33 @@
+.wms-home {
+  width: 100%; max-width: 100%; overflow: auto;
+  button:disabled { opacity: .45; cursor: not-allowed; }
+  select option { background: #0A1830; color: #EAF4FF; }
+  .greet .quick-actions { display: flex; gap: 10px; }
+  .link-group { display: flex; gap: 12px; }
+  .data-note,.empty { padding: 18px; color: var(--t2); font-size: 13px; }
+  .data-note.bad,.overdue-row { color: var(--red); }
+  .data-note button { color: var(--cyan); background: none; border: 0; cursor: pointer; }
+  .data-note { border-top: 1px solid var(--panel-line); }
+  .chart-box { height: 250px; width: 100%; }
+  .table-wrap { overflow-x: auto; }
+  .row-actions { white-space: nowrap; }
+  .row-actions a + a { margin-left: 10px; }
+  .filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; }
+  .filters label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--t2); }
+  .filters select,.filters input { height: 32px; padding: 5px 8px; border: 1px solid var(--panel-line); border-radius: 5px; background: rgba(6,15,31,.7); color: var(--t1); max-width: 190px; }
+  .filters input[type=month] { color-scheme: dark; }
+  .loc { border: 1px solid var(--panel-line); cursor: pointer; color: #04121F; }
+  .loc.disabled { background: #4B5D73 !important; color: var(--t2); }
+  .dashboard-business .drill-head { margin-bottom: 16px; }
+  .receive-progress { display: flex; gap: 16px; align-items: center; padding: 14px 18px; margin: 12px 0; border: 1px solid var(--panel-line); background: var(--panel); }
+  .dashboard-business .ele-body { padding: 0; }
+  // 现有业务表单保留自身布局,撤销原型对第三方组件的全局间距重置。
+  .dashboard-business .el-form-item { margin-bottom: 18px; }
+  .dashboard-business .el-card__body { padding: 20px; }
+  .dashboard-business .el-input__inner { padding: 0 15px; }
+  .dashboard-business .el-button { padding: 9px 15px; }
+  .dashboard-business .el-table th,.dashboard-business .el-table td { padding: 8px 0; }
+  .dashboard-business .el-table .cell { padding: 0 10px; }
+  .dashboard-business .el-form-item__label { padding-right: 12px; }
+}
+@media (max-width: 1000px) { .wms-home { min-width: 900px; } }

+ 17 - 0
src/views/warehouseManagement/index/dashboard-screens.js

@@ -0,0 +1,17 @@
+const columns = (text) => text.split('|').map((part) => { const [key, name] = part.split(':'); return { key, name, numeric: /量|金额|天数|天$|天期|利用率/.test(name) }; });
+export const screens = {
+  ledger: { title: '库存台账', subtitle: '按仓库、单位、物料及批次核对在库状态', definition: '在库量含待检及锁定;可用量扣锁并排除未放行。不同基本单位不相加。估值仅使用可核实的批次入库单价。', metrics: ['inventoryQuantity', 'inventoryValue'], columns: columns('categoryCode:物料编码|categoryName:物料名称|warehouseName:仓库|systemBatchNo:系统批次|batchNo:批次号|quantity:在库量|availableQuantity:可用量|lockedQuantity:锁定量|pendingReleaseQuantity:未放行量|unit:单位|ageDays:库龄天数|qualityStatus:质检状态|valuation:批次估值'), action: '批次流水' },
+  dead: { title: '呆滞品分析', subtitle: '识别首次入库及最近实际消耗均超过 180 天的在库批次并推动真实处置', definition: '首次日期缺失单列“日期待核实”。处置申请及审批本身不改变库存;执行关联的调拨或出库才变动库存。', metrics: ['stagnantRate'], columns: columns('categoryCode:物料编码|categoryName:物料名称|warehouseName:仓库|systemBatchNo:系统批次|quantity:呆滞量|unit:单位|valuation:批次估值|stagnantDays:未消耗天数|ageDays:库龄天数'), action: '发起处置' },
+  io: { title: '出入库动态', subtitle: '已完成收发货流水及原始单据', definition: '只统计已完成出入库流水;草稿、待审批及待执行计入待办。不同基本单位分别核对。', period: true, columns: columns('orderNo:单据号|direction:方向|businessType:业务类型|categoryName:物料名称|warehouseName:仓库|quantity:数量|unit:单位|amount:金额|operateTime:作业时间|operatorName:操作员'), action: '查看单据' },
+  todo: { title: '待办单据', subtitle: '本月仓储盘点及未执行单据', definition: '进行中的逾期盘点仍计入待办并标红;计划完成时间缺失时显示“日期待核实”。待我审批按当前用户当前节点查询。', metrics: ['pendingInventory', 'pendingApproval'], columns: columns('orderNo:单据号|type:单据类型|warehouseName:仓库|status:状态|createTime:创建时间|dueTime:计划完成|reason:说明'), action: '查看任务' },
+  age: { title: '批次库龄明细', subtitle: '真实来源日期与库龄结构', definition: '从首次入库日期起算;调拨、拆合包不重置。缺失来源日期单列“日期待核实”,不按新建时间补造。', metrics: ['ageCoverage'], columns: columns('categoryCode:物料编码|categoryName:物料名称|warehouseName:仓库|batchNo:批次号|systemBatchNo:系统批次|firstInboundTime:首次入库|lastActivityTime:最近实际消耗|ageDays:库龄天数|quantity:在库量|unit:单位|valuation:批次估值'), action: '批次流水' },
+  alerts: { title: '安全库存预警清单', subtitle: '核对缺料、偏低及超储并发起补货', definition: '可用量扣锁并排除未放行。只按仓库、物料及其基本单位匹配有效阈值;未配置与未知不等于零。', columns: columns('categoryCode:物料编码|categoryName:物料名称|warehouseName:仓库|availableQuantity:可用量|minInventory:最低库存|secureInventory:安全库存|maxInventory:最高库存|gapQuantity:缺口数量|unit:单位|status:预警类型|reason:说明'), action: '生成采购申请' },
+  turnover: { title: '库存周转分析', subtitle: '按真实日末采集记录估算本月库存周转', definition: '使用截至昨日的真实日末采集与已完成实际消耗;未采集日期不补造,历史覆盖不足时说明无法计算。', metrics: ['turnoverDays'], columns: columns('categoryName:物料名称|warehouseName:仓库|unit:单位|averageQuantity:平均库存量|outboundQuantity:出库量|turnoverDays:周转天数|recordedDays:已采集天数|expectedDays:所需天数|reason:说明'), action: '查看库存' },
+  locations: { title: '库位利用率分析', subtitle: '有效普通库位占用情况及库位维护', definition: '统计分母仅为已启用普通库位,排除维修、暂存、虚拟及停用库位。仓库未维护库位时显示“未配置库位”。', metrics: ['locationUtilization'], columns: columns('code:库位编码|warehouseName:仓库|areaName:库区|shelfName:货架|status:占用状态|usage:用途|enabled:启用状态'), action: '库位明细' },
+  qc: { title: '入库质检分析', subtitle: '本月质检批次与实际检验结论', definition: '合格率为合格批次除以已检批次;让步接收计入分母不计合格。无需质检与未知状态不冒充合格。', period: true, metrics: ['qualityPassRate', 'qualityCoverage'], columns: columns('categoryName:物料名称|warehouseName:仓库|unit:单位|systemBatchNo:系统批次|qualityStatus:质检状态|qualityResult:检验结论|qualityTime:检验时间|reason:说明'), action: '打开检验工作台' },
+  flow: { title: '待我审批', subtitle: '当前用户当前节点的仓储审批任务', definition: '仅查询授权仓库、本月到达当前用户的仓储流程;批量审批逐单返回结果,复杂表单进入原审批页处理。', period: true, columns: columns('processInstance.processTypeName:流程分类|processInstance.name:流程名称|name:任务名称|businessCode:单据编码|businessName:单据名称|businessType:单据类型|processInstance.startUserNickname:发起人|createTime:到达时间'), action: '审批' }
+};
+export const metricLabels = {
+  inventoryQuantity: ['在库量', '', 'inventoryReason'], inventoryValue: ['可核实批次估值', '元', 'valuationReason'], stagnantRate: ['呆滞占比', '%', 'stagnantReason'], turnoverDays: ['本月周转天数', '天', 'turnoverReason'],
+  locationUtilization: ['库位利用率', '%', 'locationReason'], qualityPassRate: ['质检通过率', '%', 'qualityReason'], ageCoverage: ['库龄来源覆盖率', '%', 'stagnantReason'], qualityCoverage: ['质检记录覆盖率', '%', 'qualityReason'], pendingInventory: ['进行中盘点', '单', ''], pendingApproval: ['待我审批', '单', 'approvalReason']
+};

+ 235 - 0
src/views/warehouseManagement/index/dashboard-theme.scss

@@ -0,0 +1,235 @@
+/* 保留设计稿视觉参数,仅应用在 WMS 首页。 */
+.wms-home{
+
+  /* 大屏底色 */
+  --bg-top:#123258; --bg-mid:#0A1830; --bg-deep:#060F1F;
+  /* 面板 */
+  --panel:linear-gradient(180deg, rgba(16,40,72,.75), rgba(10,26,48,.85));
+  --panel-line:rgba(70,160,230,.28);
+  --inset:inset 0 0 24px rgba(35,120,210,.10);
+  /* 主色 */
+  --cyan:#38C8F5; --blue:#2E8BE8;
+  /* 语义色 */
+  --orange:#F0A03A; --red:#F25E5E; --green:#3BD48A; --purple:#9B7DF7;
+  /* 文字 */
+  --t1:#EAF4FF; --t2:#9FB8D4; --t3:#5F7A99; --th:#6FA6D8;
+  /* 线/网格 */
+  --grid:rgba(80,150,220,.12);
+  --split:rgba(70,160,230,.35);
+  --radius:10px;
+  --glow:0 0 12px rgba(56,200,245,.35);
+
+&,& *{margin:0;padding:0;box-sizing:border-box;font-family:"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;}
+&{
+  background:radial-gradient(ellipse at 50% 0%, var(--bg-top) 0%, var(--bg-mid) 52%, var(--bg-deep) 100%) fixed;
+  color:var(--t1); font-size:14px; min-height:100%;
+}
+
+
+
+
+::-webkit-scrollbar{width:8px;height:8px;}
+::-webkit-scrollbar-thumb{background:rgba(70,160,230,.35);border-radius:4px;}
+::-webkit-scrollbar-track{background:transparent;}
+
+/* ========== 主区 ========== */
+.main{flex:1;padding:0 20px 32px;min-width:0;}
+.view{display:none;}
+.view.active{display:block;animation:fadeIn .18s ease;}
+@keyframes fadeIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
+
+/* 大屏标题带 */
+.screen-band{margin:0 -20px 16px;padding:18px 20px 16px;position:relative;
+  background:linear-gradient(180deg, rgba(18,50,88,.85), rgba(10,26,48,.15));
+  border-bottom:1px solid var(--panel-line);}
+.screen-band::after{content:"";position:absolute;left:50%;bottom:-1px;transform:translateX(-50%);width:220px;height:2px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);}
+.screen-band .en{font-size:11px;letter-spacing:3px;color:var(--th);text-transform:uppercase;}
+.screen-band h1{font-size:24px;font-weight:700;letter-spacing:2px;margin-top:4px;color:#fff;text-shadow:0 0 18px rgba(56,200,245,.35);}
+.screen-band .meta{position:absolute;right:20px;bottom:16px;display:flex;gap:22px;font-size:12px;color:var(--t2);}
+.screen-band .meta b{color:var(--cyan);font-weight:600;}
+
+/* 通用面板 */
+.card{background:var(--panel);border:1px solid var(--panel-line);border-radius:var(--radius);
+  box-shadow:var(--inset);position:relative;overflow:hidden;}
+.card.corner::before,.card.corner::after{content:"";position:absolute;width:14px;height:14px;border:2px solid var(--cyan);opacity:.75;}
+.card.corner::before{top:-1px;left:-1px;border-right:none;border-bottom:none;border-radius:var(--radius) 0 0 0;}
+.card.corner::after{bottom:-1px;right:-1px;border-left:none;border-top:none;border-radius:0 0 var(--radius) 0;}
+.card-hd{display:flex;align-items:center;justify-content:space-between;padding:14px 18px 0;}
+.card-hd h3{font-size:14px;font-weight:600;color:#EAF4FF;display:flex;align-items:center;gap:8px;}
+.card-hd h3::before{content:"";width:4px;height:14px;border-radius:2px;background:var(--cyan);box-shadow:0 0 8px rgba(56,200,245,.7);}
+.card-hd .sub{font-size:12px;color:var(--t3);margin-top:4px;padding-left:12px;}
+.link{color:var(--cyan);font-size:12px;cursor:pointer;text-decoration:none;}
+.link:hover{text-shadow:0 0 10px rgba(56,200,245,.6);}
+.tag{display:inline-block;font-size:11px;padding:2px 8px;border-radius:10px;font-weight:500;border:1px solid transparent;}
+.tag.g{background:rgba(59,212,138,.14);color:var(--green);border-color:rgba(59,212,138,.4);}
+.tag.r{background:rgba(242,94,94,.14);color:var(--red);border-color:rgba(242,94,94,.4);}
+.tag.o{background:rgba(240,160,58,.14);color:var(--orange);border-color:rgba(240,160,58,.4);}
+.tag.b{background:rgba(56,200,245,.14);color:var(--cyan);border-color:rgba(56,200,245,.4);}
+.tag.p{background:rgba(155,125,247,.14);color:var(--purple);border-color:rgba(155,125,247,.4);}
+.tag.gr{background:rgba(120,160,200,.12);color:var(--t2);border-color:rgba(120,160,200,.3);}
+
+/* ========== 问候 + 快捷操作 ========== */
+.greet{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;}
+.greet h2{font-size:18px;font-weight:700;color:#fff;}
+.greet p{font-size:12px;color:var(--t3);margin-top:4px;}
+.greet p b{color:var(--cyan);font-weight:600;}
+.btn{display:inline-flex;align-items:center;gap:6px;background:linear-gradient(135deg,#38C8F5,#2E8BE8);color:#04121F;border:none;border-radius:7px;padding:8px 14px;font-size:13px;cursor:pointer;font-weight:600;box-shadow:0 0 12px rgba(56,200,245,.3);}
+.btn:hover{box-shadow:0 0 18px rgba(56,200,245,.6);}
+.btn.ghost{background:rgba(56,200,245,.06);color:var(--t1);border:1px solid var(--panel-line);box-shadow:none;font-weight:500;}
+.btn.ghost:hover{border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 12px rgba(56,200,245,.25);}
+
+/* ========== 今日待办条(单行合并 V1.2) ========== */
+.todo-bar{display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--panel-line);border-radius:var(--radius);box-shadow:var(--inset);padding:18px 10px;margin-bottom:14px;}
+.todo-item{flex:1;display:flex;align-items:center;gap:14px;padding:8px 26px;cursor:pointer;border-radius:8px;transition:all .2s;position:relative;}
+.todo-item:hover{background:rgba(56,200,245,.08);box-shadow:inset 0 0 14px rgba(56,200,245,.15);}
+.todo-item:hover .todo-arrow{opacity:1;transform:translateX(0);}
+.todo-item+.todo-item::before{content:"";position:absolute;left:0;top:8%;bottom:8%;width:1px;background:rgba(160,190,225,.28);}
+.todo-icon{width:48px;height:48px;border-radius:11px;flex-shrink:0;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,.35);}
+.todo-icon svg{width:23px;height:23px;}
+.icon-in{background:linear-gradient(180deg,#3D9BF0,#2E7FD8);}
+.icon-out{background:linear-gradient(180deg,#F5AC4E,#E08F28);}
+.icon-check{background:linear-gradient(180deg,#43DE97,#2CB877);}
+.icon-approve{background:linear-gradient(180deg,#A98FFA,#8A66F0);}
+.todo-num{font-size:30px;font-weight:700;line-height:1.1;text-shadow:var(--glow);display:flex;align-items:baseline;gap:8px;}
+.todo-label{font-size:13px;color:var(--t2);margin-top:3px;}
+.todo-arrow{margin-left:auto;color:var(--t3);font-size:16px;opacity:0;transform:translateX(-4px);transition:all .2s;}
+.badge-today{font-size:11px;color:var(--t3);font-weight:400;text-shadow:none;}
+.badge-overdue{font-size:11px;color:var(--red);background:rgba(242,94,94,.15);border:1px solid rgba(242,94,94,.5);border-radius:8px;padding:1px 7px;font-weight:400;text-shadow:none;}
+
+/* ========== KPI 卡 ========== */
+.kpi-row{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:14px;}
+.kpi{padding:14px 16px;cursor:pointer;transition:transform .12s, box-shadow .12s, border-color .12s;position:relative;overflow:hidden;}
+.kpi:hover{transform:translateY(-2px);border-color:var(--cyan);box-shadow:0 0 22px rgba(56,200,245,.28), var(--inset);}
+.kpi:hover .kpi-go{opacity:1;}
+.kpi .idx{position:absolute;top:8px;right:10px;font-size:20px;font-weight:800;color:rgba(110,170,220,.18);letter-spacing:1px;}
+.kpi .lb{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--t2);position:relative;z-index:1;}
+.kpi .lb .q{width:14px;height:14px;border-radius:50%;background:rgba(56,200,245,.12);color:var(--th);font-size:10px;display:inline-flex;align-items:center;justify-content:center;cursor:help;border:1px solid rgba(56,200,245,.3);}
+.kpi .val{font-size:24px;font-weight:700;margin:8px 0 4px;letter-spacing:-.3px;text-shadow:var(--glow);}
+.kpi .val small{font-size:12px;font-weight:500;color:var(--t3);margin-left:2px;text-shadow:none;}
+.kpi .delta{font-size:11px;display:flex;align-items:center;gap:4px;}
+.kpi .delta.good{color:var(--green);} .kpi .delta.warn{color:var(--orange);} .kpi .delta.bad{color:var(--red);} .kpi .delta.dim{color:var(--t3);}
+.kpi-go{position:absolute;top:12px;right:12px;opacity:0;transition:opacity .15s;color:var(--cyan);font-size:12px;}
+.bar-mini{height:4px;background:rgba(80,150,220,.15);border-radius:2px;margin-top:10px;overflow:hidden;}
+.bar-mini i{display:block;height:100%;border-radius:2px;box-shadow:0 0 8px currentColor;}
+
+/* ========== 图表区 ========== */
+.grid-2-1{display:grid;grid-template-columns:1fr 380px;gap:12px;margin-bottom:12px;}
+.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px;}
+.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
+.chart-body{padding:8px 18px 16px;}
+.legend{display:flex;gap:14px;font-size:11px;color:var(--t2);}
+.legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px;vertical-align:-1px;}
+table.tbl{width:100%;border-collapse:collapse;font-size:12.5px;}
+table.tbl th{text-align:left;color:var(--th);font-weight:500;padding:9px 10px;border-bottom:1px solid var(--grid);white-space:nowrap;}
+table.tbl td{padding:9px 10px;border-bottom:1px solid var(--grid);white-space:nowrap;color:var(--t1);}
+table.tbl tr:hover td{background:rgba(56,200,245,.06);}
+.num-r{text-align:right;font-variant-numeric:tabular-nums;}
+/* 预警列表 */
+.alert-item{display:flex;align-items:center;gap:10px;padding:9px 18px;border-bottom:1px solid var(--grid);font-size:12.5px;}
+.alert-item:last-child{border-bottom:none;}
+.alert-item .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;box-shadow:0 0 8px currentColor;}
+.alert-item .meta{color:var(--t2);margin-left:auto;white-space:nowrap;}
+.alert-item b{color:#EAF4FF;font-weight:600;}
+/* 动态 feed */
+.feed{padding:6px 18px 14px;}
+.feed-item{display:flex;gap:10px;padding:8px 0;font-size:12.5px;border-bottom:1px dashed var(--grid);color:var(--t2);}
+.feed-item:last-child{border:none;}
+.feed-item .tm{color:var(--t3);white-space:nowrap;font-size:11.5px;padding-top:1px;}
+.feed-item b{color:#EAF4FF;font-weight:600;}
+
+/* ========== 下钻页 ========== */
+.crumb{display:flex;align-items:center;gap:8px;margin:16px 0 14px;color:var(--t3);font-size:13px;}
+.crumb b{color:#EAF4FF;}
+.crumb .back{color:var(--cyan);cursor:pointer;display:none;align-items:center;gap:4px;}
+.view.drill .crumb .back{display:inline-flex;}
+.drill-head{margin-bottom:12px;}
+.drill-head h2{font-size:17px;font-weight:700;text-shadow:0 0 14px rgba(56,200,245,.25);}
+.drill-head p{font-size:12px;color:var(--t3);margin-top:4px;}
+.def-strip{display:flex;gap:10px;align-items:flex-start;background:rgba(56,200,245,.07);border:1px solid rgba(56,200,245,.28);border-radius:8px;padding:10px 14px;font-size:12px;color:#B9D8EF;margin-bottom:12px;line-height:1.6;}
+.def-strip b{color:var(--cyan);}
+.filters{display:flex;gap:10px;align-items:center;padding:12px 14px;flex-wrap:wrap;}
+.filters select,.filters input{height:32px;border:1px solid var(--panel-line);border-radius:6px;padding:0 10px;font-size:12.5px;color:var(--t1);background:rgba(6,15,31,.6);outline:none;}
+.filters select:focus,.filters input:focus{border-color:var(--cyan);box-shadow:0 0 10px rgba(56,200,245,.25);}
+.filters select option{background:#0A1830;color:var(--t1);}
+.filters .lb{font-size:12px;color:var(--t2);}
+.filters input::placeholder{color:var(--t3);}
+.pager{display:flex;align-items:center;justify-content:flex-end;gap:6px;padding:12px 14px;font-size:12px;color:var(--t3);}
+.pager .pg{min-width:26px;height:26px;border:1px solid var(--panel-line);border-radius:5px;display:inline-flex;align-items:center;justify-content:center;background:rgba(6,15,31,.5);cursor:pointer;color:var(--t2);}
+.pager .pg:hover{border-color:var(--cyan);color:var(--cyan);}
+.pager .pg.on{background:linear-gradient(135deg,#38C8F5,#2E8BE8);border-color:transparent;color:#04121F;font-weight:700;}
+.kpi-strip{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:12px;}
+.kpi-strip .kpi{padding:12px 14px;cursor:default;}
+.kpi-strip .kpi .val{font-size:19px;margin:6px 0 2px;}
+/* 页签 */
+.tabs{display:flex;gap:8px;padding:12px 14px 0;flex-wrap:wrap;}
+.tab{padding:7px 14px;border-radius:7px;font-size:13px;color:var(--t2);cursor:pointer;border:1px solid transparent;background:rgba(56,200,245,.05);transition:all .15s;}
+.tab:hover{color:#EAF4FF;border-color:var(--panel-line);}
+.tab.on{background:linear-gradient(135deg,#38C8F5,#2E8BE8);color:#04121F;font-weight:600;box-shadow:0 0 12px rgba(56,200,245,.35);}
+.tab-pane{display:none;}
+.tab-pane.on{display:block;animation:fadeIn .18s ease;}
+/* 盘点任务弹窗 */
+.modal-mask{display:none;position:fixed;inset:0;background:rgba(4,10,22,.62);z-index:60;align-items:center;justify-content:center;}
+.modal-mask.on{display:flex;}
+.modal{width:680px;max-width:92%;background:linear-gradient(180deg,#0F2A4C,#0A1B36);border:1px solid rgba(70,160,230,.4);border-radius:12px;box-shadow:0 18px 60px rgba(0,0,0,.5), inset 0 0 24px rgba(35,120,210,.12);overflow:hidden;animation:fadeIn .18s ease;}
+.modal-hd{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;border-bottom:1px solid rgba(70,160,230,.25);}
+.modal-hd h3{font-size:15px;color:#EAF4FF;font-weight:600;display:flex;align-items:center;gap:8px;}
+.modal-hd h3::before{content:"";width:4px;height:14px;background:#38C8F5;border-radius:2px;display:inline-block;}
+.modal-x{cursor:pointer;color:#9FB8D4;font-size:18px;line-height:1;padding:2px 6px;}
+.modal-x:hover{color:#EAF4FF;}
+.modal-ft{display:flex;justify-content:flex-end;gap:10px;padding:12px 18px 16px;}
+/* 库位热力图 */
+.loc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(76px,1fr));gap:8px;padding:10px 18px 6px;}
+.loc{height:52px;border-radius:6px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:11px;cursor:pointer;border:1px solid rgba(255,255,255,.10);transition:all .15s;color:#04121F;}
+.loc b{font-size:12.5px;font-weight:700;}
+.loc:hover{transform:translateY(-2px);border-color:var(--cyan);box-shadow:0 0 14px rgba(56,200,245,.45);}
+.legend-box{display:flex;gap:16px;padding:6px 18px 14px;font-size:11px;color:var(--t2);flex-wrap:wrap;}
+.legend-box i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px;vertical-align:-1px;}
+/* ========== 单据新增表单页 ========== */
+.frm{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 26px;padding:14px 18px 6px;}
+.frm .it{display:flex;align-items:center;gap:8px;min-width:0;}
+.frm .lb{width:96px;text-align:right;font-size:12.5px;color:var(--t2);flex-shrink:0;}
+.frm .lb.req::before{content:"*";color:var(--red);margin-right:3px;font-weight:700;}
+.frm input,.frm select{flex:1;height:32px;border:1px solid var(--panel-line);border-radius:6px;background:rgba(6,15,31,.6);color:var(--t1);padding:0 10px;font-size:12.5px;outline:none;min-width:0;}
+.frm input:focus,.frm select:focus{border-color:var(--cyan);box-shadow:0 0 10px rgba(56,200,245,.25);}
+.frm input::placeholder{color:var(--t3);}
+.frm input[readonly]{color:var(--t3);}
+.frm select option{background:#0A1830;}
+.frm .span2{grid-column:span 2;}
+.btn-pick{height:32px;padding:0 12px;border-radius:6px;border:none;background:linear-gradient(135deg,#38C8F5,#2E8BE8);color:#04121F;font-size:12.5px;font-weight:600;cursor:pointer;flex-shrink:0;}
+.sec-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 18px 0;}
+.sec-hd h3{font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;}
+.sec-hd h3::before{content:"";width:4px;height:14px;border-radius:2px;background:var(--cyan);box-shadow:0 0 8px rgba(56,200,245,.7);}
+.btn-add{height:30px;padding:0 14px;border-radius:6px;border:1px solid rgba(56,200,245,.5);background:rgba(56,200,245,.10);color:var(--cyan);font-size:12.5px;cursor:pointer;}
+.btn-add:hover{background:rgba(56,200,245,.2);box-shadow:0 0 12px rgba(56,200,245,.3);}
+.form-foot{display:flex;justify-content:center;gap:14px;padding:18px 18px 22px;}
+/* ========== 仓库切换器 ========== */
+.ws-wrap{position:relative;}
+.ws-menu{display:none;position:absolute;top:calc(100% + 10px);right:0;min-width:210px;z-index:99;
+  background:linear-gradient(180deg, rgba(16,40,72,.98), rgba(10,26,48,.98));
+  border:1px solid rgba(56,200,245,.5);border-radius:8px;padding:6px;
+  box-shadow:0 8px 30px rgba(0,0,0,.5), 0 0 18px rgba(56,200,245,.2);}
+.ws-menu.show{display:block;animation:fadeIn .15s ease;}
+.ws-menu::before{content:"";position:absolute;top:-6px;right:20px;width:10px;height:10px;
+  background:rgba(16,40,72,.98);border-left:1px solid rgba(56,200,245,.5);border-top:1px solid rgba(56,200,245,.5);transform:rotate(45deg);}
+.ws-item{padding:9px 12px;border-radius:6px;font-size:13px;color:var(--t2);cursor:pointer;}
+.ws-item:hover{background:rgba(56,200,245,.12);color:#EAF4FF;}
+.ws-item.on{color:var(--cyan);background:rgba(56,200,245,.10);font-weight:600;}
+/* toast */
+.toast{position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(20px);
+  background:linear-gradient(180deg, rgba(16,40,72,.98), rgba(10,26,48,.98));
+  border:1px solid rgba(56,200,245,.6);color:#EAF4FF;padding:10px 18px;border-radius:8px;font-size:13px;
+  opacity:0;transition:all .25s;pointer-events:none;z-index:99;box-shadow:0 0 26px rgba(56,200,245,.35);}
+.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
+@media (max-width:1400px){
+  .kpi-row{grid-template-columns:repeat(3,1fr);}
+  .grid-2-1,.grid-3,.grid-2{grid-template-columns:1fr;}
+  .kpi-strip{grid-template-columns:repeat(3,1fr);}
+  /* 待办条任何宽度都强制一行,只收紧间距不允许换行 */
+  .todo-item{padding:8px 14px;gap:10px;}
+  .todo-num{font-size:24px;}
+  .todo-icon{width:40px;height:40px;border-radius:9px;}
+  .todo-icon svg{width:19px;height:19px;}
+  .screen-band .meta{position:static;margin-top:10px;}
+}
+
+}

+ 36 - 0
src/views/warehouseManagement/index/dashboard-utils.js

@@ -0,0 +1,36 @@
+// 空值保留未知语义;不把缺价、缺日期或无历史显示成零。
+export function displayNumber(value) {
+  if (value == null || value === '') return '—';
+  const number = Number(value);
+  return Number.isFinite(number) ? number.toLocaleString('zh-CN', { maximumFractionDigits: 4 }) : String(value);
+}
+
+export function monthRange(now = new Date()) {
+  const prefix = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
+  const last = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
+  return { startTime: prefix + '-01 00:00:00', endTime: prefix + '-' + last + ' 23:59:59' };
+}
+
+export function cleanQuery(query) {
+  return Object.keys(query).reduce((result, key) => {
+    if (query[key] != null && query[key] !== '') result[key] = String(query[key]);
+    return result;
+  }, {});
+}
+
+// 只从当前授权菜单解析跨系统入口,路径保持门户同源。
+export function findPortalEntry(roots, predicate) {
+  const visit = (nodes, systemRoot, parentPath) => {
+    for (const node of nodes || []) {
+      const path = node.path || '';
+      const match = path.match(/^(\/page-[a-z]+)(?:\/|$)/);
+      const root = match ? match[1] : systemRoot;
+      const full = match ? path : root && path ? (path.startsWith('/') ? root + path : (parentPath || root) + '/' + path) : parentPath;
+      if (predicate(node) && full && /^\/page-[a-z]+\/[\w/-]+$/.test(full)) return full;
+      const found = visit(node.children, root, full);
+      if (found) return found;
+    }
+    return '';
+  };
+  return visit(roots, '', '');
+}

+ 211 - 0
src/views/warehouseManagement/index/dashboard.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="wms-home">
+    <main class="main">
+        <home-overview v-if="view === 'home'" :summary="summary" :unit="unit" :updated-at="updatedAt" :error="error" @open="open" @refresh="refresh" @business="business" />
+        <section v-else-if="businessComponent" class="view active dashboard-business"><nav class="crumb"><a href="#" @click.prevent="open('home')">‹ 返回首页</a><span>仓储管理系统</span> / <b>{{ businessTitle }}</b></nav><div class="drill-head"><h2>{{ businessTitle }}</h2></div><div v-if="receiveQueue.length" class="receive-progress">批量收货:正在核对第 {{ preparedReceipts.length + 1 }} / {{ receiveQueue.length }} 单。请核对仓库、产品明细及包装后确认。<el-button @click="cancelReceive">取消批量收货</el-button></div><div v-if="!receiveQueue.length && preparedReceipts.length" class="receive-progress">有 {{ preparedReceipts.length }} 单待重试。<el-button :loading="receiving" @click="submitReceipts">重试收货</el-button></div><component :is="businessComponent" :key="view + ($route.query.inId || '')" :batch-prepare="receiveQueue.length > 0" @batch-receive="startReceive" @prepared="prepareReceipt" @cancelled="cancelReceive" /></section>
+        <dashboard-detail v-else :key="view" :view="view" :filters="filters" :options="options" :result="result" :summary="summary" :loading="loading" :error="error" :exporting="exporting" @back="open('home')" @navigate="open" @search="search" @retry="loadDetail" @export="exportRows" @business="business" @batch="batchApprove" />
+    </main>
+    <dashboard-actions ref="actions" :options="options" :filters="filters" @success="refresh" @business="business" />
+  </div>
+</template>
+<script>
+import HomeOverview from './components/HomeOverview.vue';
+import DashboardDetail from './detail.vue';
+import DashboardActions from './components/DashboardActions.vue';
+import { homeOptions, homeSummary, homeDetail, warehouseTasks, dashboardAction } from '@/api/warehouseManagement/dashboard';
+import { getResourcesTree } from '@/api/layout';
+import { getCurrentUser } from '@/utils/token-util';
+import { monthRange, cleanQuery, findPortalEntry } from './dashboard-utils';
+import { screens } from './dashboard-screens';
+
+const businessViews = {
+  addin: () => import('../stockManagement/add.vue'), addout: () => import('../outgoingManagement/add.vue'),
+  ioapp: () => import('../inboundRequests/index.vue'), inapp: () => import('../warehouseApplication/index.vue')
+};
+const legacyRoutes = {
+  ledger: '/warehouseManagement/stockLedger',
+  stockLedger: '/warehouseManagement/stockLedger',
+  stockManagement: '/warehouseManagement/stockManagement',
+  outgoingManagement: '/warehouseManagement/outgoingManagement'
+};
+const filterKeys = ['warehouseId','factoryId','unit','categoryId','startTime','endTime','keyword','type','direction','stockBatchId','allocationId','age','level','inspection','status','page'];
+export default {
+  name: 'WmsHome',
+  components: { HomeOverview, DashboardDetail, DashboardActions },
+  data() { return {
+    view: 'home', warehouseId: '', unit: '', filters: {}, options: {}, summary: {}, result: {}, updatedAt: '', error: '', loading: false, exporting: false, portalRoots: [], optionsLoaded: false, receiveQueue: [], preparedReceipts: [], receiving: false
+  }; },
+  computed: {
+    businessComponent() { return businessViews[this.view] || null; },
+    businessTitle() { return { addin:'新增入库单',addout:'新增出库单',ioapp:'出入库申请',inapp:'入总库申请' }[this.view]; }
+  },
+  watch: { '$route.query': { deep: true, handler() { this.readRoute(); this.refresh(); } } },
+  async created() {
+    this.active = true; this.generation = 0; this.requests = {}; this.timers = [];
+    this.readRoute();
+    try {
+      this.options = await homeOptions({ warehouseId: this.warehouseId }); this.optionsLoaded = true;
+      if (!this.unit && !Object.prototype.hasOwnProperty.call(this.$route.query, 'unit')) this.unit = this.options.defaultUnit || '';
+      this.filters = { ...this.filters, unit: this.unit };
+      await this.refresh();
+    } catch (e) { this.error = e.message || '筛选项加载失败,请刷新重试'; }
+    this.startTimers();
+  },
+  mounted() { document.body.classList.add('wms-dashboard-theme'); },
+  activated() { const resumed = !this.active; this.active = true; document.body.classList.add('wms-dashboard-theme'); this.startTimers(); if (resumed) this.refresh(); },
+  deactivated() { this.stop(); },
+  beforeDestroy() { this.stop(); if (this.$route.query.dashboard !== '1') document.body.classList.remove('wms-dashboard-theme'); },
+  methods: {
+    readRoute() {
+      const query = this.$route.query;
+      const knownView = screens[query.dashboardView] || businessViews[query.dashboardView] || query.dashboardView === 'home';
+      this.view = knownView ? query.dashboardView : 'home';
+      this.filters = filterKeys.reduce((r,key) => { if (query[key] != null) r[key] = query[key]; return r; }, { ...monthRange(), page: 1 });
+      if (this.view === 'todo' && !this.filters.type) this.filters.type = 'inventory';
+      this.warehouseId = this.filters.warehouseId || ''; this.unit = this.filters.unit || '';
+    },
+    route(view, filters, replace = false) {
+      const query = cleanQuery({ ...filters, unit: filters.unit || '', dashboard: '1', dashboardView: view });
+      // 显式保留空单位,返回时不再自动选入默认单位。
+      query.unit = filters.unit || '';
+      if (this.$route.query.dashboardView === view && JSON.stringify(query) === JSON.stringify(this.$route.query)) return;
+      this.$router[replace ? 'replace' : 'push']({ path: this.$route.path, query }).catch((e) => { if (e.name !== 'NavigationDuplicated') this.$message.error(e.message); });
+    },
+    open(view, extra = {}) {
+      if (legacyRoutes[view]) return this.$router.push({ path: legacyRoutes[view], query: cleanQuery({ ...this.filters, ...extra }) });
+      const filters = { ...this.filters, page: 1, ...extra };
+      if (view !== this.view) ['stockBatchId','allocationId','age','level','inspection','type','direction'].forEach((key) => { if (!(key in extra)) delete filters[key]; });
+      if (view === 'todo' && !filters.type) filters.type = 'inventory';
+      if (view === 'ioapp' && !filters.direction) filters.direction = 'in';
+      if (['ioapp','inapp'].includes(view)) {
+        filters.status = Object.prototype.hasOwnProperty.call(extra,'status') ? extra.status : '0';
+        delete filters.categoryId; filters.unit = ''; Object.assign(filters,monthRange());
+      }
+      this.route(view, filters);
+    },
+    search(filters) { this.route(this.view, filters); },
+    parameters(extra = {}) { return cleanQuery({ ...this.filters, ...extra }); },
+    async refresh() {
+      if (!this.active) return;
+      this.generation++; this.requests = {}; this.error = ''; this.loading = false;
+      if (!this.optionsLoaded) {
+        try { this.options = await homeOptions({ warehouseId:this.warehouseId }); this.optionsLoaded = true; }
+        catch (e) { this.error = e.message || '筛选项加载失败,请重试'; return; }
+      }
+      await Promise.allSettled([this.loadSummary(), this.businessComponent || this.view === 'home' ? Promise.resolve() : this.loadDetail()]);
+    },
+    async loadSummary() {
+      const generation = this.generation; const seq = (this.requests.summary || 0) + 1; this.requests.summary = seq;
+      try {
+        const data = await homeSummary(this.parameters());
+        if (!this.active || generation !== this.generation || seq !== this.requests.summary) return;
+        this.summary = data || {}; this.updatedAt = new Date().toLocaleTimeString('zh-CN', { hour12: false });
+      } catch (e) { if (this.active && generation === this.generation) this.error = e.message || '首页数据加载失败,请重试'; }
+    },
+    async fetchDetail(filters = this.filters, page = filters.page || 1, limit = 20) {
+      if (this.view === 'flow') {
+        const data = await warehouseTasks({ pageNo: page, pageSize: limit, warehouseScope: true, warehouseId: filters.warehouseId || undefined, month: (filters.startTime || monthRange().startTime).slice(0,7), name: filters.keyword || undefined });
+        return { ...data, records: data.list || data.records || [], total: data.total == null ? data.count : data.total };
+      }
+      return homeDetail(cleanQuery({ ...filters, view: this.view, page, limit }));
+    },
+    async loadDetail() {
+      const generation = this.generation; const seq = (this.requests.detail || 0) + 1; this.requests.detail = seq;
+      this.loading = true; this.result = {};
+      try { const data = await this.fetchDetail(); if (this.active && generation === this.generation && seq === this.requests.detail) this.result = data || {}; }
+      catch (e) { if (this.active && generation === this.generation) this.error = e.message || '明细加载失败,请重试'; }
+      finally { if (this.active && generation === this.generation && seq === this.requests.detail) this.loading = false; }
+    },
+    async refreshPart(part) {
+      if (!this.active || this.view !== 'home') return;
+      const generation = this.generation; const seq = (this.requests[part] || 0) + 1; this.requests[part] = seq;
+      try {
+        const data = await homeSummary(this.parameters({ section: part }));
+        if (!this.active || generation !== this.generation || seq !== this.requests[part]) return;
+        this.$set(this.summary, part, data[part] || []);
+        this.$set(this.summary, part === 'alerts' ? 'alertReason' : 'activityReason', data[part === 'alerts' ? 'alertReason' : 'activityReason'] || '');
+      } catch (e) { if (this.active && generation === this.generation) this.$set(this.summary, part === 'alerts' ? 'alertReason' : 'activityReason', e.message || '刷新失败,请手动重试'); }
+    },
+    startTimers() {
+      if (!this.active || this.timers && this.timers.length) return;
+      this.timers = [setInterval(() => this.refresh(), 15*60*1000),setInterval(() => this.refreshPart('alerts'),5*60*1000),setInterval(() => this.refreshPart('activities'),30*1000)];
+    },
+    stop() { this.active = false; this.generation++; (this.timers || []).forEach(clearInterval); this.timers = []; },
+    async exportRows() {
+      this.exporting = true; const generation = this.generation; const filters = { ...this.filters }; const view = this.view;
+      try {
+        const rows = []; let page = 1; let total;
+        do { const data = await this.fetchDetail(filters,page++,200); if (!this.active || generation !== this.generation || view !== this.view) return; rows.push(...(data.records || [])); total = Number(data.total || 0); if (!(data.records || []).length) break; } while (rows.length < total);
+        const XLSX = await import('xlsx'); const columns = screens[view].columns;
+        const output = rows.map((row) => columns.reduce((r,c) => { r[c.name] = c.key.split('.').reduce((v,k) => v == null ? null : v[k],row); return r; },{}));
+        const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook,XLSX.utils.json_to_sheet(output),'明细'); XLSX.writeFile(workbook,screens[view].title + '-' + new Date().toISOString().slice(0,10) + '.xlsx');
+      } catch (e) { this.$message.error(e.message || '导出失败,请重试'); } finally { this.exporting = false; }
+    },
+    navigate(path, query = {}) { this.$router.push({ path, query: cleanQuery({ ...this.filters, ...query, dashboard:'1' }) }); },
+    async portal(predicate, query) {
+      try {
+        if (!this.portalRoots.length) { const user = getCurrentUser(); this.portalRoots = await getResourcesTree({ groupId:user.currentGroupId,roleId:user.currentRoleId }); }
+        const path = findPortalEntry(this.portalRoots,predicate);
+        if (!path) return this.$message.warning('当前账号未获得对应业务入口,请联系管理员配置菜单权限');
+        const params = new URLSearchParams(cleanQuery(query)); window.history.pushState(null,'',path + (params.toString() ? '?' + params : ''));
+      } catch (e) { this.$message.error(e.message || '授权入口加载失败'); }
+    },
+    business(kind,row) {
+      if (['purchase','threshold','disposal','disposal-list','finance'].includes(kind)) return this.$refs.actions.open(kind,row);
+      if (kind === 'dead') return this.$refs.actions.open('disposal',row);
+      if (kind === 'alerts') return row.status === 'excess' ? this.open('ledger',{ categoryId:row.categoryId,warehouseId:row.warehouseId }) : this.$refs.actions.open('purchase',row);
+      if (kind === 'flow') return this.portal((n) => /bpm\/todo/.test(n.component || '') || /^\/?todo$/.test(n.path || ''), { taskId:row.id,warehouseScope:'true',warehouseId:this.warehouseId,month:(this.filters.startTime || '').slice(0,7),dashboard:'1' });
+      if (kind === 'purchase-document') return this.portal((n) => /purchasingManage\/purchaseNeedManage/.test((n.component || '') + (n.path || '')), { requirementId:row.id,dashboard:'1' });
+      if (kind === 'qc') return this.portal((n) => /inspectionWork|检验工作台|质检报告/.test((n.component || '') + (n.title || n.name || '')), { stockBatchId:row.stockBatchId || row.id,systemBatchNo:row.systemBatchNo,warehouseId:row.warehouseId,dashboard:'1' });
+      if (kind === 'io') return row.orderId ? this.navigate(String(row.direction) === '1' ? '/warehouseManagement/stockManagement/details' : '/warehouseManagement/outgoingManagement/details',{ id:row.orderId }) : this.$message.warning('该流水缺少来源单据身份,请先核实');
+      if (kind === 'todo') return row.type === 'inventory' ? this.navigate('/warehouseManagement/stocktaking/work/details',{ id:row.id }) : this.open(row.type === 'central' ? 'inapp' : 'ioapp',{ direction:row.type === 'out' ? 'out' : 'in',keyword:row.orderNo });
+      if (kind === 'locations') return this.open('ledger',{ allocationId:row.id,warehouseId:row.warehouseId });
+      if (kind === 'location-maintain') return this.navigate('/warehouseManagement/warehouseDefinition');
+      if (kind === 'turnover') return this.open('ledger',{ categoryId:row.categoryId,warehouseId:row.warehouseId,unit:row.unit });
+      if (['ledger','age'].includes(kind)) return this.navigate('/warehouseManagement/stockLedger/batchDetails',{ id:row.categoryId,batchNo:row.batchNo,systemBatchNo:row.systemBatchNo,stockBatchId:row.stockBatchId || row.id,warehouseId:row.warehouseId });
+      if (kind === 'disposal-document') return this.navigate(row.documentType === 'ALLOT' ? '/warehouseManagement/inventoryAllocation/details' : '/warehouseManagement/outgoingManagement/details',{ id:row.documentId });
+    },
+    async startReceive(rows) {
+      if (this.receiving || this.receiveQueue.length) return;
+      if (this.preparedReceipts.length) return this.$message.warning('请先重试未完成的收货');
+      this.receiveQueue = rows.slice(); this.preparedReceipts = []; this.nextReceipt();
+    },
+    nextReceipt() {
+      const row = this.receiveQueue[this.preparedReceipts.length];
+      if (!row) return this.submitReceipts();
+      this.route('addin',{ ...this.filters,code:row.code,inId:row.id,type:'1',sourceType:row.sourceType,name:'inboundRequests',batchReceive:'1' },true);
+    },
+    prepareReceipt(request) {
+      if (!this.receiveQueue.length || this.receiving) return;
+      const row = this.receiveQueue[this.preparedReceipts.length];
+      // 路由切换完成前旧表单仍可能发出确认事件,不能准备到下一张申请。
+      if (!row || String(this.$route.query.inId) !== String(row.id)) return;
+      this.preparedReceipts.push({ requestKey:'wms-receive-' + row.id + '-' + Date.now(),request }); this.nextReceipt();
+    },
+    async cancelReceive() {
+      try { await this.$confirm('取消本次批量收货?尚未提交的收货信息将清除。','取消确认'); this.receiveQueue = []; this.preparedReceipts = []; this.route('ioapp',{ ...this.filters,direction:'in',status:'0' },true); } catch (e) { /* 取消确认后继续核对。 */ }
+    },
+    async submitReceipts() {
+      if (this.receiving || !this.preparedReceipts.length) return;
+      this.receiving = true;
+      try {
+        await this.$confirm('已核对 ' + this.preparedReceipts.length + ' 单,确认保存并提交原审批或质检?','批量收货确认');
+        const results = await dashboardAction('batch-receive',{ items:this.preparedReceipts });
+        const succeeded = new Set(results.filter((r) => r.success).map((r) => String(r.key)));
+        this.preparedReceipts = this.preparedReceipts.filter((r) => !succeeded.has(String(r.requestKey))); this.receiveQueue = [];
+        this.route('ioapp',{ ...this.filters,direction:'in',status:'0' },true); this.$refs.actions.showResults(results,'成功表示已保存并提交原审批或质检;库存是否增加取决于原流程的执行结果。'); this.refresh();
+      } catch (e) { if (e !== 'cancel' && e !== 'close') this.$message.error(e.message || '收货失败,保留原信息供重试'); this.receiveQueue = []; this.route('ioapp',{ ...this.filters,direction:'in',status:'0' },true); }
+      finally { this.receiving = false; }
+    },
+    async batchApprove(rows) {
+      try {
+        const { value } = await this.$prompt('审批意见(将逐单校验当前节点权限)','批量审批',{ inputValue:'同意',inputValidator:(v) => !!(v || '').trim(),inputErrorMessage:'请输入审批意见' });
+        const results = await dashboardAction('batch-approve',{ taskIds:rows.map((r) => r.id),reason:value,month:(this.filters.startTime || '').slice(0,7),warehouseId:this.warehouseId || undefined });
+        this.$refs.actions.showResults(results); this.refresh();
+      } catch (e) { if (e !== 'cancel' && e !== 'close') this.$message.error(e.message || '批量审批失败,请重试'); }
+    }
+  }
+};
+</script>
+<style lang="scss" src="./dashboard-theme.scss"></style>
+<style lang="scss" src="./dashboard-layout.scss"></style>

+ 76 - 0
src/views/warehouseManagement/index/detail.vue

@@ -0,0 +1,76 @@
+<template>
+  <section class="wms-drill view active">
+    <nav class="crumb"><a href="#" @click.prevent="$emit('back')">‹ 返回首页</a><span>仓储管理系统</span> / <b>{{ current.title }}</b></nav>
+    <div class="drill-head"><h2>{{ current.title }}</h2><p>{{ current.subtitle }}</p></div>
+    <div class="def-strip"><b>口径说明:</b><span>{{ current.definition }}<br v-if="result.reason"><span v-if="result.reason">{{ result.reason }}</span></span></div>
+    <div v-if="current.metrics && current.metrics.length" class="kpi-strip">
+      <div v-for="key in current.metrics" :key="key" class="card kpi"><div class="lb">{{ metricLabels[key][0] }}</div><div class="val">{{ show(summary[key]) }}<small v-if="summary[key] != null">{{ metricLabels[key][1] || filters.unit }}</small></div><div class="delta dim">{{ summary[metricLabels[key][2]] || '' }}</div></div>
+    </div>
+    <div v-if="view === 'locations' && rows.length" class="card corner">
+      <div class="sec-hd"><h3>当前页库位分布</h3><button class="btn-add" @click="$emit('business', 'location-maintain', {})">维护库位</button></div>
+      <div class="loc-grid"><button v-for="row in rows" :key="row.id" class="loc" :class="{ disabled: row.enabled === false || row.enabled === 0 }" :style="{ background: row.occupied ? '#F0A03A' : '#3BD48A' }" @click="$emit('business', 'locations', row)"><b>{{ row.code || '未配置库位' }}</b><span>{{ row.occupied ? '在用' : '空闲' }}</span></button></div>
+      <div class="legend-box">显示当前页 {{ rows.length }} 个真实库位;全部库位可通过分页查看。</div>
+    </div>
+    <div class="card corner" v-loading="loading">
+      <div v-if="view === 'todo'" class="tabs"><button class="tab" :class="{ on: query.type === 'inventory' }" @click="changeType('inventory')">盘点任务</button><button class="tab" @click="$emit('navigate', 'flow')">待我审批</button></div>
+      <div class="filters">
+        <label><span>仓库</span><select v-model="query.warehouseId"><option value="">全部授权仓库</option><option v-for="item in options.warehouses || []" :key="item.id" :value="item.id">{{ item.name }}</option></select></label>
+        <label v-if="!['todo','flow','locations'].includes(view)"><span>单位</span><select v-model="query.unit"><option value="">按单位分别展示</option><option v-for="item in options.units || []" :key="item.unit" :value="item.unit">{{ item.unit }}</option></select></label>
+        <label v-if="!['flow', 'todo', 'locations'].includes(view)"><span>物料</span><select v-model="query.categoryId"><option value="">全部</option><option v-for="item in options.categories || []" :key="item.id" :value="item.id">{{ item.name }}</option></select></label>
+        <label v-if="current.period && view !== 'flow'"><span>开始日期</span><input v-model="startDate" type="date"></label><label v-if="current.period && view !== 'flow'"><span>结束日期</span><input v-model="endDate" type="date"></label>
+        <label v-if="view === 'flow'"><span>任务到达月份</span><input v-model="flowMonth" type="month"></label>
+        <label v-if="view === 'io'"><span>方向</span><select v-model="query.direction"><option value="">全部</option><option value="1">入库</option><option value="2">出库</option></select></label>
+        <label v-if="view === 'age'"><span>库龄</span><select v-model="query.age"><option value="">全部</option><option value="0-30">0–30 天</option><option value="31-90">31–90 天</option><option value="91-180">91–180 天</option><option value="180+">180 天以上</option><option value="unknown">日期待核实</option></select></label>
+        <label v-if="view === 'alerts'"><span>预警类型</span><select v-model="query.level"><option value="">全部</option><option value="critical">缺料</option><option value="warning">偏低</option><option value="excess">超储</option><option value="unknown">未配置</option></select></label>
+        <label v-if="['ledger','qc'].includes(view)"><span>质检</span><select v-model="query.inspection"><option value="">全部</option><option value="passed">合格</option><option value="failed">不合格</option><option value="concession">让步接收</option><option value="pending">待检</option><option value="none">无需质检</option><option value="unknown">待核实</option></select></label>
+        <label><span>关键词</span><input v-model.trim="query.keyword" placeholder="物料 / 批次 / 单据" @keyup.enter="search"></label>
+        <button class="btn" @click="search">查询</button><button class="btn ghost" :disabled="exporting || !result.total" @click="$emit('export')">{{ exporting ? '导出中…' : '↓ 导出 Excel' }}</button>
+        <button v-if="view === 'io'" class="btn ghost" @click="$emit('navigate', 'addin')">新增入库单</button><button v-if="view === 'io'" class="btn ghost" @click="$emit('navigate', 'addout')">新增出库单</button>
+        <button v-if="view === 'dead'" class="btn ghost" @click="$emit('business', 'disposal-list', {})">处置申请与财务登记</button>
+        <button v-if="view === 'alerts'" class="btn ghost" @click="$emit('business', 'threshold', {})">配置安全库存</button>
+        <button v-if="view === 'flow'" class="btn ghost" :disabled="!selected.length" @click="$emit('batch', selected)">批量审批({{ selected.length }})</button>
+      </div>
+      <div v-if="error" class="data-note bad" role="alert">{{ error }} <button @click="$emit('retry')">重试</button></div>
+      <div class="table-wrap"><table class="tbl"><thead><tr><th v-if="view === 'flow'"><input type="checkbox" :checked="rows.length > 0 && selected.length === rows.length" aria-label="选择当前页全部任务" @change="selectAll($event.target.checked)"></th><th v-for="column in current.columns" :key="column.key" :class="{ 'num-r': column.numeric }">{{ column.name }}</th><th>操作</th></tr></thead><tbody>
+        <tr v-for="(row, index) in rows" :key="row.id || index" :class="{ 'overdue-row': row.overdue }"><td v-if="view === 'flow'"><input v-model="selected" type="checkbox" :value="row" :aria-label="'选择任务' + (row.businessCode || row.name)"></td><td v-for="column in current.columns" :key="column.key" :class="{ 'num-r': column.numeric }">{{ cell(row, column.key) }}<span v-if="column.key === 'dueTime' && row.overdue" class="badge-overdue">逾期</span></td><td class="row-actions"><a class="link" href="#" @click.prevent="$emit('business', view, row)">{{ current.action }}</a><a v-if="view === 'alerts'" class="link" href="#" @click.prevent="$emit('business', 'threshold', row)">配置阈值</a><a v-if="view === 'dead'" class="link" href="#" @click.prevent="$emit('business', 'ledger', row)">批次流水</a></td></tr>
+        <tr v-if="!loading && !rows.length"><td class="empty" :colspan="current.columns.length + (view === 'flow' ? 2 : 1)">{{ error ? '数据加载失败,请重试' : result.reason || (view === 'locations' ? '未配置库位' : '当前筛选无明细数据') }}</td></tr>
+      </tbody></table></div>
+      <div class="pager">共 {{ show(result.total) }} 条 · 每页 20 条 <button class="pg" :disabled="Number(filters.page || 1) <= 1" @click="page(-1)">‹</button><strong>{{ filters.page || 1 }}</strong><button class="pg" :disabled="!result.total || Number(filters.page || 1) * 20 >= result.total" @click="page(1)">›</button></div>
+    </div>
+  </section>
+</template>
+<script>
+import { displayNumber } from './dashboard-utils';
+import { screens, metricLabels } from './dashboard-screens';
+export default {
+  name: 'WarehouseDashboardDetail',
+  props: { view: String, filters: { type: Object, default: () => ({}) }, options: { type: Object, default: () => ({}) }, result: { type: Object, default: () => ({}) }, summary: { type: Object, default: () => ({}) }, error: String, loading: Boolean, exporting: Boolean },
+  data() { return { query: {}, selected: [], metricLabels }; },
+  computed: {
+    current() { return screens[this.view] || screens.ledger; }, rows() { return this.result.records || this.result.list || []; },
+    flowMonth: { get() { return (this.query.startTime || '').slice(0,7); },set(v) { this.$set(this.query,'startTime',v ? v + '-01 00:00:00' : ''); this.$set(this.query,'endTime',v ? v + '-' + new Date(Number(v.slice(0,4)),Number(v.slice(5,7)),0).getDate() + ' 23:59:59' : ''); } },
+    startDate: { get() { return (this.query.startTime || '').slice(0, 10); }, set(v) { this.$set(this.query, 'startTime', v ? v + ' 00:00:00' : ''); } },
+    endDate: { get() { return (this.query.endTime || '').slice(0, 10); }, set(v) { this.$set(this.query, 'endTime', v ? v + ' 23:59:59' : ''); } }
+  },
+  watch: { filters: { immediate: true, deep: true, handler(v) { this.query = { ...v }; } }, result() { this.selected = []; } },
+  methods: {
+    show: displayNumber,
+    search() { if (this.query.startTime && this.query.endTime && this.query.startTime > this.query.endTime) return this.$message.warning('开始日期不能晚于结束日期'); this.$emit('search', { ...this.query, page: 1 }); },
+    page(offset) { this.$emit('search', { ...this.filters, page: Number(this.filters.page || 1) + offset }); },
+    changeType(type) { this.$emit('search', { ...this.filters, type, page: 1 }); },
+    selectAll(checked) { this.selected = checked ? this.rows.slice() : []; },
+    cell(row, key) {
+      const value = key.split('.').reduce((v, k) => v == null ? null : v[k], row);
+      if (key === 'direction') return String(value) === '1' ? '入库' : String(value) === '2' ? '出库' : '待核实';
+      if (key === 'dueTime' && !value || key === 'firstInboundTime' && !value || key === 'ageDays' && value == null) return '日期待核实';
+      if (key === 'enabled') return value === true || value === 1 ? '启用' : value === false || value === 0 ? '停用' : '待核实';
+      if (key === 'qualityStatus') return { 0:'无需质检',1:'待检',2:'已检' }[value] || '待核实';
+      if (key === 'qualityResult') return { 1:'合格',2:'不合格',3:'让步接收' }[value] || '待核实';
+      if (key === 'status' && this.view === 'locations') return { 1:'空闲',2:'在用',3:'已满',4:'失效' }[value] || '待核实';
+      const labels = { ordinary:'普通',repair:'维修',temporary:'暂存',virtual:'虚拟',critical: '缺料', warning: '偏低', excess: '超储', passed: '合格', failed: '不合格', concession: '让步接收', pending: '待检', none: '无需质检', unknown: '待核实', in: '待入库', out: '待出库', central: '入总库', inventory: '盘点' };
+      if (['usage','qualityStatus','qualityResult','status','type'].includes(key) && labels[value]) return labels[value];
+      return typeof value === 'number' ? displayNumber(value) : value == null || value === '' ? '—' : String(value);
+    }
+  }
+};
+</script>

+ 6 - 20
src/views/warehouseManagement/index/index.vue

@@ -1,24 +1,10 @@
-<template>
-  <div>
-    <yuxin v-if="$hasPermission('wms:home:yuxin')"></yuxin>
-     
-    <common v-else></common>
-  </div>
-</template>
+<template><warehouse-dashboard /></template>
 
 <script>
-  import common from './common.vue';
-  import yuxin from './yuxin.vue';
+import WarehouseDashboard from './dashboard.vue';
 
-  export default {
-    components: { common, yuxin },
-    data() {
-      return {};
-    },
-    computed: {},
-
-    mounted() {}
-  };
+export default {
+  name: 'WmsHome',
+  components: { WarehouseDashboard }
+};
 </script>
-
-<style lang="scss" scoped></style>

+ 3 - 348
src/views/warehouseManagement/index/yuxin.vue

@@ -1,350 +1,5 @@
-<template>
-  <div id="my_index">
-    <div class="viewBar">
-      <div v-for="item in list">
-        <div class="item">
-          <div class="title"
-            >{{ item.title }}</div
-          >
-          <div class="count">{{ item.count }}<span>{{ item.unit }}</span></div>
-        </div>
-        <div class="icon">
-          <img :src="item.icon" />
-        </div>
-      </div>
-    </div>
-    <div>
-        <el-row :gutter="16" class="mb-10">
-            <el-col :span="12">
-                <el-card shadow="always" class="box-card">
-                    <div slot="header" class="clearfix">
-                        <span>场站月用煤量对比</span>
-                    </div>
-                    <div class="grid-content">
-                        <chart-bar-line
-                            :xAxisData="monthCoalData.xAxisData"
-                            :barSeries="monthCoalData.barSeries"
-                            barYAxisName="吨"
-                            height="280px"
-                        />
-                        <!-- :lineSeries="monthCoalData.lineSeries" -->
-                    </div>
-                </el-card>
-            </el-col>
-            <el-col :span="12">
-                <el-card shadow="always" class="box-card">
-                    <div slot="header" class="clearfix">
-                        <span>场站日用煤量对比</span>
-                    </div>
-                    <div class="grid-content">
-                        <chart-bar-line
-                            :xAxisData="dayCoalData.xAxisData"
-                            :barSeries="dayCoalData.barSeries"
-                            barYAxisName="吨"
-                            height="280px"
-                        />
-                        <!-- :lineSeries="dayCoalData.lineSeries" -->
-                    </div>
-                </el-card>
-            </el-col>
-        </el-row>
-        <el-row :gutter="16" class="mb-10">
-            <el-col :span="12">
-                <el-card shadow="always" class="box-card">
-                    <div slot="header" class="clearfix">
-                        <span>场站月拉灰量对比</span>
-                    </div>
-                    <div class="grid-content">
-                        <chart-bar-line
-                            :xAxisData="monthGreyData.xAxisData"
-                            :barSeries="monthGreyData.barSeries"
-                            barYAxisName="车"
-                            height="280px"
-                        />
-                        <!-- :lineSeries="monthGreyData.lineSeries" -->
-                    </div>
-                </el-card>
-            </el-col>
-            <el-col :span="12">
-                <el-card shadow="always" class="box-card">
-                    <div slot="header" class="clearfix">
-                        <span>场站日拉灰量对比</span>
-                    </div>
-                    <div class="grid-content">
-                        <chart-bar-line
-                            :xAxisData="dayGreyData.xAxisData"
-                            :barSeries="dayGreyData.barSeries"
-                            barYAxisName="车"
-                            height="280px"
-                        />
-                        <!-- :lineSeries="dayGreyData.lineSeries" -->
-                    </div>
-                </el-card>
-            </el-col>
-        </el-row>
-        <el-row :gutter="16" class="mb-10">
-            <el-col :span="12">
-                <el-card shadow="always" class="box-card">
-                    <div slot="header" class="clearfix">
-                        <span>场站月拉渣量对比</span>
-                    </div>
-                    <div class="grid-content">
-                        <chart-bar-line
-                            :xAxisData="monthSlagData.xAxisData"
-                            :barSeries="monthSlagData.barSeries"
-                            barYAxisName="车"
-                            height="280px"
-                        />
-                        <!-- :lineSeries="monthSlagData.lineSeries" -->
-                    </div>
-                </el-card>
-            </el-col>
-            <el-col :span="12">
-                <el-card shadow="always" class="box-card">
-                    <div slot="header" class="clearfix">
-                        <span>场站日拉渣量对比</span>
-                    </div>
-                    <div class="grid-content">
-                        <chart-bar-line
-                            :xAxisData="daySlagData.xAxisData"
-                            :barSeries="daySlagData.barSeries"
-                            barYAxisName="车"
-                            height="280px"
-                        />
-                         <!-- :lineSeries="daySlagData.lineSeries" -->
-                    </div>
-                </el-card>
-            </el-col>
-        </el-row>
-    </div>
-  </div>
-</template>
-
+<template><warehouse-dashboard /></template>
 <script>
-import indexApi from '@/api/main/index.js';
-import chartBarLine from '@/components/ChartBarLine/index.vue';
-
-  const  COAL_WAREHOUSE_MAP = {
-    '2008474572461486082': '5#场站',
-    '2064887500559134721': '6#场站',
-    '2064887643979165697': '7#场站'
-  };
-
-  const GREY_WAREHOUSE_MAP = {
-    '2008475053149696001': '5#场站',
-    '2008475191167463426': '6#场站',
-    '2008475309006434305': '7#场站'
-  };
-
-  const COAL_STATION_KEYS = ['2008474572461486082', '2064887500559134721', '2064887643979165697'];
-
-  const GREY_STATION_KEYS = ['2008475053149696001', '2008475191167463426', '2008475309006434305'];
-
-  // 各图表颜色配置
-  const CHART_COLORS = {
-    monthCoal:  ['#3876f1', '#f8cd5d', '#5cd07b'],
-    dayCoal:    ['#5e5e5e', '#f7cd5c', '#00a1f1'],
-    monthGrey:  ['#979797', '#5df9f5', '#5cd07b'],
-    dayGrey:    ['#98aa00', '#5df9f5', '#5cd07b'],
-    monthSlag:  ['#7ca7ff', '#fef538', '#b6b6b6'],
-    daySlag:    ['#39496b', '#ff7b00', '#b6b6b6']
-  };
-
-  export default {
-    components: { chartBarLine },
-    data() {
-      return {
-        list: [
-          {
-            title: '来煤累计数',
-            key: 'coalInbound',
-            count: '',
-            unit: '吨',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-          {
-            title: '用煤累计数',
-            unit: '吨',
-            key: 'coalConsumption',
-            count: '',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-          {
-            title: '煤库存数',
-            count: '',
-            unit: '吨',
-            key: 'coalInventory',
-            icon: require('../../../assets/index/Vector@2x.png')
-          },
-        ],
-        // 图表数据
-        monthCoalData: { xAxisData: [], barSeries: [], lineSeries: [] },
-        dayCoalData:   { xAxisData: [], barSeries: [], lineSeries: [] },
-        monthGreyData: { xAxisData: [], barSeries: [], lineSeries: [] },
-        dayGreyData:   { xAxisData: [], barSeries: [], lineSeries: [] },
-        monthSlagData: { xAxisData: [], barSeries: [], lineSeries: [] },
-        daySlagData:   { xAxisData: [], barSeries: [], lineSeries: [] }
-      };
-    },
-    computed: {},
-
-    mounted() {
-        this.init();
-        this.monthCoalStatistics();
-        this.dayCoalStatistics();
-        this.monthGreyStatistics();
-        this.dayGreyStatistics();
-        this.monthSlagStatistics();
-        this.daySlagStatistics();
-    },
-    methods: {
-        async init() {
-        let data = await indexApi.coalStatistics();
-        this.list.forEach((item) => {
-          item.count = data[item.key];
-        });
-      },
-      /* 将接口数据转为图表格式
-       * records: API 返回的数组
-       * timeKey: 时间字段 'month' | 'dayTime'
-       * colors: 颜色数组 [5#, 6#, 7#]
-       * stationKeys: 场站 ID 顺序数组
-       * warehouseMap: warehouseId → 场站名称映射
-       */
-      _formatChartData(records, timeKey, colors, stationKeys, warehouseMap) {
-        // 生成完整的时间轴(key 用于数据匹配,label 用于显示)
-        const now = new Date();
-        const slots = timeKey === 'month'
-          ? Array.from({ length: 12 }, (_, i) => {
-              const m = String(i + 1).padStart(2, '0');
-              return { key: `${now.getFullYear()}-${m}`, label: `${i + 1}月` };
-            })
-          : (() => {
-              const y = now.getFullYear();
-              const m = String(now.getMonth() + 1).padStart(2, '0');
-              const lastDay = new Date(y, +m, 0).getDate();
-              return Array.from({ length: lastDay }, (_, i) => {
-                const d = String(i + 1).padStart(2, '0');
-                return { key: `${y}-${m}-${d}`, label: `${i + 1}` };
-              });
-            })();
-        const timeKeys = slots.map((s) => s.key);
-        const xAxisData = slots.map((s) => s.label);
-        // 按时间分组汇总
-        const byTime = {};
-        records.forEach((r) => {
-          const t = r[timeKey];
-          if (!t) return;
-          if (!byTime[t]) byTime[t] = {};
-          byTime[t][r.warehouseId] = (byTime[t][r.warehouseId] || 0) + parseFloat(r.totalQuantity || 0);
-        });
-        const barSeries = [];
-        stationKeys.forEach((wid, idx) => {
-          const name = warehouseMap[wid];
-          const data = timeKeys.map((t) => +(byTime[t]?.[wid] || 0).toFixed(2));
-          const color = colors[idx];
-          barSeries.push({ name, data, color });
-        });
-        return { xAxisData, barSeries, lineSeries: [] };
-      },
-      // 宇信煤的月度统计
-      async monthCoalStatistics() {
-        const data = await indexApi.monthCoalStatistics();
-        this.monthCoalData = this._formatChartData(data, 'month', CHART_COLORS.monthCoal, COAL_STATION_KEYS, COAL_WAREHOUSE_MAP);
-      },
-      // 宇信煤的日统计
-      async dayCoalStatistics() {
-        const data = await indexApi.dayCoalStatistics();
-        this.dayCoalData = this._formatChartData(data, 'dayTime', CHART_COLORS.dayCoal, COAL_STATION_KEYS, COAL_WAREHOUSE_MAP);
-      },
-      // 宇信灰的月度统计
-      async monthGreyStatistics() {
-        const data = await indexApi.greyMonthStatistics();
-        this.monthGreyData = this._formatChartData(data, 'month', CHART_COLORS.monthGrey, GREY_STATION_KEYS, GREY_WAREHOUSE_MAP);
-      },
-      // 宇信灰的日统计
-      async dayGreyStatistics() {
-        const data = await indexApi.greyDayStatistics();
-        this.dayGreyData = this._formatChartData(data, 'dayTime', CHART_COLORS.dayGrey, GREY_STATION_KEYS, GREY_WAREHOUSE_MAP);
-      },
-      // 宇信渣的月度统计
-      async monthSlagStatistics() {
-        const data = await indexApi.slagMonthStatistics();
-        this.monthSlagData = this._formatChartData(data, 'month', CHART_COLORS.monthSlag, GREY_STATION_KEYS, GREY_WAREHOUSE_MAP);
-      },
-      // 宇信渣的日统计
-      async daySlagStatistics() {
-        const data = await indexApi.slagDayStatistics();
-        this.daySlagData = this._formatChartData(data, 'dayTime', CHART_COLORS.daySlag, GREY_STATION_KEYS, GREY_WAREHOUSE_MAP);
-      },
-    }
-  };
+import WarehouseDashboard from './dashboard.vue';
+export default { name: 'WmsYuxinHome', components: { WarehouseDashboard } };
 </script>
-
-<style lang="scss" scoped>
-#my_index {
-    // height: calc(100vh - 96px);
-    display: flex;
-    width: 100%;
-    padding: 10px;
-    flex-direction: column;
-    box-sizing: border-box;
-    // background-color: #dbdbdb;
-    .viewBar {
-      height: 90px;
-      width: 100%;
-      display: flex;
-      margin-bottom: 10px;
-      justify-content: space-around;
-      > div {
-        // flex: 1;
-        width: 25%;
-        margin-left: 10px;
-        background: linear-gradient(to right, #2d80ee 100%, #e9f2ff 50%);
-        border-radius: 8px;
-        display: flex;
-        padding: 0 20px;
-        box-sizing: border-box;
-        .item {
-          flex: 1;
-          display: flex;
-          flex-direction: column;
-          .title {
-            flex: 1;
-            display: flex;
-            align-items: center;
-            font-size: 16px;
-            color: rgba(255, 255, 255, 1);
-            > span {
-              font-size: 12px;
-            }
-          }
-          .count {
-            flex: 1;
-            display: flex;
-            align-items: center;
-            font-size: 28px;
-            color: rgba(255, 255, 255, 1);
-          }
-        }
-        .icon {
-          width: 50px;
-          height: 100%;
-          display: flex;
-          align-items: center;
-          justify-content: center;
-          > img {
-            height: 50px;
-            width: 50px;
-          }
-        }
-      }
-      > div:first-child {
-        margin-left: 0;
-      }
-    }
-    .mb-10 {
-        margin-bottom: 10px;
-    }
-  }
-</style>

+ 12 - 1
src/views/warehouseManagement/stockManagement/add.vue

@@ -943,6 +943,7 @@
       </div>
       <div class="storage_btn">
         <el-button
+          v-if="!batchPrepare"
           type="primary"
           :loading="saveLoading"
           @click="handleStorage('save')"
@@ -952,7 +953,7 @@
           type="primary"
           :loading="saveLoading"
           @click="handleStorage('Storage')"
-          >入库</el-button
+          >{{ batchPrepare ? '确认本单并核对下一单' : '入库' }}</el-button
         >
       </div>
     </el-card>
@@ -1027,6 +1028,7 @@
   export default {
     mixins: [tabMixins],
     name: 'stockManagementAdd',
+    props: { batchPrepare: { type: Boolean, default: false } },
     components: {
       selectType,
       WarehousingDialog,
@@ -1802,6 +1804,8 @@
               delete item.extInfo;
               return {
                 ...item,
+                // 批量收货保留申请明细身份,同物料多行不能只按物料匹配。
+                ...(this.batchPrepare ? { extInfo: { dashboardApplyDetailId: item.id } } : {}),
                 index,
                 batchNo,
                 id: '',
@@ -2103,6 +2107,7 @@
       },
       // 返回
       back() {
+        if (this.batchPrepare) return this.$emit('cancelled');
         this.$router.go(-1);
       },
       // 添加产品
@@ -2511,6 +2516,12 @@
             if (!obj.id && String(obj.bizType) === '2') {
               obj.extInfo.inboundRequestId = this.ensureInboundRequestId();
             }
+            // 批量收货先保留原表单校验后的完整明细,全部核对后统一调用收货入口。
+            if (this.batchPrepare) {
+              this.saveLoading = false;
+              this.$emit('prepared', obj);
+              return;
+            }
             let api = obj.id ? storageApi.update : storageApi.storage;
             api(obj)
               .then(async (res) => {

+ 11 - 3
src/views/warehouseManagement/warehouseApplication/index.vue

@@ -193,16 +193,24 @@
         ]
       };
     },
-    created() {},
+    created() { this.applyDashboardFilters(); },
+    watch: { '$route.query': { deep:true,handler() { this.applyDashboardFilters(); } } },
     methods: {
+      applyDashboardFilters() {
+        if (this.$route.query.dashboard !== '1') return;
+        const q = this.$route.query;
+        const where = { type:1,warehouseId:q.warehouseId || '',factoryId:q.factoryId || '',categoryId:q.categoryId || '',unit:q.unit || '',code:q.keyword || q.code || '',status:q.status == null ? '' : q.status,startTime:q.startTime || '',endTime:q.endTime || '',time:q.startTime && q.endTime ? [q.startTime.slice(0,10),q.endTime.slice(0,10)] : [] };
+        this.$nextTick(() => { this.$refs.search.where = { ...this.$refs.search.defaultWhere,...where }; this.reload(where); });
+      },
       datasource({ page, where, limit }) {
         if (where.time?.length) {
-          where.startTime = where.time[0];
-          where.endTime = where.time[1];
+          where.startTime = where.time[0].slice(0,10) + ' 00:00:00';
+          where.endTime = where.time[1].slice(0,10) + ' 23:59:59';
         }
         delete where.time;
         return storageApi.getStoreList({
           ...where,
+          ...(this.$route.query.dashboard === '1' ? { type:1 } : {}),
           pageNum: page,
           size: limit
         });

+ 16 - 1
src/views/warehouseManagement/warehouseDefinition/add.vue

@@ -617,6 +617,17 @@
                   <span v-else>{{ row.goodsAllocationCode }}</span>
                 </template></el-table-column
               >
+              <el-table-column label="用途" min-width="120" align="center">
+                <template slot-scope="{ row }">
+                  <el-select v-if="row.isEdit" v-model="row.allocationPurpose" placeholder="待核实">
+                    <el-option label="普通" value="ordinary" /><el-option label="维修" value="repair" /><el-option label="暂存" value="temporary" /><el-option label="虚拟" value="virtual" />
+                  </el-select>
+                  <span v-else>{{ { ordinary:'普通',repair:'维修',temporary:'暂存',virtual:'虚拟' }[row.allocationPurpose] || '待核实' }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="启停状态" min-width="110" align="center">
+                <template slot-scope="{ row }"><el-select v-if="row.isEdit" v-model="row.enabled" placeholder="待核实"><el-option label="启用" :value="1" /><el-option label="停用" :value="0" /></el-select><span v-else>{{ row.enabled === 1 ? '启用' : row.enabled === 0 ? '停用' : '待核实' }}</span></template>
+              </el-table-column>
               <el-table-column label="货位长(cm)" min-width="120" align="center">
                 <template slot-scope="scope">
                   <el-form-item
@@ -1306,6 +1317,8 @@
               const allocationCode = `${obj.code}-${rowNum}-${colNum}`;
 
               newAllocations.push({
+                allocationPurpose: 'ordinary',
+                enabled: 1,
                 warehouseAreaCode: obj.reservoirAreaCode,
                 warehouseAreaGoodsCode: obj.code,
                 goodsAllocationCode: allocationCode,
@@ -1837,7 +1850,9 @@
             weightLimit: item.goodsAllocationHigh,
             inUseAllocationNumber: item.weightLimit,
             volumeUnit: item.volumeUnit,
-            warehouseAreaGoodsCode: item.warehouseAreaGoodsCode
+            warehouseAreaGoodsCode: item.warehouseAreaGoodsCode,
+            allocationPurpose: item.allocationPurpose,
+            enabled: item.enabled
           };
         });
         let params = {

+ 5 - 1
src/views/warehouseManagement/warehouseDefinition/details.vue

@@ -271,6 +271,10 @@
     </el-card>
 
     <el-dialog :visible.sync="statusVisible" title="变更货位状态" width="400px">
+      <el-form label-width="80px">
+        <el-form-item label="用途"><el-select v-model="curLocation.allocationPurpose" placeholder="待核实"><el-option label="普通" value="ordinary" /><el-option label="维修" value="repair" /><el-option label="暂存" value="temporary" /><el-option label="虚拟" value="virtual" /></el-select></el-form-item>
+        <el-form-item label="启停状态"><el-select v-model="curLocation.enabled" placeholder="待核实"><el-option label="启用" :value="1" /><el-option label="停用" :value="0" /></el-select></el-form-item>
+      </el-form>
       <el-radio-group v-model="statusSet">
         <el-radio :label="1">正常</el-radio>
 
@@ -535,7 +539,7 @@
           this.$message.success('操作成功!');
           this.getInfo();
           this.statusVisible = false;
-        } catch (error) {}
+        } catch (error) { this.$message.error(error.message || '货位状态保存失败'); }
         this.changeLoading = false;
       },
       goodsInfoView({ id }, title) {