Răsfoiți Sursa

feat:sim卡信息管理新增日增量列

Jerry Liang 2 luni în urmă
părinte
comite
3b0cdf16d5

+ 4 - 0
src/App.vue

@@ -14,6 +14,7 @@ import { RouterView } from 'vue-router'
 import Noise from '@/components/Noise.vue'
 import { getConfigProviderProps } from '@/composable/useDiscreteApi'
 import { useConfigureStore } from '@/stores/configure'
+import { simCardCacheDB } from '@/service/simCardManage/simCardCache'
 
 const configureStore = useConfigureStore()
 const configProviderProps = getConfigProviderProps()
@@ -28,6 +29,9 @@ import { ref, onMounted } from 'vue';
 const configProvider = ref(null)
 
 onMounted(() => {
+  // 清理过期的 SIM 卡月数据缓存
+  simCardCacheDB.cleanup()
+
   if (configProvider.value) {
     console.log(configProvider.value);  // 直接获取当前组件实例的 attrs
   }

+ 78 - 0
src/service/simCardManage/simCardCache.ts

@@ -0,0 +1,78 @@
+import { openDB, type IDBPDatabase } from 'idb'
+import type { SimCardMonthlyData } from './interface'
+
+const DB_NAME = 'sim-card-cache'
+const DB_VERSION = 1
+const STORE_NAME = 'monthly-data'
+
+interface CacheEntry {
+  key: string // `${giccid}_${month}`
+  data: SimCardMonthlyData
+  expireAt: number // 过期时间戳(次日0点)
+}
+
+let dbPromise: Promise<IDBPDatabase> | null = null
+
+function getDB() {
+  if (!dbPromise) {
+    dbPromise = openDB(DB_NAME, DB_VERSION, {
+      upgrade(db) {
+        if (!db.objectStoreNames.contains(STORE_NAME)) {
+          const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' })
+          store.createIndex('expireAt', 'expireAt')
+        }
+      },
+    })
+  }
+  return dbPromise
+}
+
+/** 计算次日 0 点的时间戳 */
+function getNextMidnight(): number {
+  const now = new Date()
+  return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime()
+}
+
+export const simCardCacheDB = {
+  /**
+   * 从缓存获取月数据(惰性过期:读取时检查 TTL)
+   * 过期返回 null
+   */
+  async get(giccid: string, month: string): Promise<SimCardMonthlyData | null> {
+    const db = await getDB()
+    const key = `${giccid}_${month}`
+    const entry: CacheEntry | undefined = await db.get(STORE_NAME, key)
+    if (!entry) return null
+    if (Date.now() > entry.expireAt) {
+      // 惰性删除过期条目
+      await db.delete(STORE_NAME, key)
+      return null
+    }
+    return entry.data
+  },
+
+  /** 写入缓存,TTL 设为次日 0 点 */
+  async set(giccid: string, month: string, data: SimCardMonthlyData): Promise<void> {
+    const db = await getDB()
+    const entry: CacheEntry = {
+      key: `${giccid}_${month}`,
+      data,
+      expireAt: getNextMidnight(),
+    }
+    await db.put(STORE_NAME, entry)
+  },
+
+  /** App 启动时调用:批量清除所有过期条目 */
+  async cleanup(): Promise<void> {
+    const db = await getDB()
+    const tx = db.transaction(STORE_NAME, 'readwrite')
+    const index = tx.store.index('expireAt')
+    // 遍历所有 expireAt <= now 的条目并删除
+    let cursor = await index.openCursor(IDBKeyRange.upperBound(Date.now()))
+    while (cursor) {
+      await cursor.delete()
+      cursor = await cursor.continue()
+    }
+    await tx.done
+  },
+}

+ 30 - 3
src/views/sim-card-manage/index.vue

@@ -103,10 +103,11 @@ import {
   NForm,
   NFormItem,
   NScrollbar,
+  NSpin,
 } from 'naive-ui'
 import type { PaginationProps, DataTableColumns, SelectOption } from 'naive-ui'
 import { useFullCard } from '@/composable/comp/useFullCard'
-import { getSimCardInfo, getSimCardMonthlyData } from '@/service/simCardManage'
+import { getSimCardInfo } from '@/service/simCardManage'
 import type {
   SimCardInfo,
   SimCardMonthlyData,
@@ -115,10 +116,14 @@ import type {
 } from '@/service/simCardManage/interface'
 import SimCardDataView from './component/simCardDataView.vue'
 import SiteInfoView from '../site-info-manage/component/siteInfoView.vue'
+import { useSimCardMonthlyCache, cacheKey } from './useSimCardMonthlyCache'
 
 const dialog = useDialog()
 const message = useMessage()
 
+const { memoryCache, loadingSet, computeDailyIncrement, onPageChange, getWithPriority } =
+  useSimCardMonthlyCache()
+
 const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
 const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
 const siteInfoViewRef = useTemplateRef<InstanceType<typeof SiteInfoView>>('siteInfoViewRef')
@@ -207,7 +212,7 @@ function createColumns(): DataTableColumns<SimCardInfo> {
                 {
                   text: true,
                   onClick: () => {
-                    getSimCardMonthlyData(rowData.giccid, rowData.month)
+                    getWithPriority(rowData.giccid, rowData.month)
                       .then((resp) => {
                         simCardData.value = resp
                         showSimCardDataView.value = true
@@ -297,6 +302,23 @@ function createColumns(): DataTableColumns<SimCardInfo> {
         return (rowData.allDataUsage / 1024).toFixed(2)
       },
     },
+    {
+      title: 'Increment (MB)',
+      key: 'increment',
+      minWidth: 130,
+      render(rowData) {
+        const key = cacheKey(rowData.giccid, rowData.month)
+
+        if (loadingSet.has(key) || !memoryCache.has(key)) {
+          return h(NSpin, { size: 'small' })
+        }
+
+        const increment = computeDailyIncrement(rowData.giccid, rowData.month)
+        if (increment === null) return '-'
+        const valueMB = (increment / 1024).toFixed(2)
+        return increment >= 0 ? `+${valueMB}` : valueMB
+      },
+    },
     {
       title: 'UpdateTime',
       key: 'updateTime',
@@ -321,7 +343,7 @@ const optionsPlatform: SelectOption[] = [
 ]
 
 const tableData = ref<SimCardInfo[]>([])
-const dataTableMaxWidth = ref<number | undefined>(1120)
+const dataTableMaxWidth = ref<number | undefined>(1250)
 const tableColumns = computed<DataTableColumns<SimCardInfo>>(createColumns)
 const tablePagination = reactive<PaginationProps>({
   simple: false,
@@ -374,6 +396,11 @@ const querySimCardInfoList = async (
     tableData.value = []
   }
   tablePagination.itemCount = responseList.total
+
+  // 触发预加载队列(已缓存的会被跳过)
+  if (tableData.value.length > 0) {
+    onPageChange(tableData.value)
+  }
 }
 
 const handleListQuery = async () => {

+ 177 - 0
src/views/sim-card-manage/useSimCardMonthlyCache.ts

@@ -0,0 +1,177 @@
+import { reactive } from 'vue'
+import { getSimCardMonthlyData } from '@/service/simCardManage'
+import { simCardCacheDB } from '@/service/simCardManage/simCardCache'
+import type { SimCardMonthlyData, SimCardInfo } from '@/service/simCardManage/interface'
+
+/** 响应式内存缓存 key: `${giccid}_${month}` */
+const memoryCache = reactive(new Map<string, SimCardMonthlyData>())
+
+/** 正在请求中的 key 集合(响应式,驱动表格 loading 状态) */
+const loadingSet = reactive(new Set<string>())
+
+// ─── 队列控制 ───
+let fetchQueue: Array<{ giccid: string; month: string }> = []
+let currentVersion = 0
+let isProcessing = false
+
+// ─── 工具函数 ───
+
+export function cacheKey(giccid: string, month: string): string {
+  return `${giccid}_${month}`
+}
+
+/** 获取上月 month 字符串 (输入 "2026-08" → 输出 "2026-07") */
+function getPrevMonth(month: string): string {
+  const [year, mon] = month.split('-').map(Number)
+  const date = new Date(year, mon - 2, 1)
+  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
+}
+
+// ─── 核心:请求单条数据并写入双层缓存 ───
+
+async function fetchAndCache(giccid: string, month: string): Promise<SimCardMonthlyData> {
+  const key = cacheKey(giccid, month)
+
+  // 1. 内存缓存命中
+  const memHit = memoryCache.get(key)
+  if (memHit) return memHit
+
+  // 2. IndexedDB 缓存命中(带惰性过期)
+  const dbHit = await simCardCacheDB.get(giccid, month)
+  if (dbHit) {
+    memoryCache.set(key, dbHit)
+    return dbHit
+  }
+
+  // 3. 网络请求
+  loadingSet.add(key)
+  try {
+    const data = await getSimCardMonthlyData(giccid, month)
+    memoryCache.set(key, data)
+    await simCardCacheDB.set(giccid, month, data)
+    return data
+  } finally {
+    loadingSet.delete(key)
+  }
+}
+
+// ─── 队列处理循环 ───
+
+async function processQueue(version: number) {
+  if (isProcessing) return
+  isProcessing = true
+
+  while (fetchQueue.length > 0) {
+    // 版本号不匹配 → 当前请求完成后停止
+    if (currentVersion !== version) break
+
+    const item = fetchQueue.shift()!
+    const key = cacheKey(item.giccid, item.month)
+
+    // 已在内存中则跳过
+    if (memoryCache.has(key)) continue
+
+    await fetchAndCache(item.giccid, item.month)
+
+    // 检查版本号是否仍有效(fetchAndCache 是异步的)
+    if (currentVersion !== version) break
+
+    // 如果获取到的数据最新日是1号,追加上月数据到队列
+    const data = memoryCache.get(key)
+    if (data) {
+      const days = Object.keys(data.dailyUsage).map(Number)
+      if (days.length > 0) {
+        const latestDay = Math.max(...days)
+        if (latestDay === 1) {
+          const prevMonth = getPrevMonth(item.month)
+          const prevKey = cacheKey(item.giccid, prevMonth)
+          if (!memoryCache.has(prevKey)) {
+            fetchQueue.push({ giccid: item.giccid, month: prevMonth })
+          }
+        }
+      }
+    }
+  }
+
+  isProcessing = false
+}
+
+// ─── 计算日增量 ───
+
+/**
+ * 计算某 SIM 卡当月的日增量
+ * - 取最新一天的用量 - 前一天的用量
+ * - 1号时需要上月最后一天数据
+ * - 前一日数据不存在返回 null(展示 "-")
+ */
+function computeDailyIncrement(giccid: string, month: string): number | null {
+  const key = cacheKey(giccid, month)
+  const data = memoryCache.get(key)
+  if (!data) return null
+
+  const days = Object.keys(data.dailyUsage).map(Number).sort((a, b) => a - b)
+  if (days.length === 0) return null
+
+  const latestDay = days[days.length - 1]
+  const latestUsage = data.dailyUsage[latestDay].dataUsage
+
+  if (latestDay === 1) {
+    // 1日:读取上月最后一天
+    const prevMonth = getPrevMonth(month)
+    const prevKey = cacheKey(giccid, prevMonth)
+    const prevData = memoryCache.get(prevKey)
+    if (!prevData) return null
+
+    const prevDays = Object.keys(prevData.dailyUsage).map(Number).sort((a, b) => a - b)
+    if (prevDays.length === 0) return null
+
+    const lastDayOfPrevMonth = prevDays[prevDays.length - 1]
+    return latestUsage - prevData.dailyUsage[lastDayOfPrevMonth].dataUsage
+  } else {
+    // 非1日:取前一天
+    const prevDayData = data.dailyUsage[latestDay - 1]
+    if (!prevDayData) return null
+    return latestUsage - prevDayData.dataUsage
+  }
+}
+
+// ─── 对外暴露的方法 ───
+
+/**
+ * 翻页 / 搜索后调用:重建预加载队列
+ * 已缓存的数据会被跳过,只补充未缓存的
+ */
+function onPageChange(simCards: SimCardInfo[]) {
+  currentVersion++
+  const version = currentVersion
+
+  // 重建队列
+  fetchQueue = []
+  for (const sim of simCards) {
+    const key = cacheKey(sim.giccid, sim.month)
+    if (!memoryCache.has(key) && !loadingSet.has(key)) {
+      fetchQueue.push({ giccid: sim.giccid, month: sim.month })
+    }
+  }
+
+  processQueue(version)
+}
+
+/**
+ * 点击查看月数据时调用(优先级最高)
+ * 直接发请求,不等待队列
+ */
+async function getWithPriority(giccid: string, month: string): Promise<SimCardMonthlyData> {
+  return await fetchAndCache(giccid, month)
+}
+
+export function useSimCardMonthlyCache() {
+  return {
+    memoryCache,
+    loadingSet,
+    cacheKey,
+    computeDailyIncrement,
+    onPageChange,
+    getWithPriority,
+  }
+}