| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- /* ==============================================================
- * utils/indicator-favorite.js - 关注指标收藏(方案A:后端持久化)
- * 主路径:调后端 /api/indicator/favorite/*(JWT 自动附带,跨设备同步)
- * 降级读:后端不可用/离线/未登录时回落到本地 Storage 旧 key(方案B 兼容)
- * 隔离维度:userId × familyId × memberId × sourceType × definitionId
- * (前四维决定 Storage key,后端另加 definitionId 作唯一键)
- *
- * ⚠️ readFavorites / toggleFavorite / removeFavorite 自方案A 起为**异步**(返回 Promise),
- * 调用方必须 .then() 消费;writeFavorites 仍为同步(仅写本地缓存,供降级读)。
- * ============================================================== */
- import { toggleIndicatorFavorite, listIndicatorFavorites, removeIndicatorFavorite } from './api.js'
- const KEY_PREFIX = 'cfc:indicator:fav'
- /**
- * 拼装存储 key(维度隔离;缺失维度用占位符,避免不同账号/成员互相覆盖)
- * @param {{userId?:*, familyId?:*, memberId?:*, sourceType?:*}} scope
- * @returns {string}
- */
- export function buildFavoriteKey(scope) {
- var s = scope || {}
- var userId = (s.userId === undefined || s.userId === null || s.userId === '') ? 'anon' : String(s.userId)
- var familyId = (s.familyId === undefined || s.familyId === null || s.familyId === '') ? 'nofamily' : String(s.familyId)
- var memberId = (s.memberId === undefined || s.memberId === null || s.memberId === '') ? 'nomember' : String(s.memberId)
- var sourceType = (s.sourceType === undefined || s.sourceType === null || s.sourceType === '') ? 'all' : String(s.sourceType)
- return KEY_PREFIX + ':' + userId + ':' + familyId + ':' + memberId + ':' + sourceType
- }
- /** 数字数组去重、丢弃非法值 */
- function normalizeIds(val) {
- if (val === undefined || val === null || val === '') return []
- if (typeof val === 'string') {
- try { val = JSON.parse(val) } catch (e) { return [] }
- }
- if (!Array.isArray(val)) return []
- var out = []
- var seen = {}
- for (var i = 0; i < val.length; i++) {
- var n = Number(val[i])
- if (isNaN(n)) continue
- if (seen[n]) continue
- seen[n] = true
- out.push(n)
- }
- return out
- }
- /**
- * 读取本地缓存(同步,仅作降级读/乐观更新快照)
- * @param {Object} scope
- * @returns {number[]}
- */
- function readLocal(scope) {
- var key = buildFavoriteKey(scope)
- var val
- try {
- val = uni.getStorageSync(key)
- } catch (e) {
- return []
- }
- return normalizeIds(val)
- }
- /**
- * 写入本地缓存(同步;后端成功结果/乐观更新都会回写,供离线降级读)
- * @param {Object} scope
- * @param {number[]} ids
- * @returns {number[]} 写入后的规范化列表
- */
- function writeLocal(scope, ids) {
- var key = buildFavoriteKey(scope)
- var arr = normalizeIds(ids)
- try {
- uni.setStorageSync(key, arr)
- } catch (e) {
- // 存储失败静默(收藏是增强功能,不影响主流程)
- console.warn('[indicator-favorite] 写入失败', key, e)
- }
- return arr
- }
- /**
- * 写入某视角的本地收藏缓存(同步)。
- * 方案A 起仅供降级读与测试使用,正常收藏路径走 toggleFavorite/removeFavorite。
- * @param {Object} scope
- * @param {number[]} ids
- * @returns {number[]} 写入后的规范化列表
- */
- export function writeFavorites(scope, ids) {
- return writeLocal(scope, ids)
- }
- /** scope 是否具备调后端的最小维度(memberId + sourceType) */
- function canUseBackend(scope) {
- var s = scope || {}
- var memberId = Number(s.memberId)
- return !!memberId && !isNaN(memberId) && !!s.sourceType
- }
- /**
- * 静默调用(失败返回 null,不抛异常、不弹 toast):
- * api.js 三函数已带 _noToast,这里只做「非 200 / 网络错误 / 无 data」统一归一为 null。
- */
- function silentCall(fn, params) {
- var p
- try {
- p = fn(params)
- } catch (e) {
- return Promise.resolve(null)
- }
- return Promise.resolve(p).then(function (res) {
- if (res && res.code === 200 && res.data) return res.data
- return null
- }, function () {
- return null
- })
- }
- /**
- * 读取某视角的收藏指标 definitionId 列表(异步):
- * 优先后端;后端不可用/未登录/缺维度时降级读本地 Storage。
- * @param {Object} scope
- * @returns {Promise<number[]>}
- */
- export function readFavorites(scope) {
- if (!canUseBackend(scope)) {
- return Promise.resolve(readLocal(scope))
- }
- var params = { memberId: Number(scope.memberId), sourceType: scope.sourceType }
- return silentCall(listIndicatorFavorites, params).then(function (data) {
- if (data && Array.isArray(data.definitionIds)) {
- // 回写本地缓存:下次离线/后端不可用时可降级读
- return writeLocal(scope, data.definitionIds)
- }
- return readLocal(scope)
- })
- }
- /**
- * 切换某指标的收藏状态(异步):乐观更新本地 → 调后端 → 失败回滚。
- * @param {Object} scope
- * @param {number|string} definitionId
- * @returns {Promise<{ids:number[], added:boolean}>} added=true 表示新加入收藏
- */
- export function toggleFavorite(scope, definitionId) {
- var id = Number(definitionId)
- if (isNaN(id)) {
- return Promise.resolve({ ids: readLocal(scope), added: false })
- }
- var before = readLocal(scope)
- // 乐观更新本地(后端失败时回滚到 before)
- var optimistic = before.slice()
- var idx = optimistic.indexOf(id)
- if (idx >= 0) optimistic.splice(idx, 1)
- else optimistic.push(id)
- writeLocal(scope, optimistic)
- if (!canUseBackend(scope)) {
- // 未登录/无成员视角:本地模式(与方案B 行为一致)
- return Promise.resolve({ ids: optimistic, added: idx < 0 })
- }
- var params = { memberId: Number(scope.memberId), sourceType: scope.sourceType, definitionId: id }
- return silentCall(toggleIndicatorFavorite, params).then(function (data) {
- if (!data) {
- // 后端不可用:回滚本地,保持与后端一致的「未变更」语义
- writeLocal(scope, before)
- return { ids: before, added: false }
- }
- // 以后端返回的 added 为准同步本地缓存(本地缓存可能已过期)
- var synced = before.slice()
- var i = synced.indexOf(id)
- if (data.added === true) {
- if (i < 0) synced.push(id)
- } else if (i >= 0) {
- synced.splice(i, 1)
- }
- writeLocal(scope, synced)
- return { ids: normalizeIds(synced), added: data.added === true }
- })
- }
- /**
- * 显式移除收藏(异步):乐观更新本地 → 调后端 /remove → 失败回滚。
- * 与 toggle 的区别:不做状态翻转判断,后端幂等(不存在也视作已删除)。
- * @param {Object} scope
- * @param {number|string} definitionId
- * @returns {Promise<{ids:number[], removed:boolean}>}
- */
- export function removeFavorite(scope, definitionId) {
- var id = Number(definitionId)
- if (isNaN(id)) {
- return Promise.resolve({ ids: readLocal(scope), removed: false })
- }
- var before = readLocal(scope)
- var optimistic = before.slice()
- var idx = optimistic.indexOf(id)
- if (idx >= 0) optimistic.splice(idx, 1)
- writeLocal(scope, optimistic)
- if (!canUseBackend(scope)) {
- return Promise.resolve({ ids: optimistic, removed: true })
- }
- var params = { memberId: Number(scope.memberId), sourceType: scope.sourceType, definitionId: id }
- return silentCall(removeIndicatorFavorite, params).then(function (data) {
- if (!data) {
- writeLocal(scope, before)
- return { ids: before, removed: false }
- }
- return { ids: normalizeIds(optimistic), removed: data.removed !== false }
- })
- }
|