Skip to content

Latest commit

 

History

History
562 lines (501 loc) · 29.5 KB

File metadata and controls

562 lines (501 loc) · 29.5 KB

模型购买统计 Web 项目计划文档

一、项目概述

1.1 项目目标

构建一个纯本地运行的 Web 应用,用于统计和管理模型购买记录。用户可录入每次购买的模型信息、支付金额与状态,系统自动计算补款价格并汇总统计,支持按月/年筛选查看。支持通过 JSON 文件导入历史记录,也可将当前数据导出为 JSON/Excel 文件,形成完整的备份与迁移链路。每条记录可上传模型图片附件(base64 存储),便于直观识别与管理。支持明亮/暗黑主题切换,主题偏好持久化。

1.2 技术栈

技术 版本建议 用途
Vite ^5.x 构建工具与开发服务器
Vue 3 ^3.4.x 前端框架(Composition API + <script setup>)
Element Plus ^2.x UI 组件库(表单/表格/日期选择器/选择器)
Pinia ^2.x 状态管理(持久化到 localStorage)
TypeScript ^5.x 类型安全(可选但推荐)
localStorage - 本地数据持久化(无服务器)
xlsx(SheetJS) ^0.18.x Excel 文件导出(.xlsx)
localforage ^1.10.x IndexedDB 封装,存储模型图片(base64)
ECharts ^5.x 图表统计(柱状图:模型数量 + 补款总价)
VueUse ^10.x 工具函数库(用于 useDark 主题切换)

1.3 运行环境约束

  • 纯本地项目,不依赖任何后端服务器
  • 记录数据存储于浏览器 localStorage(轻量结构化数据)
  • 模型图片存储于浏览器 IndexedDB(大容量二进制数据,避免挤占 localStorage 配额)
  • 通过 npm run dev 启动本地开发服务器,npm run build 生成静态产物
  • 支持离线使用(构建后直接用浏览器打开 index.html 或通过静态服务器访问)

二、需求分析

2.1 输入字段定义

字段 类型 是否必填 说明
模型名称 string 必填 文本输入
购买渠道 enum/string 必填 淘宝 / pdd / jd / 自定义;选"自定义"时需手动输入渠道名称
已支付人民币 number 必填 数值,≥ 0;预购状态下即"定金"
购买状态 enum 必填 预购 / 全款
全款价格 number 预购必填 仅预购状态显示
邮费 number 预购必填 仅预购状态显示,默认 0
支付日期 date 选填 未填写时默认本地当前时间
补款日期 date 预购必填 仅预购状态显示
水贴记录 string 选填 文本输入,记录该模型对应的水贴信息(如水贴名称/品牌/链接等);留空视为无水贴
模型图片 File[] 选填 支持上传多张图片(最多 5 张,单张原图 ≤ 5MB),压缩后存入 IndexedDB;表格中显示首张缩略图

2.2 计算规则

补款价格 (仅预购状态):

补款价格 = 全款价格 + 邮费 - 已支付人民币
  • 若结果 < 0,提示用户检查输入(异常但不阻断)
  • 全款状态:补款价格 = 0,补款日期为空

2.3 输出表格列

列名 数据来源 说明
模型名称 输入 文本
购买渠道 输入 标签样式展示;自定义渠道直接显示文本
已支付人民币 输入 货币格式 ¥#.00
支付日期 输入/默认 YYYY-MM-DD
购买状态 输入 标签样式区分(预购=warning,全款=success)
补款价格 自动计算 货币格式;全款显示 "-"
补款日期 输入 全款显示 "-"
水贴 输入 有内容显示"有"标签(success);留空显示"无"标签(info);hover 显示完整水贴记录
模型图片 输入 显示首张缩略图(48×48);无图显示占位图标;点击缩略图弹窗预览全部图片

