feat(stocktake): 欢迎页识别跨域角色,完善场景 A 提示

- 公司选择器原先只对 SUPER_ADMIN 解锁,漏掉了同样持有 crossDomain
  权限的 WAREHOUSE_MGR —— 它同样不受后端公司隔离约束,盘点范围完全
  由所选公司决定,必须一并要求显式选择。
  注意 SUPER_ADMIN 的权限是通配符 ['*'],permissions.includes('crossDomain')
  对其恒为 false,故角色与权限码两个条件都要判。

- 场景 A 提示改为「当前公司已有盘点在进行中,已扫 XXX 件,发起人:XXX」,
  使用后端新增的 scanned / initiator 字段(scanned 已排除系统自动漏盘记录)。
  按钮文案统一为「加入当前盘点」。

- 清理 serverDraftCount:提示语改用 activeSession.scanned 后它只写不读,
  属死状态,一并移除。

- 确认弹窗文案原先指向已不存在的「继续上次盘点」入口,改为说明开新会话后
  「加入当前盘点」只会进入最新会话。
This commit is contained in:
yueli
2026-09-11 12:44:10 +08:00
parent f8403d2fb2
commit 5b0c7c1382

View File

@ -16,7 +16,7 @@
placeholder="请选择盘点公司"
size="large"
class="company-select"
:disabled="!isSuperAdmin"
:disabled="!hasCrossDomain"
:loading="companyLoading"
@change="handleCompanyChange"
>
@ -55,10 +55,10 @@
@click="joinActiveSession"
:loading="btnLoading"
>
加入正在进行的盘点
加入当前盘点
</el-button>
<div class="active-session-tip">
当前已有未完成的盘点进度{{ serverDraftCount }}
当前公司已有盘点在进行中已扫 {{ activeSession.scanned }} <template v-if="activeSession.initiator">发起人{{ activeSession.initiator }}</template>
</div>
<!-- 次要入口缺了它本司一旦有过草稿就永远开不了新一轮 -->
<el-button
@ -187,7 +187,7 @@
>
<div class="confirm-content">
<el-icon :size="48" color="#E6A23C"><WarningFilled /></el-icon>
<p class="confirm-text">存在未完成的盘点记录开启新一轮盘点会生成新的会话<strong>原有数据将保留</strong>之后仍可通过继续上次盘点回到之前的会话确定开始吗</p>
<p class="confirm-text">当前公司已有盘点在进行中开启新一轮会生成新的会话<strong>原有数据将保留</strong>之后加入当前盘点只会进入最新的这个会话确定开始吗</p>
</div>
<template #footer>
<el-button @click="cancelConfirm">取消</el-button>
@ -509,7 +509,6 @@ const loading = ref(false)
const btnLoading = ref(false)
const printing = ref(false)
const isSessionActive = ref(false)
const serverDraftCount = ref(0)
const syncStatus = ref<'success' | 'syncing' | 'failed'>('success')
// 新增:扫码相关状态
@ -562,17 +561,30 @@ const COMPANY_STORAGE_KEY = 'stocktake_company'
const selectedCompany = ref<string>('')
const companyOptions = ref<string[]>([])
const companyLoading = ref(false)
// 该公司的活跃会话{ session_id, total, last_scan_time };无进行中的盘点则为 null
const activeSession = ref<{ session_id: string; total: number; last_scan_time: string | null } | null>(null)
// 该公司的活跃会话;无进行中的盘点则为 null
const activeSession = ref<{
session_id: string
total: number // 该会话草稿行数
scanned: number // 实际扫到的件数(按物料去重,不含系统自动漏盘)
last_scan_time: string | null
initiator: string | null
} | null>(null)
// ★ 探测失败标记:网络抖动时绝不能把「没查到」当成「没有盘点」——
// 否则界面会退回「开始新盘点」,库管一点就新建会话,把多人协同拆开
const probeFailed = ref(false)
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN')
// ★ 跨域角色:超管,或持有 crossDomain 权限的角色(当前为 WAREHOUSE_MGR
// 后端 get_current_company_filter() 对它们返回 None不隔离所以盘点范围
// 完全取决于用户选哪个公司 —— 这类用户必须显式选择,不能有默认值。
// 注意SUPER_ADMIN 的权限是通配符 ['*']permissions.includes('crossDomain')
// 对它恒为 false因此角色与权限码两个条件都要判。
const hasCrossDomain = computed(() =>
userStore.role === 'SUPER_ADMIN' || userStore.permissions.includes('crossDomain')
)
// 选择器下方的说明文字
const companyHint = computed(() => {
if (isSuperAdmin.value) {
if (hasCrossDomain.value) {
return selectedCompany.value ? `当前盘点范围:${selectedCompany.value}` : '请先选择要盘点的公司'
}
if (!selectedCompany.value) return '您的账号未绑定公司,请联系管理员'
@ -658,7 +670,7 @@ const api = {
method: 'get',
params: withCompany({ session_id: sessionId })
}),
// ★ 该公司最近一次活跃的盘点会话(多人协同:用于「加入正在进行的盘点」)
// ★ 该公司最近一次活跃的盘点会话(多人协同:用于「加入当前盘点」)
activeSession: () => request({
url: '/v1/inbound/stock/draft/active-session',
method: 'get',
@ -723,8 +735,8 @@ onMounted(async () => {
const initEntry = async () => {
await loadCompanies()
if (!isSuperAdmin.value) {
// 普通用户锁定本公司(后端会强制隔离,这里只是让选择器显示正确
if (!hasCrossDomain.value) {
// 非跨域用户:后端会强制隔离到本公司,这里只是让选择器显示正确
const myCompany = userStore.companyName
if (myCompany) {
// 本公司可能尚无物料、不在列表中,补进去否则 el-select 显示为空
@ -732,7 +744,7 @@ const initEntry = async () => {
persistCompany(myCompany)
}
} else {
// 超管沿用上次选择;必须显式选一个公司,否则无法确定盘点范围
// 跨域角色沿用上次选择;必须显式选一个公司,否则盘点范围无从确定
const saved = localStorage.getItem(COMPANY_STORAGE_KEY)
if (saved && companyOptions.value.includes(saved)) persistCompany(saved)
}
@ -757,27 +769,30 @@ const loadCompanies = async () => {
const handleCompanyChange = async (val: string) => {
clearPersistedSession()
activeSession.value = null
serverDraftCount.value = 0
probeFailed.value = false
persistCompany(val)
await checkServerDraft()
}
// ★ 探测该公司是否有进行中的盘点 —— 服务端是唯一事实来源,
// 这样第二台 PDA 选同一公司后就能看到「加入正在进行的盘点」。
// 这样第二台 PDA 选同一公司后就能看到「加入当前盘点」。
const checkServerDraft = async () => {
if (!selectedCompany.value) {
activeSession.value = null
serverDraftCount.value = 0
return
}
try {
const res: any = await api.activeSession()
const data = res?.data || {}
activeSession.value = data.session_id
? { session_id: data.session_id, total: data.total || 0, last_scan_time: data.last_scan_time || null }
? {
session_id: data.session_id,
total: data.total || 0,
scanned: data.scanned || 0,
last_scan_time: data.last_scan_time || null,
initiator: data.initiator || null
}
: null
serverDraftCount.value = data.total || 0
probeFailed.value = false
} catch (e) {
// ★ 探测失败 ≠ 没有盘点。保持 activeSession 不变并把界面切到「重试」,
@ -845,7 +860,7 @@ const joinActiveSession = async () => {
btnLoading.value = true
try {
persistSession(activeSession.value.session_id)
await enterScanning('已加入正在进行的盘点')
await enterScanning('已加入当前盘点')
} catch (e: any) {
ElMessage.error(e?.message || '加入失败')
} finally { btnLoading.value = false }