|
@@ -1,11 +1,18 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="wms-home">
|
|
<div class="wms-home">
|
|
|
<main class="main">
|
|
<main class="main">
|
|
|
- <home-overview v-if="view === 'home'" :summary="summary" :options="options" :warehouse-id="warehouseId" :unit="unit" :updated-at="updatedAt" :error="error" @open="open" @refresh="refresh" @business="business" @warehouse="setWarehouse" @unit="setUnit" />
|
|
|
|
|
|
|
+ <home-overview v-if="view === 'home'" :summary="summary" :options="options" :warehouse-id="warehouseId" :unit="unit" :updated-at="updatedAt" :error="error" @open="open" @refresh="refresh" @business="business" @warehouse="setWarehouse" @unit="setUnit" @navigate="navigate" />
|
|
|
<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>
|
|
<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" :stagnant-days="stagnantDays" @back="open('home')" @navigate="open" @search="search" @retry="loadDetail" @export="exportRows" @business="business" @batch="batchApprove" />
|
|
<dashboard-detail v-else :key="view" :view="view" :filters="filters" :options="options" :result="result" :summary="summary" :loading="loading" :error="error" :exporting="exporting" :stagnant-days="stagnantDays" @back="open('home')" @navigate="open" @search="search" @retry="loadDetail" @export="exportRows" @business="business" @batch="batchApprove" />
|
|
|
</main>
|
|
</main>
|
|
|
<dashboard-actions ref="actions" :options="options" :filters="filters" :alert-rows="alertRows" @success="refresh" @business="business" />
|
|
<dashboard-actions ref="actions" :options="options" :filters="filters" :alert-rows="alertRows" @success="refresh" @business="business" />
|
|
|
|
|
+ <!-- 首屏遮罩:看板要等 home-options + home-summary 都回来才有数字,之前是空卡片逐步填充,
|
|
|
|
|
+ 用户看到的是「没有数据」。这里盖住内容,加载完再显示;失败也会撤掉遮罩,露出 HomeOverview 的错误条与「重试」。
|
|
|
|
|
+ 只管首屏一次,不是每次 refresh 都盖(15 分钟定时刷新和筛选项切换不该闪遮罩)。 -->
|
|
|
|
|
+ <div v-if="homeLoading && view === 'home'" class="home-loading" role="status" aria-live="polite">
|
|
|
|
|
+ <i class="home-loading__spinner" aria-hidden="true"></i>
|
|
|
|
|
+ <span>正在加载首页数据…</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
<script>
|
|
<script>
|
|
@@ -27,7 +34,7 @@ export default {
|
|
|
name: 'WmsHome',
|
|
name: 'WmsHome',
|
|
|
components: { HomeOverview, DashboardDetail, DashboardActions },
|
|
components: { HomeOverview, DashboardDetail, DashboardActions },
|
|
|
data() { return {
|
|
data() { return {
|
|
|
- view: 'home', warehouseId: '', unit: '', filters: {}, options: {}, summary: {}, result: {}, updatedAt: '', error: '', loading: false, exporting: false, portalRoots: [], optionsLoaded: false, receiveQueue: [], preparedReceipts: [], receiving: false
|
|
|
|
|
|
|
+ view: 'home', warehouseId: '', unit: '', filters: {}, options: {}, summary: {}, result: {}, updatedAt: '', error: '', loading: false, exporting: false, portalRoots: [], optionsLoaded: false, receiveQueue: [], preparedReceipts: [], receiving: false, homeLoading: true
|
|
|
}; },
|
|
}; },
|
|
|
computed: {
|
|
computed: {
|
|
|
// 呆滞阈值向下透传:优先用首页已请求到的同一份 home-summary.stagnantDays(不额外发请求),
|
|
// 呆滞阈值向下透传:优先用首页已请求到的同一份 home-summary.stagnantDays(不额外发请求),
|
|
@@ -56,15 +63,21 @@ export default {
|
|
|
this.readRoute();
|
|
this.readRoute();
|
|
|
try {
|
|
try {
|
|
|
this.options = await homeOptions({ warehouseId: this.warehouseId }); this.optionsLoaded = true;
|
|
this.options = await homeOptions({ warehouseId: this.warehouseId }); this.optionsLoaded = true;
|
|
|
|
|
+ // 默认单位来自筛选项(后缀 defaultUnit),第一条 readRoute 时 options 还没到,这里必须再兜一次。
|
|
|
|
|
+ this.applyDefaultUnit(Object.prototype.hasOwnProperty.call(this.$route.query, 'unit'));
|
|
|
this.filters = { ...this.filters, unit: this.unit };
|
|
this.filters = { ...this.filters, unit: this.unit };
|
|
|
await this.refresh();
|
|
await this.refresh();
|
|
|
} catch (e) { this.error = e.message || '筛选项加载失败,请刷新重试'; }
|
|
} catch (e) { this.error = e.message || '筛选项加载失败,请刷新重试'; }
|
|
|
|
|
+ // 首屏数据(筛选项 + 汇总)已经落定或失败,撤掉遮罩;失败时由 HomeOverview 的错误条 + 重试接手。
|
|
|
|
|
+ this.homeLoading = false;
|
|
|
this.startTimers();
|
|
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(); },
|
|
|
|
|
|
|
+ // 深色主题只由页面自身的 .wms-home 承担(见 dashboard-element.scss),不再往 body 挂全局 class。
|
|
|
|
|
+ // 原来的 body class 依赖路由销毁时机与 $route.query.dashboard 判断,而下钻目标页同样带着 dashboard=1,
|
|
|
|
|
+ // 一旦判断落空就把深色主题留给了下一个页签,业务页因此变成深蓝背景。
|
|
|
|
|
+ activated() { const resumed = !this.active; this.active = true; this.startTimers(); if (resumed) this.refresh(); },
|
|
|
deactivated() { this.stop(); },
|
|
deactivated() { this.stop(); },
|
|
|
- beforeDestroy() { this.stop(); if (this.$route.query.dashboard !== '1') document.body.classList.remove('wms-dashboard-theme'); },
|
|
|
|
|
|
|
+ beforeDestroy() { this.stop(); },
|
|
|
methods: {
|
|
methods: {
|
|
|
readRoute() {
|
|
readRoute() {
|
|
|
const query = this.$route.query;
|
|
const query = this.$route.query;
|
|
@@ -73,10 +86,29 @@ export default {
|
|
|
this.filters = filterKeys.reduce((r,key) => { if (query[key] != null) r[key] = query[key]; return r; }, { ...monthRange(), page: 1 });
|
|
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';
|
|
if (this.view === 'todo' && !this.filters.type) this.filters.type = 'inventory';
|
|
|
this.warehouseId = this.filters.warehouseId || ''; this.unit = this.filters.unit || '';
|
|
this.warehouseId = this.filters.warehouseId || ''; this.unit = this.filters.unit || '';
|
|
|
|
|
+ if (this.view === 'home') this.applyDefaultUnit(Object.prototype.hasOwnProperty.call(query, 'unit'));
|
|
|
|
|
+ },
|
|
|
|
|
+ // 首页默认基本单位:多基本单位下不选单位,库存总量/周转天数/库龄/分类分布会整片为空,
|
|
|
|
|
+ // 用户进首页看到的就是「没有数据」。优先用后端 home-options 的 defaultUnit(已按授权仓库算好),
|
|
|
|
|
+ // 缺失时回退 units 里的第一个非空单位。
|
|
|
|
|
+ defaultUnit() {
|
|
|
|
|
+ const options = this.options || {};
|
|
|
|
|
+ if (options.defaultUnit) return String(options.defaultUnit);
|
|
|
|
|
+ const rows = Array.isArray(options.units) ? options.units : [];
|
|
|
|
|
+ const first = rows.find((row) => row && row.unit != null && String(row.unit).trim() !== '');
|
|
|
|
|
+ return first ? String(first.unit) : '';
|
|
|
|
|
+ },
|
|
|
|
|
+ // 只在用户没表态时兜底:URL 里没有 unit 参数才算没表态,显式 unit=(全部单位)保持不动。
|
|
|
|
|
+ applyDefaultUnit(explicit) {
|
|
|
|
|
+ if (explicit || this.unit) return;
|
|
|
|
|
+ const unit = this.defaultUnit();
|
|
|
|
|
+ if (!unit) return;
|
|
|
|
|
+ this.unit = unit;
|
|
|
|
|
+ this.filters = { ...this.filters, unit };
|
|
|
},
|
|
},
|
|
|
route(view, filters, replace = false) {
|
|
route(view, filters, replace = false) {
|
|
|
const query = cleanQuery({ ...filters, unit: filters.unit || '', dashboard: '1', dashboardView: view });
|
|
const query = cleanQuery({ ...filters, unit: filters.unit || '', dashboard: '1', dashboardView: view });
|
|
|
- // 显式保留空单位,返回时不再自动选入默认单位。
|
|
|
|
|
|
|
+ // 单位始终写进 query:空值也要显式落成 unit=,代表用户主动选了「全部单位」,下钻/返回时不能被默认单位覆盖。
|
|
|
query.unit = filters.unit || '';
|
|
query.unit = filters.unit || '';
|
|
|
if (this.$route.query.dashboardView === view && JSON.stringify(query) === JSON.stringify(this.$route.query)) return;
|
|
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); });
|
|
this.$router[replace ? 'replace' : 'push']({ path: this.$route.path, query }).catch((e) => { if (e.name !== 'NavigationDuplicated') this.$message.error(e.message); });
|
|
@@ -96,11 +128,16 @@ export default {
|
|
|
if (view === 'ioapp' && !filters.direction) filters.direction = 'in';
|
|
if (view === 'ioapp' && !filters.direction) filters.direction = 'in';
|
|
|
if (['ioapp','inapp'].includes(view)) {
|
|
if (['ioapp','inapp'].includes(view)) {
|
|
|
filters.status = Object.prototype.hasOwnProperty.call(extra,'status') ? extra.status : '0';
|
|
filters.status = Object.prototype.hasOwnProperty.call(extra,'status') ? extra.status : '0';
|
|
|
|
|
+ // 出入库申请/入总库申请按单位过滤会把申请单筛空,这两个视图不继承首页单位。
|
|
|
delete filters.categoryId; filters.unit = ''; Object.assign(filters,monthRange());
|
|
delete filters.categoryId; filters.unit = ''; Object.assign(filters,monthRange());
|
|
|
}
|
|
}
|
|
|
|
|
+ // 回到首页时把单位补回来:上面的业务视图清了 unit,不补就会落到「全部单位」,KPI 又变成没有数据。
|
|
|
|
|
+ if (view === 'home' && this.view !== 'home' && !filters.unit) filters.unit = this.defaultUnit();
|
|
|
this.route(view, filters);
|
|
this.route(view, filters);
|
|
|
},
|
|
},
|
|
|
- setWarehouse(warehouseId) { this.route('home', { ...this.filters, warehouseId, unit: '', page: 1 }); },
|
|
|
|
|
|
|
+ // 切换仓库不再清空基本单位:单位列表是「全部授权仓库」口径,与单仓无关;
|
|
|
|
|
+ // 清空后落到「全部单位」会立刻变成没有数据,正是要避免的情况。
|
|
|
|
|
+ setWarehouse(warehouseId) { this.route('home', { ...this.filters, warehouseId, page: 1 }); },
|
|
|
setUnit(unit) { this.route('home', { ...this.filters, unit, page: 1 }); },
|
|
setUnit(unit) { this.route('home', { ...this.filters, unit, page: 1 }); },
|
|
|
search(filters) { this.route(this.view, filters); },
|
|
search(filters) { this.route(this.view, filters); },
|
|
|
parameters(extra = {}) { return cleanQuery({ ...this.filters, ...extra }); },
|
|
parameters(extra = {}) { return cleanQuery({ ...this.filters, ...extra }); },
|
|
@@ -181,7 +218,13 @@ export default {
|
|
|
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');
|
|
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; }
|
|
} 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' }) }); },
|
|
|
|
|
|
|
+ // 下钻到系统原有业务页(详情、批次)时只带目标页真正需要的参数:
|
|
|
|
|
+ // 不再带 dashboard=1(该参数只服务看板自身的内部视图:tgTable 批量收货勾选、warehouseApplication 批量审批),
|
|
|
|
|
+ // 也不再整包透传首页筛选(startTime/endTime/page/unit/categoryId/warehouseId 详情页一个都不认,
|
|
|
|
|
+ // 只会把 URL 撑长、让人误以为详情被筛过)。unknown/null 参数由 cleanQuery 丢弃,缺 planId 的旧链接
|
|
|
|
|
+ // 仍由 PlanServiceImpl#getOrderInfoByPlanId 反查兼容。
|
|
|
|
|
+ // 同时吞掉 NavigationDuplicated:重复点同一行不该弹错误提示。
|
|
|
|
|
+ navigate(path, query = {}) { this.$router.push({ path, query: cleanQuery({ ...query }) }).catch((e) => { if (e.name !== 'NavigationDuplicated') this.$message.error(e.message); }); },
|
|
|
async portal(predicate, query) {
|
|
async portal(predicate, query) {
|
|
|
try {
|
|
try {
|
|
|
if (!this.portalRoots.length) { const user = getCurrentUser(); this.portalRoots = await getResourcesTree({ groupId:user.currentGroupId,roleId:user.currentRoleId }); }
|
|
if (!this.portalRoots.length) { const user = getCurrentUser(); this.portalRoots = await getResourcesTree({ groupId:user.currentGroupId,roleId:user.currentRoleId }); }
|
|
@@ -211,10 +254,12 @@ export default {
|
|
|
return this.portal((n) => /purchasingManage\/purchaseNeedManage/.test((n.component || '') + (n.path || '')), { requirementId, requirementCode:row.purchaseRequestCode || row.requirementCode, code:row.purchaseRequestCode, dashboard:'1' });
|
|
return this.portal((n) => /purchasingManage\/purchaseNeedManage/.test((n.component || '') + (n.path || '')), { requirementId, requirementCode:row.purchaseRequestCode || row.requirementCode, code:row.purchaseRequestCode, 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 === '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' && row.activityType === 'inventory') return this.navigate('/warehouseManagement/stocktaking/work/details',{ id:row.orderId });
|
|
|
|
|
|
|
+ // 盘点工单详情同时要 planOrderId(=id) 与 planId:只带 id 时详情页拿不到计划信息(服务端虽能由工单反查,
|
|
|
|
|
+ // 但 URL 与列表页入口不一致,排查时两套地址对不上)。planId 由首页 detail/recentActivities 的查询列下发。
|
|
|
|
|
+ if (kind === 'io' && row.activityType === 'inventory') return this.navigate('/warehouseManagement/stocktaking/work/details',{ id:row.orderId, planId:row.planId });
|
|
|
if (kind === 'io' && row.activityType === 'transfer') return this.open('io',{ warehouseId:row.warehouseId,keyword:row.orderNo });
|
|
if (kind === 'io' && row.activityType === 'transfer') return this.open('io',{ warehouseId:row.warehouseId,keyword:row.orderNo });
|
|
|
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 === '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 === 'todo') return row.type === 'inventory' ? this.navigate('/warehouseManagement/stocktaking/work/details',{ id:row.id, planId:row.planId }) : this.open(row.type === 'central' ? 'inapp' : 'ioapp',{ direction:row.type === 'out' ? 'out' : 'in',keyword:row.orderNo });
|
|
|
if (kind === 'turnover') return this.open('stockLedger');
|
|
if (kind === 'turnover') return this.open('stockLedger');
|
|
|
if (['batch','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 (['batch','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 });
|
|
if (kind === 'disposal-document') return this.navigate(row.documentType === 'ALLOT' ? '/warehouseManagement/inventoryAllocation/details' : '/warehouseManagement/outgoingManagement/details',{ id:row.documentId });
|