import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { authApi } from '../api/auth' import { captureOrganizationContext, commitOrganizationId, getOrganizationId, resetOrganizationContext } from '../utils/organizationContext' // 本标签页上次选择的公司按用户分开存:同一浏览器不同账号登录时互不继承选择。 function selectionStorageKey(userId) { return `wmms_organization_id:${userId ?? 'anonymous'}` } // 直接读 localStorage 里的用户信息拿 id,不导入 auth store,避免 store 之间相互依赖成环。 function currentUserId() { try { const user = JSON.parse(localStorage.getItem('wmms_user') || 'null') return user?.id ?? null } catch { return null } } function readStoredOrganizationId() { const raw = sessionStorage.getItem(selectionStorageKey(currentUserId())) const id = Number(raw) return Number.isInteger(id) && id > 0 ? id : null } function writeStoredOrganizationId(id) { sessionStorage.setItem(selectionStorageKey(currentUserId()), String(id)) } export const useOrganizationStore = defineStore('organization', () => { const organizations = ref([]) const currentOrganizationId = ref(null) const currentWarehouseId = ref(null) const currentLocationId = ref(null) // 当前公司的仓库摘要(含库位),供切换器与页面展示,普通用户无需调用管理员仓库接口。 const currentWarehouse = ref(null) const contextVersion = ref(0) const switching = ref(false) // 写请求(保存)进行中标记:业务 store 在发起保存前 beginSaving()、结束后 endSaving()。 // 保存飞行中若切换公司,保存的成功/失败响应会因版本变化被视为作废并静默丢弃, // 用户既看不到成功也看不到失败,所以保存期间必须禁止切换。 const saving = ref(false) // 任何会导致“公司上下文变更或归属不明”的操作进行中都算忙:切换中或保存中。 const busy = computed(() => switching.value || saving.value) // 是否已完成一次公司列表加载,路由屏障据此决定是否需要先初始化。 const initialized = ref(false) // 最近一次 initialize 是否因临时故障失败:用于把“加载失败”与“确实没有可用公司”区分开, // 避免把网络异常误导成“请先选择要操作的公司”。 const loadError = ref(false) // 把 store 内展示用的版本号同步成上下文工具里的真实版本,两者始终一致。 function syncVersion() { contextVersion.value = captureOrganizationContext().version } // 仓库、库位的自动选择规则:只有唯一仓库启用时设置仓库 ID,其唯一库位也启用时才设置库位 ID。 // 缺配置、停用或失败时对应 ID 保持为空,绝不回退到旧公司的仓库或库位。 function applyWarehouseContext(warehouse) { currentWarehouse.value = warehouse || null // 先清空旧公司的仓库、库位,再按目标公司摘要设置。 currentWarehouseId.value = null currentLocationId.value = null if (warehouse && warehouse.status === 'active') { currentWarehouseId.value = warehouse.id const location = warehouse.location if (location && location.status === 'active') { currentLocationId.value = location.id } } } // 校验成功后才提交本地状态:先递增版本(同时取消旧读请求),再写公司 ID 和仓库、库位。 function applyContext(data) { const organizationId = data?.organization_id ?? null commitOrganizationId(organizationId) currentOrganizationId.value = organizationId applyWarehouseContext(data?.warehouse || null) syncVersion() if (organizationId) writeStoredOrganizationId(organizationId) } // 没有可用公司(或候选公司都失效)时进入空状态:公司 ID 保持为空, // 拦截器不会给任何组织级请求写组织头,业务页面也不挂载。 function applyEmptyContext() { commitOrganizationId(null) currentOrganizationId.value = null applyWarehouseContext(null) syncVersion() } // 用切换校验接口验证候选公司:该接口会返回目标公司的仓库摘要,顺带完成授权校验。 async function validateCandidate(organizationId) { try { const res = await authApi.switchOrganizationContext(organizationId, getOrganizationId() ?? 0) applyContext(res.data || {}) return true } catch (error) { // 只有 403(该用户对该候选公司确实无授权)才算“候选失效”,返回 false 交给调用方尝试下一个。 if (error?.response?.status === 403) return false // 网络异常、500 等临时故障必须向上抛出:若被当成候选失效,initialize 会落入空状态并 // 提示“请先选择要操作的公司”,把一次性故障误导成需要重新选公司。 throw error } } async function initialize() { loadError.value = false try { const res = await authApi.listOrganizations() const data = res.data || {} organizations.value = data.organizations || [] if (organizations.value.length === 0) { // 无授权公司:空状态,不发起任何校验请求。 applyEmptyContext() initialized.value = true return } // 本标签页上次选择优先,其次默认公司;两者都必须重新校验授权。 const defaultId = data.default_organization_id ?? null const candidates = [readStoredOrganizationId(), defaultId] const seen = new Set() for (const candidate of candidates) { if (candidate == null || seen.has(candidate)) continue seen.add(candidate) if (!organizations.value.some(org => org.id === candidate)) continue if (await validateCandidate(candidate)) { initialized.value = true return } } // 候选都失效但仍有授权公司:保留列表,要求用户重新选择。 applyEmptyContext() initialized.value = true } catch (error) { // 临时故障(列表拉取失败或候选校验非 403)不降级成“无公司”空状态: // 标记加载失败供 App 显示“加载失败”而非“请先选择公司”,并向上抛出交由调用方重试。 loadError.value = true throw error } } // 业务 store 复用的“写请求进行中”开关:发起保存前置位、结束后复位(务必放在 finally)。 function beginSaving() { saving.value = true } function endSaving() { saving.value = false } async function switchOrganization(organizationId) { // 切换或保存进行中禁止再次切换:避免并发切换互相覆盖,以及写请求归属不明确。 // 保存飞行中切换会让保存的响应被版本判定作废并被静默吞掉,用户既看不到成功也看不到失败。 if (busy.value) return if (!organizationId || organizationId === currentOrganizationId.value) return switching.value = true try { const res = await authApi.switchOrganizationContext( organizationId, currentOrganizationId.value ?? 0 ) // 校验成功后才提交;抛错时保持旧公司不变,由调用方决定是否提示。 applyContext(res.data || {}) } finally { switching.value = false } } // 登出或切换账号时清空:上下文、公司列表、仓库库位与初始化标记全部复位。 function reset() { resetOrganizationContext() organizations.value = [] currentOrganizationId.value = null currentWarehouse.value = null currentWarehouseId.value = null currentLocationId.value = null switching.value = false saving.value = false initialized.value = false loadError.value = false syncVersion() } return { organizations, currentOrganizationId, currentWarehouseId, currentLocationId, currentWarehouse, contextVersion, switching, saving, busy, initialized, loadError, beginSaving, endSaving, initialize, switchOrganization, reset } })