2.4 统计汇总(表格最下方)

  • 总价:所有记录的 已支付人民币 之和 + 所有预购记录的 补款价格 之和
    • 即:总价 = Σ已支付人民币 + Σ补款价格
    • 含义:全部模型购买的总支出(含定金与补款)
  • 待支付价格:所有预购记录的 补款价格 之和
    • 即:待支付价格 = Σ补款价格(仅预购)
  • 统计需根据当前筛选条件动态计算

2.5 筛选与搜索功能

  • 按年筛选:下拉选择年份(数据中最早年份 ~ 当前年份),默认全部
  • 按月筛选:下拉选择月份(1-12),默认全部;仅当选择具体年份后月份筛选生效
  • 按购买状态筛选:下拉选择 全部 / 预购 / 全款,默认全部
    • 用于快速区分待补款记录(预购)与已结清记录(全款)
  • 按是否有水贴筛选:下拉选择 全部 / 有水贴 / 无水贴,默认全部
    • 用于快速筛选已配备水贴的模型,便于制作计划
  • 按模型名称搜索:文本输入框,支持模糊匹配(不区分大小写)
    • 输入内容实时过滤,无需点击搜索按钮
    • 匹配规则:模型名称包含输入字符串即命中(使用 includes + toLowerCase)
    • 支持中英文混合搜索
  • 筛选/搜索依据:年月筛选基于支付日期,状态筛选基于购买状态,水贴筛选基于水贴记录是否为空,名称搜索基于模型名称
  • 筛选条件为"与"关系:年月 + 状态 + 水贴 + 名称同时满足才显示
  • 筛选结果实时更新表格与统计

2.6 图表统计

  • 图表类型:柱状图(ECharts)
  • 统计维度:模型数量 + 补款总价(双系列分组柱状图)
  • 联动筛选:与 FilterBar 的年/月筛选条件联动,筛选变化时图表实时更新
  • X 轴动态规则:
    • 未选年份(全部):X 轴为各年份(如 2024 / 2025 / 2026),统计每年的数量与补款总价
    • 选了具体年份、未选月份:X 轴为 1-12 ,统计该年每月数据
    • 选了具体年份 + 月份:X 轴为该月的各日期(1-31),统计每日数据
  • Y 轴:
    • 左 Y 轴:模型数量(整数,柱状图系列 1)
    • 右 Y 轴:补款总价(人民币,柱状图系列 2)
  • 交互:hover 显示 tooltip(年份/月份 + 数量 + 总价);图例可切换显示/隐藏某一系列

三、数据模型设计

3.1 TypeScript 类型定义

// 购买状态枚举
type PurchaseStatus = 'preorder' | 'fullpaid'

// 预置购买渠道
type PresetChannel = 'taobao' | 'pdd' | 'jd' | 'custom'

// 单条购买记录
interface PurchaseRecord {
  id: string                 // 唯一标识(时间戳+随机串)
  modelName: string          // 模型名称
  channel: PresetChannel     // 购买渠道(淘宝/pdd/jd/自定义)
  customChannel?: string     // 自定义渠道名称(仅 channel='custom' 时填写)
  paidAmount: number         // 已支付人民币(预购时为定金)
  status: PurchaseStatus     // 购买状态
  fullPrice?: number         // 全款价格(仅预购)
  shippingFee?: number       // 邮费(仅预购,默认0)
  paymentDate: string        // 支付日期(YYYY-MM-DD)
  supplementDate?: string    // 补款日期(仅预购)
  decalRecord?: string       // 水贴记录(选填,水贴名称/品牌/链接等;留空视为无水贴)
  imageIds?: string[]        // 模型图片 ID 数组(选填,最多 5 张;实际 base64 数据存 IndexedDB)
  createdAt: number          // 创建时间戳
}

// 计算属性(不存储,派生)
interface ComputedRecord extends PurchaseRecord {
  supplementAmount: number   // 补款价格(预购=fullPrice+shippingFee-paidAmount;全款=0)
}

