feat(stocktake): 欢迎页改为创建盘点任务表单

无活跃会话时不再只有一个孤零零的按钮,而是先配置再开始:

- 盘点模式:明盘 / 盲盘,默认明盘
- 盘点范围:全仓盘点 / 活跃库位抽盘

有活跃会话时表单隐藏,显示绿色「加入当前盘点」,提示语带出该会话的
模式与范围(如「盲盘 · 全仓盘点,已扫 12 件,发起人:韩善龙」)。
次要入口「开启新一轮盘点」改为展开配置表单,并在表单内提示
「开启新一轮会把当前进行中的盘点置为已结束(历史数据仍保留)」,
提交时仍走原有的 10 秒确认弹窗。

「活跃库位抽盘」选项置灰并注明原因:范围过滤尚未实现,此时若放开,
结束盘点会把抽盘范围外的全部库存批量标成盘亏。后端同样以 400 拦截,
前后端一致,避免绕过 UI 直接调接口。

activeSession 类型与 checkServerDraft 同步补上 mode / scope_type。
This commit is contained in:
yueli
2026-09-11 13:28:53 +08:00
parent 3f964614e3
commit 091847596e

View File

@ -46,8 +46,8 @@
</el-button>
</template>
<!-- 场景 A该公司已有进行中的盘点 绿色加入 -->
<template v-else-if="activeSession">
<!-- 场景 A已有进行中的盘点且未主动选择开启新一轮 绿色加入 -->
<template v-else-if="activeSession && !creatingNew">
<el-button
type="success"
size="large"
@ -58,31 +58,47 @@
加入当前盘点
</el-button>
<div class="active-session-tip">
当前公司已有盘点在进行中已扫 {{ activeSession.scanned }} <template v-if="activeSession.initiator">发起人{{ activeSession.initiator }}</template>
{{ modeLabel(activeSession.mode) }} · {{ scopeLabel(activeSession.scope_type) }}已扫 {{ activeSession.scanned }} <template v-if="activeSession.initiator">发起人{{ activeSession.initiator }}</template>
</div>
<!-- 次要入口缺了它本司一旦有过草稿就永远开不了新一轮 -->
<el-button
link
type="primary"
class="secondary-entry"
@click="startNewSession"
:loading="btnLoading"
>
<!-- 次要入口缺了它本司一旦有过会话就永远开不了新一轮 -->
<el-button link type="primary" class="secondary-entry" @click="creatingNew = true">
开启新一轮盘点
</el-button>
</template>
<!-- 场景 B该公司当前无盘点 蓝色开始新盘点 -->
<el-button
v-else
type="primary"
size="large"
class="action-btn-full"
@click="startNewSession"
:loading="btnLoading"
>
开始新盘点
</el-button>
<!-- 场景 B创建盘点任务无活跃会话或主动开启新一轮 -->
<template v-else>
<div class="task-form">
<div class="task-form-row">
<span class="task-form-label">盘点模式</span>
<el-radio-group v-model="newMode" size="large">
<el-radio-button value="open">明盘</el-radio-button>
<el-radio-button value="blind">盲盘</el-radio-button>
</el-radio-group>
</div>
<div class="task-form-row">
<span class="task-form-label">盘点范围</span>
<el-radio-group v-model="newScopeType" size="large">
<el-radio-button value="full">全仓盘点</el-radio-button>
<!-- 抽盘的范围过滤尚未实现此时开放会导致把范围外库存批量标成盘亏 -->
<el-radio-button value="active" disabled>活跃库位抽盘</el-radio-button>
</el-radio-group>
</div>
<div class="task-form-hint">
{{ newMode === 'blind' ? '盲盘:作业时不显示账面数与差异' : '明盘:作业时可查看账面数与差异' }}
</div>
<div v-if="creatingNew" class="task-form-warn">
开启新一轮会把当前进行中的盘点置为已结束历史数据仍保留
</div>
</div>
<el-button type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
开始新盘点
</el-button>
<el-button v-if="creatingNew" link class="secondary-entry" @click="creatingNew = false">
返回继续当前盘点
</el-button>
</template>
</div>
<div class="safe-tip">
@ -594,6 +610,8 @@ const companyLoading = ref(false)
// 该公司的活跃会话;无进行中的盘点则为 null
const activeSession = ref<{
session_id: string
mode: string | null // open(明盘) / blind(盲盘)
scope_type: string | null // full(全仓) / active(抽盘)
total: number // 该会话草稿行数
scanned: number // 实际扫到的件数(按物料去重,不含系统自动漏盘)
last_scan_time: string | null
@ -621,6 +639,16 @@ const companyHint = computed(() => {
return `已锁定为您所属公司:${selectedCompany.value}`
})
// ★ 创建盘点任务的配置项(仅在无活跃会话、或主动选择「开启新一轮」时展示)
const creatingNew = ref(false) // 已有活跃会话时,是否展开创建表单
const newMode = ref<'open' | 'blind'>('open') // 盘点模式,默认明盘
const newScopeType = ref<'full' | 'active'>('full') // 盘点范围,默认全仓
const MODE_LABELS: Record<string, string> = { open: '明盘', blind: '盲盘' }
const SCOPE_LABELS: Record<string, string> = { full: '全仓盘点', active: '活跃库位抽盘' }
const modeLabel = (m?: string | null) => (m ? MODE_LABELS[m] || m : '')
const scopeLabel = (s?: string | null) => (s ? SCOPE_LABELS[s] || s : '')
const persistSession = (sessionId: string) => {
currentSessionId.value = sessionId || ''
if (sessionId) {
@ -713,12 +741,12 @@ const api = {
params: withCompany(), // ★ 公司走 query stringbody 里的会被后端忽略
data: { ...data, session_id: currentSessionId.value }
}),
// ★ 新增: 开始新会话
startNewSession: () => request({
// ★ 新增: 开始新会话(携带盘点模式与范围配置,由后端落库到 stocktake_session
startNewSession: (payload: { mode: string; scope_type: string }) => request({
url: '/v1/inbound/stock/draft/start-new',
method: 'post',
params: withCompany(),
data: {}
data: payload
}),
// ★ 新增: 结束盘点
finishStocktake: () => request({
@ -818,6 +846,8 @@ const checkServerDraft = async () => {
activeSession.value = data.session_id
? {
session_id: data.session_id,
mode: data.mode || null,
scope_type: data.scope_type || null,
total: data.total || 0,
scanned: data.scanned || 0,
last_scan_time: data.last_scan_time || null,
@ -835,6 +865,7 @@ const checkServerDraft = async () => {
// 进入扫码页的公共收尾:落到 scanning 阶段并拉取盘点基数
const enterScanning = async (message: string) => {
isSessionActive.value = true
creatingNew.value = false
localStorage.setItem('stocktake_phase', 'scanning')
// 最近扫码流水属于「本次作业」,换会话/重新加入都要清空,避免看到上一个会话的残留
recentScans.value = []
@ -872,13 +903,17 @@ const doStartNewSession = async () => {
try {
// ★ 先清掉旧会话,避免签发失败时残留上一个 session_id
clearPersistedSession()
// 后端签发新 session_id不删任何历史数据
const res: any = await api.startNewSession()
// 后端签发新 session_id 并把盘点模式/范围落库到 stocktake_session,不删任何历史数据
const res: any = await api.startNewSession({
mode: newMode.value,
scope_type: newScopeType.value
})
persistSession(res?.session_id || res?.data?.session_id || '')
await enterScanning('新盘点会话已开始')
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '操作失败')
// 后端对尚未实现的范围会返回 400把原因透传给用户而不是笼统说“操作失败”
ElMessage.error(e?.response?.data?.message || e?.msg || e?.message || '操作失败')
}
} finally { btnLoading.value = false }
}
@ -1531,6 +1566,38 @@ const goToVarianceReview = () => {
text-align: center;
line-height: 1.5;
}
/* ★ 创建盘点任务表单(模式 / 范围) */
.task-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 4px;
}
.task-form-row {
display: flex;
align-items: center;
gap: 10px;
}
.task-form-label {
flex: 0 0 64px;
font-size: 13px;
color: #606266;
text-align: right;
}
.task-form-row :deep(.el-radio-group) { flex: 1 1 auto; }
.task-form-hint {
font-size: 12px;
color: #909399;
text-align: center;
}
.task-form-warn {
font-size: 12px;
color: #e6a23c;
text-align: center;
line-height: 1.5;
}
/* 场景 A 的进度提示与次要入口 */
.active-session-tip {
font-size: 12px;