feat(stocktake): 欢迎页改为公司维度状态驱动,多设备共享盘点会话
原入口只有一个「开始新盘点」,多台 PDA 各开各的会话,无法协同。
- 新增公司选择器:普通用户锁定并默认本公司(后端本就强制隔离),
超管必须显式选择,未选时入口按钮禁用;本公司若不在下拉列表中
则补进去,避免 el-select 显示为空。
- 选定公司后调用 /draft/active-session 探测该公司是否已有进行中的
盘点。服务端为唯一事实来源,localStorage 仅作缓存。
- 状态驱动渲染入口按钮:
场景 A(已有活跃会话)→ 绿色「加入正在进行的盘点」,并提示当前
进度项数;同时保留次要入口「开启新一轮盘点」,否则公司一旦有过
草稿就永远开不了新一轮,点击走 10 秒确认弹窗。
场景 B(无活跃会话)→ 蓝色「开始新盘点」。
- 切换公司时清空本地会话并重新探测,避免跨公司串会话。
- 移除此前「刷新后请求 /draft/list 取第一条草稿猜 session_id」的逻辑:
该列表无会话/用户过滤,取到的是全库 SKU 字典序最小的那条草稿,
与当前用户和公司都无关。
- 所有盘点请求携带当前公司;导出、生成漏盘、改实盘数等 POST 请求
亦经 query string 传递公司。
This commit is contained in:
@ -9,27 +9,70 @@
|
||||
<h2 class="app-title">库存盲盘系统</h2>
|
||||
<p class="subtitle">单件自动确认,多件弹窗录入</p>
|
||||
|
||||
<!-- ★ 公司选择:多人协同的隔离维度。选同一公司的 PDA 共享同一个 session_id -->
|
||||
<div class="company-selector">
|
||||
<el-select
|
||||
v-model="selectedCompany"
|
||||
placeholder="请选择盘点公司"
|
||||
size="large"
|
||||
class="company-select"
|
||||
:disabled="!isSuperAdmin"
|
||||
:loading="companyLoading"
|
||||
@change="handleCompanyChange"
|
||||
>
|
||||
<el-option v-for="c in companyOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
<div class="company-hint">{{ companyHint }}</div>
|
||||
</div>
|
||||
|
||||
<div class="idle-actions">
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
|
||||
开始新盘点
|
||||
<!-- 超管尚未选公司:不允许进入 -->
|
||||
<el-button v-if="!selectedCompany" type="info" size="large" class="action-btn-full" disabled>
|
||||
请先选择公司
|
||||
</el-button>
|
||||
|
||||
<!-- 场景 A:该公司已有进行中的盘点 → 绿色「加入」 -->
|
||||
<template v-else-if="activeSession">
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
class="action-btn-full"
|
||||
@click="joinActiveSession"
|
||||
:loading="btnLoading"
|
||||
>
|
||||
加入正在进行的盘点
|
||||
</el-button>
|
||||
<div class="active-session-tip">
|
||||
当前已有未完成的盘点进度({{ serverDraftCount }} 项)
|
||||
</div>
|
||||
<!-- 次要入口:缺了它,本司一旦有过草稿就永远开不了新一轮 -->
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="secondary-entry"
|
||||
@click="startNewSession"
|
||||
:loading="btnLoading"
|
||||
>
|
||||
开启新一轮盘点
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 场景 B:该公司当前无盘点 → 蓝色「开始新盘点」 -->
|
||||
<el-button
|
||||
v-if="serverDraftCount > 0 && userStore.hasPermission('inventory_stocktake:operation')"
|
||||
type="warning"
|
||||
plain
|
||||
v-else
|
||||
type="primary"
|
||||
size="large"
|
||||
class="action-btn-full"
|
||||
@click="resumeSession"
|
||||
@click="startNewSession"
|
||||
:loading="btnLoading"
|
||||
>
|
||||
继续上次盘点 <span class="sub-text">({{ serverDraftCount }}项)</span>
|
||||
开始新盘点
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="safe-tip">
|
||||
<el-icon><Cloudy /></el-icon>
|
||||
<span>数据实时同步,支持断点续传</span>
|
||||
<span>数据实时同步,同公司多设备共享盘点会话</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@ -119,7 +162,7 @@
|
||||
<!-- ★ 新增: 防呆确认弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showConfirmDialog"
|
||||
title="⚠️ 确认清除盘点数据"
|
||||
title="⚠️ 确认开启新一轮盘点"
|
||||
width="400"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@ -129,16 +172,16 @@
|
||||
>
|
||||
<div class="confirm-content">
|
||||
<el-icon :size="48" color="#E6A23C"><WarningFilled /></el-icon>
|
||||
<p class="confirm-text">存在未完成记录,开始新盘点将清除它们,确定吗?</p>
|
||||
<p class="confirm-text">存在未完成的盘点记录。开启新一轮盘点会生成新的会话,<strong>原有数据将保留</strong>,之后仍可通过「继续上次盘点」回到之前的会话。确定开始吗?</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="cancelConfirm">取消</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
type="primary"
|
||||
:disabled="countdown > 0"
|
||||
@click="confirmClear"
|
||||
@click="confirmStartNew"
|
||||
>
|
||||
{{ countdown > 0 ? `确认清除 (${countdown}s)` : '确认清除' }}
|
||||
{{ countdown > 0 ? `确认开启 (${countdown}s)` : '确认开启' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -414,7 +457,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { getAllStocktakeItems, getDraftMergedList, updateStocktakeQuantity, scanStockByBarcode } from '@/api/inbound/stock'
|
||||
import { getAllStocktakeItems, getDraftMergedList, updateStocktakeQuantity, scanStockByBarcode, getStocktakeCompanies } from '@/api/inbound/stock'
|
||||
import QrScanner from '@/components/QrScanner/index.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
|
||||
@ -494,6 +537,62 @@ const listTotalFiltered = ref(0) // 过滤后的总数
|
||||
// ★ 新增: 会话ID
|
||||
const currentSessionId = ref<string>('')
|
||||
|
||||
// ★ 会话 / 公司持久化
|
||||
// session_id 由服务端的「活跃会话」接口给出:同一公司的多台 PDA 拿到的是同一个值,
|
||||
// 这是多人协同的基础。localStorage 只是缓存,服务端才是唯一事实来源。
|
||||
const SESSION_STORAGE_KEY = 'stocktake_session_id'
|
||||
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)
|
||||
|
||||
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN')
|
||||
|
||||
// 选择器下方的说明文字
|
||||
const companyHint = computed(() => {
|
||||
if (isSuperAdmin.value) {
|
||||
return selectedCompany.value ? `当前盘点范围:${selectedCompany.value}` : '请先选择要盘点的公司'
|
||||
}
|
||||
if (!selectedCompany.value) return '您的账号未绑定公司,请联系管理员'
|
||||
return `已锁定为您所属公司:${selectedCompany.value}`
|
||||
})
|
||||
|
||||
const persistSession = (sessionId: string) => {
|
||||
currentSessionId.value = sessionId || ''
|
||||
if (sessionId) {
|
||||
localStorage.setItem(SESSION_STORAGE_KEY, sessionId)
|
||||
} else {
|
||||
localStorage.removeItem(SESSION_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
const clearPersistedSession = () => {
|
||||
localStorage.removeItem(SESSION_STORAGE_KEY)
|
||||
currentSessionId.value = ''
|
||||
}
|
||||
|
||||
const persistCompany = (company: string) => {
|
||||
selectedCompany.value = company || ''
|
||||
if (company) {
|
||||
localStorage.setItem(COMPANY_STORAGE_KEY, company)
|
||||
} else {
|
||||
localStorage.removeItem(COMPANY_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
// 统一构造带公司标识的查询参数。
|
||||
// ★ 必须走 query string:后端 get_current_company_filter() 只读 request.args,
|
||||
// 对 POST 请求同样如此 —— body 里的 company_name 会被直接忽略。
|
||||
const withCompany = (extra: Record<string, any> = {}) => {
|
||||
const params: Record<string, any> = { ...extra }
|
||||
if (selectedCompany.value) params.company_name = selectedCompany.value
|
||||
return params
|
||||
}
|
||||
|
||||
// 获取应盘物资清单(盘点基数)
|
||||
const fetchAllStockItems = async (page = 1) => {
|
||||
try {
|
||||
@ -501,7 +600,8 @@ const fetchAllStockItems = async (page = 1) => {
|
||||
const res: any = await getAllStocktakeItems({
|
||||
session_id: currentSessionId.value,
|
||||
page,
|
||||
pageSize: 200
|
||||
pageSize: 200,
|
||||
company_name: selectedCompany.value || undefined
|
||||
})
|
||||
if (res && res.code === 200) {
|
||||
allStockItems.value = res.data.items || []
|
||||
@ -538,37 +638,54 @@ const api = {
|
||||
getDrafts: (sessionId?: string) => request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { session_id: sessionId }
|
||||
params: withCompany({ session_id: sessionId })
|
||||
}),
|
||||
// ★ 该公司最近一次活跃的盘点会话(多人协同:用于「加入正在进行的盘点」)
|
||||
activeSession: () => request({
|
||||
url: '/v1/inbound/stock/draft/active-session',
|
||||
method: 'get',
|
||||
params: withCompany()
|
||||
}),
|
||||
addDraft: (data: any) => request({
|
||||
url: '/v1/inbound/stock/draft/add',
|
||||
method: 'post',
|
||||
params: withCompany(), // ★ 公司走 query string,body 里的会被后端忽略
|
||||
data: { ...data, session_id: currentSessionId.value }
|
||||
}),
|
||||
// ★ 新增: 开始新会话
|
||||
startNewSession: () => request({
|
||||
url: '/v1/inbound/stock/draft/start-new',
|
||||
method: 'post',
|
||||
params: withCompany(),
|
||||
data: {}
|
||||
}),
|
||||
// ★ 新增: 结束盘点
|
||||
finishStocktake: () => request({
|
||||
url: '/v1/inbound/stock/finish',
|
||||
method: 'post',
|
||||
params: withCompany(),
|
||||
data: { session_id: currentSessionId.value }
|
||||
}),
|
||||
// ★ 新增: 获取差异报告
|
||||
getVarianceReport: () => request({
|
||||
url: '/v1/inbound/stock/variance-report',
|
||||
method: 'get',
|
||||
params: {}
|
||||
params: withCompany()
|
||||
}),
|
||||
// ★ 保留清除功能(用于兼容性)
|
||||
clearDraft: () => request({
|
||||
url: '/v1/inbound/stock/draft/clear',
|
||||
method: 'post',
|
||||
data: {}
|
||||
})
|
||||
// 后端已强制要求 session_id:不传会被 400 拒绝(防止误清空他司/他人的草稿)。
|
||||
// 清除成功后同步丢弃本地持久化的 session_id。
|
||||
clearDraft: async () => {
|
||||
const res = await request({
|
||||
url: '/v1/inbound/stock/draft/clear',
|
||||
method: 'post',
|
||||
params: withCompany(),
|
||||
data: { session_id: currentSessionId.value }
|
||||
})
|
||||
clearPersistedSession()
|
||||
isSessionActive.value = false
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
const typeToSourceTable = (type: string): string => {
|
||||
@ -581,27 +698,90 @@ const typeToSourceTable = (type: string): string => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await checkServerDraft()
|
||||
await initEntry()
|
||||
})
|
||||
|
||||
// ★ 入口初始化:加载公司列表 → 确定默认公司 → 查该公司的活跃会话
|
||||
const initEntry = async () => {
|
||||
await loadCompanies()
|
||||
|
||||
const checkServerDraft = async () => {
|
||||
try {
|
||||
// 获取草稿数量(后端已移除 is_finished 字段,直接返回所有记录)
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { page: 1, limit: 1 } // 只获取一条记录来获取总数
|
||||
})
|
||||
// 后端返回格式已改为 { items: [], total: number }
|
||||
serverDraftCount.value = (res && res.total) || 0
|
||||
} catch (e) {}
|
||||
if (!isSuperAdmin.value) {
|
||||
// 普通用户锁定本公司(后端也会强制隔离,这里只是让选择器显示正确)
|
||||
const myCompany = userStore.companyName
|
||||
if (myCompany) {
|
||||
// 本公司可能尚无物料、不在列表中,补进去否则 el-select 显示为空
|
||||
if (!companyOptions.value.includes(myCompany)) companyOptions.value.unshift(myCompany)
|
||||
persistCompany(myCompany)
|
||||
}
|
||||
} else {
|
||||
// 超管沿用上次选择;必须显式选一个公司,否则无法确定盘点范围
|
||||
const saved = localStorage.getItem(COMPANY_STORAGE_KEY)
|
||||
if (saved && companyOptions.value.includes(saved)) persistCompany(saved)
|
||||
}
|
||||
|
||||
if (selectedCompany.value) await checkServerDraft()
|
||||
}
|
||||
|
||||
// ★ 重写: 开始新盘点 - 使用新 API
|
||||
const loadCompanies = async () => {
|
||||
companyLoading.value = true
|
||||
try {
|
||||
const res: any = await getStocktakeCompanies()
|
||||
companyOptions.value = res?.data?.companies || []
|
||||
} catch (e) {
|
||||
console.error('获取公司列表失败', e)
|
||||
companyOptions.value = []
|
||||
} finally {
|
||||
companyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换公司 = 切换盘点范围 → 必须丢弃上一个公司的会话,再重新探测
|
||||
const handleCompanyChange = async (val: string) => {
|
||||
clearPersistedSession()
|
||||
activeSession.value = null
|
||||
serverDraftCount.value = 0
|
||||
persistCompany(val)
|
||||
await checkServerDraft()
|
||||
}
|
||||
|
||||
// ★ 探测该公司是否有进行中的盘点 —— 服务端是唯一事实来源,
|
||||
// 这样第二台 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 }
|
||||
: null
|
||||
serverDraftCount.value = data.total || 0
|
||||
} catch (e) {
|
||||
activeSession.value = null
|
||||
serverDraftCount.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 进入扫码页的公共收尾:落到 scanning 阶段并拉取盘点基数
|
||||
const enterScanning = async (message: string) => {
|
||||
isSessionActive.value = true
|
||||
localStorage.setItem('stocktake_phase', 'scanning')
|
||||
await fetchAllStockItems()
|
||||
await fetchInventoryList()
|
||||
ElMessage.success(message)
|
||||
}
|
||||
|
||||
// 点击「开始新盘点」或次要入口「开启新一轮盘点」
|
||||
const startNewSession = async () => {
|
||||
// ★ 新增: 防呆确认弹窗
|
||||
if (serverDraftCount.value > 0) {
|
||||
if (!selectedCompany.value) {
|
||||
ElMessage.warning('请先选择公司')
|
||||
return
|
||||
}
|
||||
// ★ 该公司已有进行中的盘点 → 二次确认(不删数据,只签发新会话)
|
||||
if (activeSession.value) {
|
||||
showConfirmDialog.value = true
|
||||
countdown.value = 10
|
||||
if (countdownTimer) clearInterval(countdownTimer)
|
||||
@ -621,16 +801,12 @@ const doStartNewSession = async () => {
|
||||
showConfirmDialog.value = false
|
||||
btnLoading.value = true
|
||||
try {
|
||||
// 调用新 API 开始新会话
|
||||
// ★ 先清掉旧会话,避免签发失败时残留上一个 session_id
|
||||
clearPersistedSession()
|
||||
// 后端只签发新 session_id,不删除任何历史数据
|
||||
const res: any = await api.startNewSession()
|
||||
currentSessionId.value = res.session_id || ''
|
||||
isSessionActive.value = true
|
||||
// ★ 标记当前阶段为 scanning(扫码中)
|
||||
localStorage.setItem('stocktake_phase', 'scanning')
|
||||
// ★ 立即加载统计基数
|
||||
await fetchAllStockItems()
|
||||
await fetchInventoryList()
|
||||
ElMessage.success('新盘点会话已开始')
|
||||
persistSession(res?.session_id || res?.data?.session_id || '')
|
||||
await enterScanning('新盘点会话已开始')
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e?.message || '操作失败')
|
||||
@ -638,7 +814,24 @@ const doStartNewSession = async () => {
|
||||
} finally { btnLoading.value = false }
|
||||
}
|
||||
|
||||
const confirmClear = () => {
|
||||
// ★ 场景 A:加入同事已开启的盘点 —— 同一公司共享同一个 session_id
|
||||
const joinActiveSession = async () => {
|
||||
if (!activeSession.value?.session_id) {
|
||||
ElMessage.warning('没有找到进行中的盘点')
|
||||
await checkServerDraft()
|
||||
return
|
||||
}
|
||||
btnLoading.value = true
|
||||
try {
|
||||
persistSession(activeSession.value.session_id)
|
||||
await enterScanning('已加入正在进行的盘点')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '加入失败')
|
||||
} finally { btnLoading.value = false }
|
||||
}
|
||||
|
||||
// 弹窗确认后开启新一轮盘点(不再删除任何数据,故不叫 confirmClear)
|
||||
const confirmStartNew = () => {
|
||||
if (countdown.value > 0) return
|
||||
doStartNewSession()
|
||||
}
|
||||
@ -652,53 +845,6 @@ const cancelConfirm = () => {
|
||||
countdown.value = 0
|
||||
}
|
||||
|
||||
// ★ 重写: 继续上次盘点 - 恢复扫码作业
|
||||
const resumeSession = async () => {
|
||||
btnLoading.value = true
|
||||
try {
|
||||
// 获取最新的会话(后端已移除 is_finished 字段)
|
||||
// 后端返回格式已改为 { items: [], total: number }
|
||||
// ★ 仅检查是否有草稿存在(取1条即可,无需99999全量)
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { page: 1, limit: 1 }
|
||||
})
|
||||
|
||||
const total = res?.total || 0
|
||||
const firstDraft = res?.items?.[0]
|
||||
|
||||
if (total === 0 || !firstDraft) {
|
||||
ElMessage.warning('没有找到未完成的盘点记录')
|
||||
return
|
||||
}
|
||||
|
||||
// ★ 直接从第一条草稿恢复 session_id,不再拉取全量草稿
|
||||
// 避免 limit=500 请求触发后端 query.all() + N+1 性能瓶颈
|
||||
currentSessionId.value = firstDraft.session_id || ''
|
||||
|
||||
// 直接恢复会话状态
|
||||
isSessionActive.value = true
|
||||
|
||||
// ★ 立即加载统计基数
|
||||
await fetchAllStockItems()
|
||||
await fetchInventoryList()
|
||||
|
||||
// ★ 智能路由:根据本地记忆的阶段决定下一步
|
||||
const phase = localStorage.getItem('stocktake_phase')
|
||||
if (phase === 'review') {
|
||||
// 如果记忆中已经是核对阶段,直接打开差异弹窗
|
||||
await openVarianceDialog()
|
||||
ElMessage.info('已恢复差异审核')
|
||||
} else {
|
||||
// 默认打开扫码镜头
|
||||
ElMessage.success('已恢复扫码,继续盘点')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('恢复失败')
|
||||
} finally { btnLoading.value = false }
|
||||
}
|
||||
|
||||
const pauseSession = () => {
|
||||
isSessionActive.value = false
|
||||
checkServerDraft()
|
||||
@ -735,7 +881,7 @@ const onScanSuccess = async (code: string) => {
|
||||
// ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find(),避免漏匹配)
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await scanStockByBarcode(trimCode)
|
||||
const res: any = await scanStockByBarcode(trimCode, selectedCompany.value || undefined)
|
||||
|
||||
if (!res || !res.data) {
|
||||
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||
@ -813,7 +959,7 @@ const handleManualInput = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find())
|
||||
const res: any = await scanStockByBarcode(code)
|
||||
const res: any = await scanStockByBarcode(code, selectedCompany.value || undefined)
|
||||
|
||||
if (!res || !res.data) {
|
||||
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||
@ -867,7 +1013,7 @@ const openQtyDialog = async (item: StockItem) => {
|
||||
const dupRes: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { session_id: currentSessionId.value, uuid: uuidForQuery, limit: 1 }
|
||||
params: withCompany({ session_id: currentSessionId.value, uuid: uuidForQuery, limit: 1 })
|
||||
})
|
||||
const dupItems = dupRes?.items || dupRes?.data?.items || []
|
||||
if (dupItems.length > 0 && dupItems[0].quantity !== undefined) {
|
||||
@ -983,12 +1129,11 @@ const exportToExcel = async () => {
|
||||
// ===== 调试结束 =====
|
||||
|
||||
ElMessage.info('正在生成盘点报告,请稍候...');
|
||||
// ★ 传递 session_id 参数,用于导出当前会话的未盘点明细
|
||||
const sessionParam = currentSessionId.value ? `?session_id=${encodeURIComponent(currentSessionId.value)}` : '';
|
||||
// 使用项目封装的 request 发送请求,确保自动携带 JWT Token
|
||||
// ★ 传递 session_id + 公司,导出范围限定在当前会话与当前公司
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/export-stocktake' + sessionParam,
|
||||
url: '/v1/inbound/stock/export-stocktake',
|
||||
method: 'get',
|
||||
params: withCompany({ session_id: currentSessionId.value || undefined }),
|
||||
responseType: 'blob' as any, // 核心:接收二进制文件流
|
||||
headers: {
|
||||
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
@ -1050,7 +1195,8 @@ const fetchInventoryList = async (silent = false) => {
|
||||
keyword: listKeyword.value || undefined,
|
||||
status_filter: listStatusFilter.value || undefined,
|
||||
page: listPage.value,
|
||||
pageSize: listLimit.value
|
||||
pageSize: listLimit.value,
|
||||
company_name: selectedCompany.value || undefined
|
||||
})
|
||||
|
||||
const data = res?.data || {}
|
||||
@ -1104,7 +1250,7 @@ const handleQuantityChange = async (row: any, val: number) => {
|
||||
source_table: row.source_table,
|
||||
quantity: val,
|
||||
session_id: currentSessionId.value // ★ 传入会话ID,后端按会话隔离更新
|
||||
})
|
||||
}, selectedCompany.value || undefined)
|
||||
ElMessage.success('实盘数已更新')
|
||||
} catch (e) {
|
||||
console.error('更新实盘数失败', e)
|
||||
@ -1153,6 +1299,7 @@ const handleGenerateMissing = async () => {
|
||||
const res = await request({
|
||||
url: '/v1/inbound/stock/stocktake/generate-missing',
|
||||
method: 'post',
|
||||
params: withCompany(), // ★ 公司走 query string
|
||||
data: {
|
||||
session_id: currentSessionId.value // ★ 必须传递 session_id 隔离会话
|
||||
}
|
||||
@ -1267,6 +1414,32 @@ const goToVarianceReview = () => {
|
||||
}
|
||||
.sub-text { font-size: 12px; opacity: 0.8; margin-left: 5px; }
|
||||
|
||||
/* ★ 公司选择器:多人协同的隔离维度 */
|
||||
.company-selector {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.company-select {
|
||||
width: 100%;
|
||||
}
|
||||
.company-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
/* 场景 A 的进度提示与次要入口 */
|
||||
.active-session-tip {
|
||||
font-size: 12px;
|
||||
color: #67c23a;
|
||||
text-align: center;
|
||||
margin-top: -5px;
|
||||
}
|
||||
.secondary-entry {
|
||||
font-size: 13px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.safe-tip { margin-top: 40px; font-size: 12px; color: #67c23a; display: flex; align-items: center; justify-content: center; gap: 5px; }
|
||||
|
||||
.active-card {
|
||||
|
||||
Reference in New Issue
Block a user