3.2 localStorage 存储(记录数据)

  • Key: modeling-statistics:records
  • Value: PurchaseRecord[] 的 JSON 字符串(不含图片二进制,仅含 imageIds 引用)
  • 读写时机:Pinia store action 中自动同步
  • 容量预估:单条约 0.5KB,localStorage 5-10MB 可存上万条,无压力

3.3 IndexedDB 存储(图片数据)

  • 使用 localforage 封装,Promise 风格 API
  • Store 名称:images
  • Key:图片 ID(与 imageIds 中的 ID 一致,格式 img_${timestamp}_${random})
  • Value:结构化对象,包含原图与缩略图双份数据
    interface StoredImage {
      id: string
      originalBlob: Blob       // 压缩后原图 Blob(用于预览)
      thumbnail: string        // 缩略图 base64(≤ 10KB,表格直显,避免频繁读 IDB)
      mimeType: string         // 'image/webp' | 'image/jpeg' | 'image/png'
      width: number            // 原图宽度
      height: number           // 原图高度
      createdAt: number
    }
  • 存储优势:
    • Blob 代替 base64:省去 33% 体积膨胀,读写性能更优
    • 缩略图分离:表格用内联 base64(已加载到内存),无需每次读 IndexedDB
    • 预览用 Blob:通过 URL.createObjectURL(blob) 生成临时 URL,显示完毕 revokeObjectURL 释放
  • 容量优势:IndexedDB 配额通常为数百 MB ~ 数 GB,远超 localStorage
  • 读写时机:
    • 上传图片时:localforage.setItem(id, { originalBlob, thumbnail, ... })
    • 表格渲染时:从记录的 imageIds 取首张 ID,getItem 后直接用 thumbnail 字段
    • 点击预览时:读取 originalBlob,createObjectURL 后传入 el-image-viewer
    • 删除记录时:遍历 imageIds 调用 removeItem(id) 清理

3.4 JSON 导入文件格式

导入文件为标准 JSON 数组,结构与 PurchaseRecord 一致(可省略 id / createdAt,导入时自动补全):

[
  {
    "modelName": "示例模型A",
    "channel": "taobao",
    "paidAmount": 100,
    "status": "preorder",
    "fullPrice": 500,
    "shippingFee": 20,
    "paymentDate": "2026-01-15",
    "supplementDate": "2026-03-01"
  },
  {
    "modelName": "示例模型B",
    "channel": "jd",
    "paidAmount": 300,
    "status": "fullpaid",
    "paymentDate": "2026-02-10"
  }
]

四、项目结构

modeling-statistics/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.json
├── plan.md                       # 本文档
└── src/
    ├── main.ts                   # 应用入口
    ├── App.vue                   # 根组件(布局)
    ├── types/
    │   └── index.ts              # 类型定义
    ├── stores/
    │   └── records.ts            # Pinia store(CRUD + 持久化)
    ├── utils/
    │   ├── calculate.ts          # 补款/统计计算函数
    │   ├── date.ts               # 日期格式化工具
    │   ├── image.ts              # 图片处理(压缩+base64 转换)
    │   └── storage.ts            # IndexedDB 封装(localforage 配置+CRUD)
    ├── components/
    │   ├── RecordForm.vue        # 录入表单(含条件字段)
    │   ├── RecordTable.vue       # 统计表格(含汇总行)
    │   ├── FilterBar.vue         # 年月筛选栏 + 模型名称搜索
    │   ├── StatSummary.vue       # 统计卡片(总价/待支付)
    │   ├── ImportButton.vue      # JSON 导入按钮(读取文件+校验+合并)
    │   ├── ExportButton.vue      # 数据导出按钮(JSON/Excel 双格式)
    │   ├── StatsChart.vue        # ECharts 柱状图(数量+补款总价,联动筛选)
    │   └── ThemeToggle.vue       # 明亮/暗黑主题切换按钮
    └── views/
        └── Home.vue              # 主页面(组合上述组件)

