feat(stocktake): 抽盘库位选择改为左右双栏,已选明细可逐个移除
原库位树是单列、层级很深,用户勾完看不出到底选了哪些库位。 现改为左右双栏: - 左 60%:el-tree(勾选),@check 触发 syncSelectedPaths - 右 40%:已选明细面板 —— 顶部「已选 N 个库位」+ 清空按钮, el-scrollbar 内平铺 el-tag 展示完整路径(等宽字体) 双向互动:点击右侧标签的 × 会调 treeRef.setChecked(path, false, true) 同步取消左侧勾选后重算列表。注意 el-tree 的 setCheckedKeys / setChecked 都**不会**触发 @check 事件,因此这两处都显式调用了 syncSelectedPaths(), 保证左右状态强同步(获取推荐后右侧也会立刻渲染)。 ★ 右侧刻意**不过滤非末级节点**,展示的就是提交给后端的 scope_config.locations 全集。原因:后端按 warehouse_location 精确匹配,而库存的库位可能落在层级 1/2 (实测 level1 有 9 个、level2 有 57 个),只提交叶子路径会把这些库存静默排除 在抽盘范围之外。面板所见 = 实际范围,才谈得上「强同步」。 配套:抽盘配置区是双栏、400px 窄容器装不下,故在展开该配置时把欢迎页 放宽到 780px(.idle-content.is-wide),其他状态维持原宽度。
This commit is contained in:
@ -2,7 +2,7 @@
|
|||||||
<div class="app-container mobile-optimized">
|
<div class="app-container mobile-optimized">
|
||||||
|
|
||||||
<el-card v-if="!isSessionActive" class="main-card idle-card" shadow="never">
|
<el-card v-if="!isSessionActive" class="main-card idle-card" shadow="never">
|
||||||
<div class="idle-content">
|
<div class="idle-content" :class="{ 'is-wide': idleWide }">
|
||||||
<div class="icon-area">
|
<div class="icon-area">
|
||||||
<el-icon :size="80" color="#409EFF"><VideoPlay /></el-icon>
|
<el-icon :size="80" color="#409EFF"><VideoPlay /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@ -119,21 +119,52 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-tree
|
<!-- 左:库位树(勾选) | 右:已选明细(可关闭,与左侧强同步) -->
|
||||||
ref="locTreeRef"
|
<div class="scope-columns">
|
||||||
v-loading="treeLoading"
|
<div class="scope-col-left">
|
||||||
class="scope-tree"
|
<el-tree
|
||||||
:data="locTreeData"
|
ref="locTreeRef"
|
||||||
node-key="full_path"
|
v-loading="treeLoading"
|
||||||
show-checkbox
|
class="scope-tree"
|
||||||
default-expand-all
|
:data="locTreeData"
|
||||||
:props="{ label: 'name', children: 'children' }"
|
node-key="full_path"
|
||||||
@check="updateCheckedLocCount"
|
show-checkbox
|
||||||
/>
|
default-expand-all
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
@check="syncSelectedPaths"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="scope-selected">
|
<div class="scope-col-right">
|
||||||
已选 <strong>{{ checkedLocCount }}</strong> 个库位
|
<div class="scope-selected-header">
|
||||||
<el-button link type="primary" size="small" @click="resetScopeSelection">清空</el-button>
|
<span>已选 <strong>{{ checkedLocCount }}</strong> 个库位</span>
|
||||||
|
<el-button
|
||||||
|
v-if="checkedLocCount"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="resetScopeSelection"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar class="scope-selected-scroll">
|
||||||
|
<div v-if="!selectedLocationPaths.length" class="scope-selected-empty">
|
||||||
|
尚未选择库位<br />可点「获取推荐」自动勾选,或在左侧树上手动勾选
|
||||||
|
</div>
|
||||||
|
<el-tag
|
||||||
|
v-for="p in selectedLocationPaths"
|
||||||
|
:key="p"
|
||||||
|
closable
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
class="scope-loc-tag"
|
||||||
|
@close="uncheckLocation(p)"
|
||||||
|
>
|
||||||
|
{{ p }}
|
||||||
|
</el-tag>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="creatingNew" class="task-form-warn">
|
<div v-if="creatingNew" class="task-form-warn">
|
||||||
@ -728,7 +759,19 @@ const recLoading = ref(false) // 获取推荐中
|
|||||||
const treeLoading = ref(false) // 库位树加载中
|
const treeLoading = ref(false) // 库位树加载中
|
||||||
const locTreeData = ref<any[]>([]) // 库位树(已按当前公司前缀过滤)
|
const locTreeData = ref<any[]>([]) // 库位树(已按当前公司前缀过滤)
|
||||||
const locTreeRef = ref() // el-tree 实例,用于 setCheckedKeys / getCheckedNodes
|
const locTreeRef = ref() // el-tree 实例,用于 setCheckedKeys / getCheckedNodes
|
||||||
const checkedLocCount = ref(0) // 当前勾选的库位数(含父节点,实时展示避免误判规模)
|
// ★ 当前勾选的库位路径集合 —— 它与提交给后端的 scope_config.locations 完全一致。
|
||||||
|
// 刻意**不过滤非末级节点**:后端按 warehouse_location 精确匹配,而库存的库位
|
||||||
|
// 可能落在层级 1/2(实测 level1 有 9 个、level2 有 57 个),只提交叶子会把
|
||||||
|
// 这些库存静默排除在抽盘范围之外。
|
||||||
|
const selectedLocationPaths = ref<string[]>([])
|
||||||
|
const checkedLocCount = computed(() => selectedLocationPaths.value.length)
|
||||||
|
|
||||||
|
// 创建表单当前是否可见(与模板里 v-else 分支的条件保持一致)
|
||||||
|
const showCreateForm = computed(() =>
|
||||||
|
!!selectedCompany.value && !probeFailed.value && !(activeSession.value && !creatingNew.value)
|
||||||
|
)
|
||||||
|
// 抽盘配置区是左右双栏,400px 的窄容器装不下,此时把欢迎页放宽
|
||||||
|
const idleWide = computed(() => showCreateForm.value && newScopeType.value === 'active')
|
||||||
|
|
||||||
// 库位树按公司前缀过滤(IRIS 只看 Y,LICA 看 C/L;未配置的公司不过滤)
|
// 库位树按公司前缀过滤(IRIS 只看 Y,LICA 看 C/L;未配置的公司不过滤)
|
||||||
const filterTreeByCompany = (nodes: any[], company: string) => {
|
const filterTreeByCompany = (nodes: any[], company: string) => {
|
||||||
@ -755,9 +798,16 @@ const loadLocationTree = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCheckedLocCount = () => {
|
// 树的勾选状态 → 响应式路径集合。唯一数据源,左右两侧都读它
|
||||||
|
const syncSelectedPaths = () => {
|
||||||
const nodes = locTreeRef.value?.getCheckedNodes() || []
|
const nodes = locTreeRef.value?.getCheckedNodes() || []
|
||||||
checkedLocCount.value = nodes.length
|
selectedLocationPaths.value = nodes.map((n: any) => n.full_path).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点右侧标签的 × → 同步取消左侧树上对应的勾选
|
||||||
|
const uncheckLocation = (path: string) => {
|
||||||
|
locTreeRef.value?.setChecked(path, false, true)
|
||||||
|
syncSelectedPaths()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取推荐并把它们在树上自动勾选
|
// 取推荐并把它们在树上自动勾选
|
||||||
@ -785,13 +835,12 @@ const fetchRecommendLocations = async () => {
|
|||||||
|
|
||||||
// false = 不触发子节点级联之外的行为,按 el-tree 默认规则勾选
|
// false = 不触发子节点级联之外的行为,按 el-tree 默认规则勾选
|
||||||
locTreeRef.value?.setCheckedKeys(locs, false)
|
locTreeRef.value?.setCheckedKeys(locs, false)
|
||||||
updateCheckedLocCount()
|
// ★ 右侧明细与左侧树同源,勾完立即同步,保证两侧永远一致
|
||||||
|
syncSelectedPaths()
|
||||||
|
|
||||||
// 推荐里有、但树上勾不到的(不在当前公司前缀范围内等)要如实告知,
|
// 推荐里有、但树上勾不到的(不在当前公司前缀范围内等)要如实告知,
|
||||||
// 否则这些库位会静默落选 —— 工人以为盘到了,其实没进范围
|
// 否则这些库位会静默落选 —— 工人以为盘到了,其实没进范围
|
||||||
const checkedPaths = new Set(
|
const checkedPaths = new Set(selectedLocationPaths.value)
|
||||||
(locTreeRef.value?.getCheckedNodes() || []).map((n: any) => n.full_path)
|
|
||||||
)
|
|
||||||
const missing = locs.filter(l => !checkedPaths.has(l))
|
const missing = locs.filter(l => !checkedPaths.has(l))
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
ElMessage.warning(`推荐中 ${missing.length} 个库位不在当前公司的库位树上,未能勾选`)
|
ElMessage.warning(`推荐中 ${missing.length} 个库位不在当前公司的库位树上,未能勾选`)
|
||||||
@ -816,10 +865,10 @@ const buildScopeConfig = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换回全仓时清空树上的勾选,避免残留到下一次抽盘
|
// 清空勾选(切换回全仓、或点「清空」按钮)
|
||||||
const resetScopeSelection = () => {
|
const resetScopeSelection = () => {
|
||||||
locTreeRef.value?.setCheckedKeys([], false)
|
locTreeRef.value?.setCheckedKeys([], false)
|
||||||
checkedLocCount.value = 0
|
syncSelectedPaths()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(newScopeType, (val) => {
|
watch(newScopeType, (val) => {
|
||||||
@ -1719,6 +1768,8 @@ const goToVarianceReview = () => {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.idle-content { width: 100%; max-width: 400px; padding: 20px; }
|
.idle-content { width: 100%; max-width: 400px; padding: 20px; }
|
||||||
|
/* 抽盘配置区是双栏,窄容器装不下,此时放宽欢迎页 */
|
||||||
|
.idle-content.is-wide { max-width: 780px; }
|
||||||
.app-title { font-size: 24px; margin-bottom: 10px; color: #303133; }
|
.app-title { font-size: 24px; margin-bottom: 10px; color: #303133; }
|
||||||
.subtitle { color: #909399; margin-bottom: 40px; }
|
.subtitle { color: #909399; margin-bottom: 40px; }
|
||||||
.icon-area { margin-bottom: 20px; }
|
.icon-area { margin-bottom: 20px; }
|
||||||
@ -1818,9 +1869,27 @@ const goToVarianceReview = () => {
|
|||||||
}
|
}
|
||||||
.scope-recommend-text { font-size: 12px; color: #606266; }
|
.scope-recommend-text { font-size: 12px; color: #606266; }
|
||||||
.scope-topn-input { width: 100px; }
|
.scope-topn-input { width: 100px; }
|
||||||
|
|
||||||
|
/* 左右双栏:左 60% 库位树 | 右 40% 已选明细 */
|
||||||
|
.scope-columns {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.scope-col-left { flex: 0 0 60%; max-width: 60%; min-width: 0; }
|
||||||
|
.scope-col-right {
|
||||||
|
flex: 1 1 40%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
/* 平板大屏:定高 + 滚动,避免树把表单撑得过长 */
|
/* 平板大屏:定高 + 滚动,避免树把表单撑得过长 */
|
||||||
.scope-tree {
|
.scope-tree {
|
||||||
max-height: 260px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #ebeef5;
|
||||||
@ -1829,15 +1898,30 @@ const goToVarianceReview = () => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
--el-tree-node-content-height: 30px;
|
--el-tree-node-content-height: 30px;
|
||||||
}
|
}
|
||||||
.scope-selected {
|
.scope-selected-header {
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #606266;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid #f2f3f5;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.scope-selected-header strong { color: #409eff; font-size: 14px; }
|
||||||
|
.scope-selected-scroll { flex: 1 1 auto; height: 260px; }
|
||||||
|
.scope-selected-empty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
line-height: 1.8;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
/* 已选库位标签:完整路径平铺,点 × 同步取消左侧勾选 */
|
||||||
|
.scope-loc-tag {
|
||||||
|
margin: 0 4px 4px 0;
|
||||||
|
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
.scope-selected strong { color: #409eff; font-size: 14px; }
|
|
||||||
|
|
||||||
/* 场景 A 的进度提示与次要入口 */
|
/* 场景 A 的进度提示与次要入口 */
|
||||||
.active-session-tip {
|
.active-session-tip {
|
||||||
|
|||||||
Reference in New Issue
Block a user