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:
yueli
2026-09-11 11:33:26 +08:00
parent 1d1ca1b89e
commit 32cb5469c4

View File

@ -9,27 +9,70 @@
<h2 class="app-title">库存盲盘系统</h2> <h2 class="app-title">库存盲盘系统</h2>
<p class="subtitle">单件自动确认,多件弹窗录入</p> <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"> <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> </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 <el-button
v-if="serverDraftCount > 0 && userStore.hasPermission('inventory_stocktake:operation')" v-else
type="warning" type="primary"
plain
size="large" size="large"
class="action-btn-full" class="action-btn-full"
@click="resumeSession" @click="startNewSession"
:loading="btnLoading" :loading="btnLoading"
> >
继续上次盘点 <span class="sub-text">({{ serverDraftCount }}项)</span> 开始新盘点
</el-button> </el-button>
</div> </div>
<div class="safe-tip"> <div class="safe-tip">
<el-icon><Cloudy /></el-icon> <el-icon><Cloudy /></el-icon>
<span>数据实时同步,支持断点续传</span> <span>数据实时同步,同公司多设备共享盘点会话</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@ -119,7 +162,7 @@
<!-- ★ 新增: 防呆确认弹窗 --> <!-- ★ 新增: 防呆确认弹窗 -->
<el-dialog <el-dialog
v-model="showConfirmDialog" v-model="showConfirmDialog"
title="⚠️ 确认清除盘点数据" title="⚠️ 确认开启新一轮盘点"
width="400" width="400"
destroy-on-close destroy-on-close
:close-on-click-modal="false" :close-on-click-modal="false"
@ -129,16 +172,16 @@
> >
<div class="confirm-content"> <div class="confirm-content">
<el-icon :size="48" color="#E6A23C"><WarningFilled /></el-icon> <el-icon :size="48" color="#E6A23C"><WarningFilled /></el-icon>
<p class="confirm-text">存在未完成记录,开始新盘点将清除它们,确定吗?</p> <p class="confirm-text">存在未完成的盘点记录。开启新一轮盘点会生成新的会话,<strong>原有数据将保留</strong>,之后仍可通过「继续上次盘点」回到之前的会话。确定开始吗?</p>
</div> </div>
<template #footer> <template #footer>
<el-button @click="cancelConfirm">取消</el-button> <el-button @click="cancelConfirm">取消</el-button>
<el-button <el-button
type="danger" type="primary"
:disabled="countdown > 0" :disabled="countdown > 0"
@click="confirmClear" @click="confirmStartNew"
> >
{{ countdown > 0 ? `确认清除 (${countdown}s)` : '确认清除' }} {{ countdown > 0 ? `确认开启 (${countdown}s)` : '确认开启' }}
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
@ -414,7 +457,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' 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 QrScanner from '@/components/QrScanner/index.vue'
import { ElMessage, ElMessageBox } from 'element-plus' 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' 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 // ★ 新增: 会话ID
const currentSessionId = ref<string>('') 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) => { const fetchAllStockItems = async (page = 1) => {
try { try {
@ -501,7 +600,8 @@ const fetchAllStockItems = async (page = 1) => {
const res: any = await getAllStocktakeItems({ const res: any = await getAllStocktakeItems({
session_id: currentSessionId.value, session_id: currentSessionId.value,
page, page,
pageSize: 200 pageSize: 200,
company_name: selectedCompany.value || undefined
}) })
if (res && res.code === 200) { if (res && res.code === 200) {
allStockItems.value = res.data.items || [] allStockItems.value = res.data.items || []
@ -538,37 +638,54 @@ const api = {
getDrafts: (sessionId?: string) => request({ getDrafts: (sessionId?: string) => request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', 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({ addDraft: (data: any) => request({
url: '/v1/inbound/stock/draft/add', url: '/v1/inbound/stock/draft/add',
method: 'post', method: 'post',
params: withCompany(), // ★ 公司走 query string,body 里的会被后端忽略
data: { ...data, session_id: currentSessionId.value } data: { ...data, session_id: currentSessionId.value }
}), }),
// ★ 新增: 开始新会话 // ★ 新增: 开始新会话
startNewSession: () => request({ startNewSession: () => request({
url: '/v1/inbound/stock/draft/start-new', url: '/v1/inbound/stock/draft/start-new',
method: 'post', method: 'post',
params: withCompany(),
data: {} data: {}
}), }),
// ★ 新增: 结束盘点 // ★ 新增: 结束盘点
finishStocktake: () => request({ finishStocktake: () => request({
url: '/v1/inbound/stock/finish', url: '/v1/inbound/stock/finish',
method: 'post', method: 'post',
params: withCompany(),
data: { session_id: currentSessionId.value } data: { session_id: currentSessionId.value }
}), }),
// ★ 新增: 获取差异报告 // ★ 新增: 获取差异报告
getVarianceReport: () => request({ getVarianceReport: () => request({
url: '/v1/inbound/stock/variance-report', url: '/v1/inbound/stock/variance-report',
method: 'get', method: 'get',
params: {} params: withCompany()
}), }),
// ★ 保留清除功能(用于兼容性) // ★ 保留清除功能(用于兼容性)
clearDraft: () => request({ // 后端已强制要求 session_id:不传会被 400 拒绝(防止误清空他司/他人的草稿)。
url: '/v1/inbound/stock/draft/clear', // 清除成功后同步丢弃本地持久化的 session_id。
method: 'post', clearDraft: async () => {
data: {} 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 => { const typeToSourceTable = (type: string): string => {
@ -581,27 +698,90 @@ const typeToSourceTable = (type: string): string => {
} }
onMounted(async () => { onMounted(async () => {
await checkServerDraft() await initEntry()
}) })
// ★ 入口初始化:加载公司列表 → 确定默认公司 → 查该公司的活跃会话
const initEntry = async () => {
await loadCompanies()
const checkServerDraft = async () => { if (!isSuperAdmin.value) {
try { // 普通用户锁定本公司(后端也会强制隔离,这里只是让选择器显示正确)
// 获取草稿数量(后端已移除 is_finished 字段,直接返回所有记录) const myCompany = userStore.companyName
const res: any = await request({ if (myCompany) {
url: '/v1/inbound/stock/draft/list', // 本公司可能尚无物料、不在列表中,补进去否则 el-select 显示为空
method: 'get', if (!companyOptions.value.includes(myCompany)) companyOptions.value.unshift(myCompany)
params: { page: 1, limit: 1 } // 只获取一条记录来获取总数 persistCompany(myCompany)
}) }
// 后端返回格式已改为 { items: [], total: number } } else {
serverDraftCount.value = (res && res.total) || 0 // 超管沿用上次选择;必须显式选一个公司,否则无法确定盘点范围
} catch (e) {} 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 () => { const startNewSession = async () => {
// ★ 新增: 防呆确认弹窗 if (!selectedCompany.value) {
if (serverDraftCount.value > 0) { ElMessage.warning('请先选择公司')
return
}
// ★ 该公司已有进行中的盘点 → 二次确认(不删数据,只签发新会话)
if (activeSession.value) {
showConfirmDialog.value = true showConfirmDialog.value = true
countdown.value = 10 countdown.value = 10
if (countdownTimer) clearInterval(countdownTimer) if (countdownTimer) clearInterval(countdownTimer)
@ -621,16 +801,12 @@ const doStartNewSession = async () => {
showConfirmDialog.value = false showConfirmDialog.value = false
btnLoading.value = true btnLoading.value = true
try { try {
// 调用新 API 开始新会话 // ★ 先清掉旧会话,避免签发失败时残留上一个 session_id
clearPersistedSession()
// 后端只签发新 session_id,不删除任何历史数据
const res: any = await api.startNewSession() const res: any = await api.startNewSession()
currentSessionId.value = res.session_id || '' persistSession(res?.session_id || res?.data?.session_id || '')
isSessionActive.value = true await enterScanning('新盘点会话已开始')
// ★ 标记当前阶段为 scanning(扫码中)
localStorage.setItem('stocktake_phase', 'scanning')
// ★ 立即加载统计基数
await fetchAllStockItems()
await fetchInventoryList()
ElMessage.success('新盘点会话已开始')
} catch (e: any) { } catch (e: any) {
if (e !== 'cancel') { if (e !== 'cancel') {
ElMessage.error(e?.message || '操作失败') ElMessage.error(e?.message || '操作失败')
@ -638,7 +814,24 @@ const doStartNewSession = async () => {
} finally { btnLoading.value = false } } 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 if (countdown.value > 0) return
doStartNewSession() doStartNewSession()
} }
@ -652,53 +845,6 @@ const cancelConfirm = () => {
countdown.value = 0 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 = () => { const pauseSession = () => {
isSessionActive.value = false isSessionActive.value = false
checkServerDraft() checkServerDraft()
@ -735,7 +881,7 @@ const onScanSuccess = async (code: string) => {
// ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find(),避免漏匹配) // ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find(),避免漏匹配)
loading.value = true loading.value = true
try { try {
const res: any = await scanStockByBarcode(trimCode) const res: any = await scanStockByBarcode(trimCode, selectedCompany.value || undefined)
if (!res || !res.data) { if (!res || !res.data) {
ElMessage.error(`未找到该物料库存: ${trimCode}`) ElMessage.error(`未找到该物料库存: ${trimCode}`)
@ -813,7 +959,7 @@ const handleManualInput = async () => {
loading.value = true loading.value = true
try { try {
// ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find()) // ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find())
const res: any = await scanStockByBarcode(code) const res: any = await scanStockByBarcode(code, selectedCompany.value || undefined)
if (!res || !res.data) { if (!res || !res.data) {
ElMessage.error(`未找到该物料库存: ${code}`) ElMessage.error(`未找到该物料库存: ${code}`)
@ -867,7 +1013,7 @@ const openQtyDialog = async (item: StockItem) => {
const dupRes: any = await request({ const dupRes: any = await request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', 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 || [] const dupItems = dupRes?.items || dupRes?.data?.items || []
if (dupItems.length > 0 && dupItems[0].quantity !== undefined) { if (dupItems.length > 0 && dupItems[0].quantity !== undefined) {
@ -983,12 +1129,11 @@ const exportToExcel = async () => {
// ===== 调试结束 ===== // ===== 调试结束 =====
ElMessage.info('正在生成盘点报告,请稍候...'); ElMessage.info('正在生成盘点报告,请稍候...');
// ★ 传递 session_id 参数,用于导出当前会话的未盘点明细 // ★ 传递 session_id + 公司,导出范围限定在当前会话与当前公司
const sessionParam = currentSessionId.value ? `?session_id=${encodeURIComponent(currentSessionId.value)}` : '';
// 使用项目封装的 request 发送请求,确保自动携带 JWT Token
const res: any = await request({ const res: any = await request({
url: '/v1/inbound/stock/export-stocktake' + sessionParam, url: '/v1/inbound/stock/export-stocktake',
method: 'get', method: 'get',
params: withCompany({ session_id: currentSessionId.value || undefined }),
responseType: 'blob' as any, // 核心:接收二进制文件流 responseType: 'blob' as any, // 核心:接收二进制文件流
headers: { headers: {
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@ -1050,7 +1195,8 @@ const fetchInventoryList = async (silent = false) => {
keyword: listKeyword.value || undefined, keyword: listKeyword.value || undefined,
status_filter: listStatusFilter.value || undefined, status_filter: listStatusFilter.value || undefined,
page: listPage.value, page: listPage.value,
pageSize: listLimit.value pageSize: listLimit.value,
company_name: selectedCompany.value || undefined
}) })
const data = res?.data || {} const data = res?.data || {}
@ -1104,7 +1250,7 @@ const handleQuantityChange = async (row: any, val: number) => {
source_table: row.source_table, source_table: row.source_table,
quantity: val, quantity: val,
session_id: currentSessionId.value // ★ 传入会话ID,后端按会话隔离更新 session_id: currentSessionId.value // ★ 传入会话ID,后端按会话隔离更新
}) }, selectedCompany.value || undefined)
ElMessage.success('实盘数已更新') ElMessage.success('实盘数已更新')
} catch (e) { } catch (e) {
console.error('更新实盘数失败', e) console.error('更新实盘数失败', e)
@ -1153,6 +1299,7 @@ const handleGenerateMissing = async () => {
const res = await request({ const res = await request({
url: '/v1/inbound/stock/stocktake/generate-missing', url: '/v1/inbound/stock/stocktake/generate-missing',
method: 'post', method: 'post',
params: withCompany(), // ★ 公司走 query string
data: { data: {
session_id: currentSessionId.value // ★ 必须传递 session_id 隔离会话 session_id: currentSessionId.value // ★ 必须传递 session_id 隔离会话
} }
@ -1267,6 +1414,32 @@ const goToVarianceReview = () => {
} }
.sub-text { font-size: 12px; opacity: 0.8; margin-left: 5px; } .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; } .safe-tip { margin-top: 40px; font-size: 12px; color: #67c23a; display: flex; align-items: center; justify-content: center; gap: 5px; }
.active-card { .active-card {