五、核心实现要点

5.1 Pinia Store 设计

// stores/records.ts
export const useRecordsStore = defineStore('records', () => {
  const records = ref<PurchaseRecord[]>(loadFromStorage())

  // 派生:带计算字段的列表
  const computedRecords = computed(() =>
    records.value.map(r => ({ ...r, supplementAmount: calcSupplement(r) }))
  )

  function addRecord(payload: Omit<PurchaseRecord, 'id' | 'createdAt'>) { ... }
  function updateRecord(id: string, payload: Partial<PurchaseRecord>) { ... }
  function deleteRecord(id: string) { ... }

  // 监听变化自动持久化
  watch(records, (val) => saveToStorage(val), { deep: true })

  return { records, computedRecords, addRecord, updateRecord, deleteRecord }
})

5.2 表单条件渲染逻辑

  • 监听 status 字段变化
    • status === 'preorder' 时,动态显示 全款价格邮费补款日期 字段并设为必填
    • 当切换为 fullpaid 时,清空上述字段
  • 监听 channel 字段变化
    • channel === 'custom' 时,动态显示 自定义渠道名称 文本输入框并设为必填
    • 切换为其他预置渠道时,清空 customChannel
  • 补款价格在表单中实时预览(只读展示),提交时由后端计算存储

5.3 表格统计行实现

  • 使用 Element Plus Table 的 summary-method 自定义合计行
  • 合计行显示在表格最下方,包含:总价、待支付价格
  • 合计行根据当前筛选后的数据计算

5.4 筛选与搜索实现

  • FilterBar 组件维护 yearmonthstatusdecalkeyword 五个响应式变量
  • 通过 computed 派生 filteredRecords,依次应用年月、状态、水贴筛选与名称搜索
  • 年份选项从数据中提取,月份固定 1-12,状态固定 预购/全款,水贴固定 /
  • 过滤逻辑:
    const filteredRecords = computed(() => {
      return store.computedRecords.filter(r => {
        // 年月筛选(基于 paymentDate)
        const date = new Date(r.paymentDate)
        const yearMatch = !year.value || date.getFullYear() === year.value
        const monthMatch = !month.value || (date.getMonth() + 1) === month.value
        // 状态筛选(基于 status)
        const statusMatch = !status.value || r.status === status.value
        // 水贴筛选(基于 decalRecord 是否为空)
        const hasDecal = !!r.decalRecord?.trim()
        const decalMatch = !decal.value ||
          (decal.value === 'yes' && hasDecal) ||
          (decal.value === 'no' && !hasDecal)
        // 名称搜索(基于 modelName,不区分大小写)
        const keywordMatch = !keyword.value ||
          r.modelName.toLowerCase().includes(keyword.value.toLowerCase())
        return yearMatch && monthMatch && statusMatch && decalMatch && keywordMatch
      })
    })
  • keyword 输入使用 debounce(约 200ms)避免高频过滤
  • 统计行与表格共用同一 filteredRecords,保证数据一致

5.5 JSON 导入实现

  • 使用 <input type="file" accept=".json"> 触发文件选择
  • 通过 FileReader.readAsText 读取文件内容
  • 解析与校验流程:
    1. JSON.parse 解析,失败则提示"文件格式错误,请选择有效的 JSON 文件"
    2. 校验为数组,且每项包含 modelName / channel / paidAmount / status 必填字段
    3. 校验 channel 为合法枚举值,statuspreorder / fullpaid
    4. 预购记录需有 fullPrice / shippingFee / supplementDate
    5. 缺失 id / createdAt 时自动补全;paymentDate 为空时填当前日期
  • 导入策略:提供两种模式供用户选择
    • 追加导入(默认):新数据追加到现有记录末尾
    • 覆盖导入:清空现有数据后替换为导入内容(需二次确认)
  • 导入成功后通过 ElMessage 提示导入条数,并自动持久化

5.6 数据导出实现(JSON / Excel)

  • 顶部"导出"按钮提供下拉菜单,可选 导出 JSON / 导出 Excel
  • 导出范围:默认导出当前筛选后的记录(与表格展示一致);提供"导出全部"选项
  • JSON 导出:
    • JSON.stringify(records, null, 2) 格式化输出
    • 通过 Blob + URL.createObjectURL + <a download> 触发下载
    • 文件名:modeling-records-YYYYMMDD-HHmmss.json
  • Excel 导出(使用 SheetJS xlsx 库):
    • 列定义:模型名称 / 购买渠道 / 已支付人民币 / 支付日期 / 购买状态 / 补款价格 / 补款日期
    • 自定义渠道显示完整名称(如 自定义:闲鱼)
    • 全款记录的补款价格/补款日期显示为 -
    • 表尾追加合计行:总价、待支付价格
    • 文件名:modeling-records-YYYYMMDD-HHmmss.xlsx
  • 空数据保护:无记录时禁用导出按钮并提示"暂无可导出的数据"

5.7 模型图片附件上传实现(IndexedDB + Blob + 智能压缩)

  • 表单中使用 Element Plus el-upload 组件(:auto-upload="false")选择本地图片
  • 图片处理流程(双份输出:原图 Blob + 缩略图 base64):
    1. 选择图片后通过 createImageBitmap(file) 解码(性能优于 Image 对象)
    2. 格式智能选择:
      • 检测浏览器 canvas.toDataURL('image/webp') 是否真支持 webp
      • 支持则优先 webp(同等质量体积比 jpeg 小 25-35%)
      • 否则 jpeg;透明通道(png)保留为 png
    3. 原图压缩:
      • 最大边 ≤ 1600px(超出等比缩放)
      • 质量档位动态调整:原图 ≤ 1MB 用 0.85,1-3MB 用 0.75,> 3MB 用 0.65
      • 输出为 Blob(canvas.toBlob),避免 base64 膨胀
    4. 缩略图生成:
      • 固定最大边 200px,质量 0.6,jpeg 格式
      • 输出为 base64(≤ 10KB),表格直接内联显示
    5. 生成图片 ID:img_${Date.now()}_${Math.random().toString(36).slice(2,8)}
    6. 组装 StoredImage 对象,localforage.setItem(id, stored) 写入 IndexedDB
    7. 将 ID 追加到记录的 imageIds 数组,记录本身仍存 localStorage
  • 限制规则:
    • 单条记录最多 5 张图片
    • 单张原图 ≤ 5MB(IndexedDB 容量大,放宽限制)
    • 支持格式:jpg / jpeg / png / webp
  • 表格缩略图展示(一次读取,缓存使用):
    • 使用 el-table 自定义列渲染
    • 组件挂载/数据变化时按需 getItem 加载首张,取 thumbnail 字段(base64)直接显示
    • 加载结果缓存在组件内存中(Map<id, thumbnail>),避免重复读 IDB
    • 缩略图尺寸 48×48,object-fit: cover
    • 加载中显示 Loading 图标,无图片显示 Picture 占位图标,加载失败显示 PictureFilled
  • 预览大图(懒加载 + Blob URL):
    • 点击缩略图弹出 el-image-viewer
    • 仅在打开预览时才读取该记录所有图片的 originalBlob
    • 通过 URL.createObjectURL(blob) 生成临时 URL,关闭预览时 revokeObjectURL 释放内存
  • 存储一致性:
    • 上传成功后先写 IndexedDB,再更新记录的 imageIds 并持久化 localStorage
    • 删除记录时:先遍历 imageIds 调用 removeItem 清理 IndexedDB,再删除记录
    • 编辑记录移除某张图片时:同步删除 IndexedDB 对应项
  • JSON 导入/导出与图片联动:
    • JSON 导出:可选择"仅记录(含图片 ID)"或"含图片(原图转 base64 内联)"
    • JSON 导入:若数据含内联 base64,逐张还原为 StoredImage 结构写入 IndexedDB 并替换为新 ID

5.8 图表统计实现(ECharts 柱状图)

  • 组件:StatsChart.vue,使用 vue-echarts 或直接 echarts.init + onMounted
  • 数据来源:复用 FilterBar 的筛选条件,统计 store.computedRecords(注意:图表仅联动年月筛选,不参与状态/水贴/名称筛选,以保证统计完整性)
  • X 轴维度决策:
    const xAxis = computed(() => {
      if (!year.value) return uniqueYears(records)           // 全部:按年
      if (!month.value) return ['1月', '2月', ..., '12月']    // 选年:按月
      return daysOfMonth(year.value, month.value)            // 年+月:按日
    })
  • 数据聚合:按 X 轴维度遍历记录,累计 count(模型数量)与 supplementTotal(补款总价,仅预购)
  • ECharts 配置要点:
    const option = {
      tooltip: { trigger: 'axis' },
      legend: { data: ['模型数量', '补款总价'] },
      xAxis: { type: 'category', data: xAxis.value },
      yAxis: [
        { type: 'value', name: '数量', position: 'left', minInterval: 1 },
        { type: 'value', name: '补款总价(¥)', position: 'right' }
      ],
      series: [
        { name: '模型数量', type: 'bar', data: countSeries, itemStyle: { color: '#409EFF' } },
        { name: '补款总价', type: 'bar', yAxisIndex: 1, data: supplementSeries, itemStyle: { color: '#E6A23C' } }
      ]
    }
  • 响应式更新:
    • 监听筛选条件与记录数据变化,watch 后调用 chart.setOption
    • 组件卸载时 chart.dispose() 释放实例
    • 监听窗口 resize 事件,调用 chart.resize() 适配宽度

5.9 主题切换实现(明亮 / 暗黑)

  • 组件:ThemeToggle.vue,使用 VueUse 的 useDark + useToggle
  • 原理:Element Plus 2.x 内置暗黑模式,通过在 <html> 标签添加 class="dark" 激活,样式由 element-plus/theme-chalk/dark/css-vars.css 提供
  • 核心代码:
    import { useDark, useToggle } from '@vueuse/core'
    import 'element-plus/theme-chalk/dark/css-vars.css'
    
    const isDark = useDark({
      selector: 'html',
      attribute: 'class',
      valueDark: 'dark',
      valueLight: '',
      storageKey: 'modeling-statistics:theme'
    })
    const toggleDark = useToggle(isDark)
  • 默认主题:
    • 首次访问读取系统偏好 prefers-color-scheme: dark(useDark 默认行为)
    • 用户切换后持久化到 localStorage(key: modeling-statistics:theme)
  • 按钮交互:
    • 明亮模式显示月亮图标 Moon,暗黑模式显示太阳图标 Sunny(Element Plus Icons)
    • 点击切换,无过渡跳变(html class 切换,CSS 变量自动过渡)
  • ECharts 主题适配:
    • 监听 isDark 变化,动态更新 ECharts 配色:
      • 明亮:背景透明,文字 #333,柱状图系列色 #409EFF / #E6A23C
      • 暗黑:背景透明,文字 #ccc,柱状图系列色 #79BBFF / #F0C78A(提亮)
    • 切换后调用 chart.setOption(newOption) 即时刷新
  • 自定义样式适配:
    • 业务组件样式使用 CSS 变量(如 var(--el-bg-color)var(--el-text-color-primary))
    • 避免硬编码颜色,确保暗黑模式下自动跟随
  • main.ts 引入:
    import 'element-plus/theme-chalk/dark/css-vars.css'

六、UI 布局规划

┌─────────────────────────────────────────────────┐
│  模型购买统计   [导入JSON] [导出▾] [+ 新增] [☾]  │  ← 顶部标题栏(右侧图标为主题切换)
├─────────────────────────────────────────────────┤
│  年份: [全部 ▾] 月份: [全部 ▾] 状态: [全部 ▾] 水贴: [全部 ▾] [搜索模型名称...] │  ← 筛选栏
├─────────────────────────────────────────────────┤
│  总价: ¥XXXX          待支付: ¥XXXX              │  ← 统计卡片
├─────────────────────────────────────────────────┤
│  ┌───────────────────────────────────────────┐  │
│  │  柱状图(联动年月筛选)                      │  │
│  │  ▓▓ ▓▓ ▓▓    数量(左轴) │ 补款总价(右轴)  │  │
│  │  ▓▓ ▓▓ ▓▓ ▓▓ ▓▓                          │  │  ← ECharts 图表
│  └───────────────────────────────────────────┘  │
├─────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────┐ │
│ │ 模型名称 渠道 已支付 支付日期 状态 补款价格 补款日期 水贴 图片 操作│ │
│ │  XXX    淘宝 ¥XX   2026-01-01 预购 ¥XX  2026-02-01 有 [图] 删除│ │
│ │  ...                                          │ │
│ │  合计:       ¥XX              ¥XX                │ │  ← 合计行
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

新增按钮 → 弹出 Dialog 表单(RecordForm.vue)

七、开发任务拆分

# 任务 优先级 依赖
1 初始化 Vite + Vue3 + TS 项目,安装 Element Plus / Pinia -
2 配置 Element Plus 按需/全量引入,Pinia 挂载 1
3 定义类型与 Pinia store(含 localStorage 持久化) 2
4 实现计算工具函数(补款、统计) 3
5 开发 RecordForm 组件(含条件字段、表单校验、图片上传) 3,4
6 开发 RecordTable 组件(含合计行、操作列、缩略图预览) 3,4
7 开发 FilterBar 组件(年月筛选) 3
8 开发 StatSummary 组件(总价/待支付卡片) 4
9 开发 ImportButton 组件(JSON 导入+校验+合并) 3
10 开发 ExportButton 组件(JSON/Excel 导出) 3,4
11 实现 IndexedDB 封装(localforage 配置 + 图片 CRUD) -
12 实现图片处理工具(base64 转换 + Canvas 压缩) -
13 开发 StatsChart 组件(ECharts 柱状图,联动年月) 3,4
14 开发 ThemeToggle 组件(useDark 明暗切换 + ECharts 适配) 2
15 组装 Home.vue 主页面 5,6,7,8,9,10,11,12,13,14
16 联调测试:录入→计算→筛选→统计→刷新持久化→图片增删→图表联动→主题切换 15
17 样式优化与边界处理(空数据、负值提示) 16

八、边界与异常处理

  1. 空数据:首次进入无记录时,表格显示"暂无数据"占位
  2. 补款价格为负:表单中红色提示"全款价格+邮费小于已支付,请检查",允许提交但表格中标记
  3. localStorage 读取失败:try-catch 兜底,初始化为空数组
  4. 日期默认值:提交时若 paymentDate 为空,填充当前本地日期
  5. 删除确认:使用 ElMessageBox.confirm 二次确认
  6. JSON 导入异常:
    • 文件非 JSON / 解析失败 → 提示"文件格式错误"
    • 字段缺失或类型不符 → 跳过该条并记录错误数,导入完成后提示"成功 X 条,失败 Y 条"
    • 覆盖导入模式 → ElMessageBox.confirm 二次确认防止误操作清空数据
    • 大文件导入(>1MB)→ 给出加载提示,避免界面卡死
  7. 数据导出异常:
    • 无记录时禁用导出按钮,提示"暂无可导出的数据"
    • Excel 导出失败(xlsx 库加载异常)→ 自动降级提示用户改用 JSON 导出
    • 浏览器拦截下载 → 提示"请允许浏览器下载弹窗"
  8. 图片上传异常:
    • 单张原图 > 5MB → 拒绝上传并提示"单张图片不能超过 5MB"
    • 超过 5 张 → 禁用继续上传并提示"最多上传 5 张图片"
    • 格式不支持 → 提示"仅支持 jpg/jpeg/png/webp 格式"
    • createImageBitmap 解码失败(文件损坏)→ 提示"图片解码失败,请检查文件"
    • IndexedDB 写入失败(配额超限 QuotaExceededError)→ 提示用户清理无用图片或导出备份后删除部分记录
    • 缩略图加载失败(IndexedDB 读取异常或数据丢失)→ 显示 PictureFilled 错误图标,不影响其他功能
    • Blob URL 未释放:组件卸载时统一 revokeObjectURL 清理已生成 URL,避免内存泄漏
    • 孤儿图片清理:记录删除失败时可能残留图片 ID,提供"清理孤儿图片"维护入口(可选增强)
  9. 图表统计异常:
    • 无记录时图表显示空状态占位("暂无数据")
    • ECharts 实例未及时 dispose → 组件卸载时统一释放,避免内存泄漏
    • 窗口 resize 未触发 chart.resize → 监听 window.resize 并 debounce 处理
    • X 轴数据量大(如选年+月按日,31 天)→ 自动旋转标签 45° 防止重叠
  10. 主题切换异常:
  • 用户禁用 localStorage(隐私模式)→ useDark 自动降级为仅内存状态,刷新后丢失偏好,不影响功能
  • 暗黑模式下自定义硬编码颜色未跟随 → 统一使用 Element Plus CSS 变量,排查并替换硬编码色值
  • ECharts 切换主题时图表未刷新 → watch(isDark) 中调用 setOption 全量更新配色
  • 系统主题变化时未同步(用户已手动切换过)→ 仅在用户未手动设置时跟随系统,手动设置后以用户偏好为准

九、验收标准

  • 可录入预购/全款两种状态记录,字段完整(含购买渠道、水贴记录、模型图片,自定义渠道可输入名称)
  • 预购记录自动计算补款价格,全款记录补款价格显示 "-"
  • 支付日期未填时默认当前本地日期
  • 表格正确展示所有列,合计行显示总价与待支付价格
  • 合计行根据年/月筛选、购买状态筛选、水贴筛选与模型名称搜索动态更新
  • 刷新浏览器数据不丢失(localStorage 持久化)
  • 支持编辑/删除记录
  • 支持 JSON 文件导入,可选追加或覆盖模式,导入数据正确合并并持久化
  • 支持数据导出 JSON 与 Excel 双格式,可导出当前筛选结果或全部记录,Excel 含合计行
  • 支持按模型名称模糊搜索,与年月筛选联动,实时更新表格与统计
  • 支持按购买状态(预购/全款)筛选,快速区分待补款与已结清记录
  • 支持按是否有水贴筛选,快速查找已配备水贴的模型
  • 支持上传模型图片(最多 5 张),智能压缩(原图 Blob + 缩略图 base64 双份)后存入 IndexedDB,表格显示缩略图并可点击预览大图
  • 支持 ECharts 柱状图统计,双系列展示模型数量与补款总价,联动年月筛选,X 轴按年/月/日动态切换
  • 支持明亮/暗黑主题切换,首次跟随系统偏好,手动切换后持久化;ECharts 配色与业务样式同步适配
  • 纯本地运行,无需后端服务

十、后续可扩展方向(非本次范围)

  • 图表扩展:饼图(渠道占比)、折线图(价格趋势)、堆叠柱状图(各状态分布)
  • 自定义主题色(除明暗外,提供多套配色方案)
  • 数据云同步(WebDAV / IndexedDB 导出导入自动化)