构建一个纯本地运行的 Web 应用,用于统计和管理模型购买记录。用户可录入每次购买的模型信息、支付金额与状态,系统自动计算补款价格并汇总统计,支持按月/年筛选查看。支持通过 JSON 文件导入历史记录,也可将当前数据导出为 JSON/Excel 文件,形成完整的备份与迁移链路。每条记录可上传模型图片附件(base64 存储),便于直观识别与管理。支持明亮/暗黑主题切换,主题偏好持久化。
技术
版本建议
用途
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 主题切换)
纯本地项目,不依赖任何后端服务器
记录数据存储于浏览器 localStorage(轻量结构化数据)
模型图片存储于浏览器 IndexedDB(大容量二进制数据,避免挤占 localStorage 配额)
通过 npm run dev 启动本地开发服务器,npm run build 生成静态产物
支持离线使用(构建后直接用浏览器打开 index.html 或通过静态服务器访问)
字段
类型
是否必填
说明
模型名称
string
必填
文本输入
购买渠道
enum/string
必填
淘宝 / pdd / jd / 自定义;选"自定义"时需手动输入渠道名称
已支付人民币
number
必填
数值,≥ 0;预购状态下即"定金"
购买状态
enum
必填
预购 / 全款
全款价格
number
预购必填
仅预购状态显示
邮费
number
预购必填
仅预购状态显示,默认 0
支付日期
date
选填
未填写时默认本地当前时间
补款日期
date
预购必填
仅预购状态显示
水贴记录
string
选填
文本输入,记录该模型对应的水贴信息(如水贴名称/品牌/链接等);留空视为无水贴
模型图片
File[]
选填
支持上传多张图片(最多 5 张,单张原图 ≤ 5MB),压缩后存入 IndexedDB;表格中显示首张缩略图
补款价格 (仅预购状态):
补款价格 = 全款价格 + 邮费 - 已支付人民币
若结果 < 0,提示用户检查输入(异常但不阻断)
全款状态:补款价格 = 0,补款日期为空
列名
数据来源
说明
模型名称
输入
文本
购买渠道
输入
标签样式展示;自定义渠道直接显示文本
已支付人民币
输入
货币格式 ¥#.00
支付日期
输入/默认
YYYY-MM-DD
购买状态
输入
标签样式区分(预购=warning,全款=success)
补款价格
自动计算
货币格式;全款显示 "-"
补款日期
输入
全款显示 "-"
水贴
输入
有内容显示"有"标签(success);留空显示"无"标签(info);hover 显示完整水贴记录
模型图片
输入
显示首张缩略图(48×48);无图显示占位图标;点击缩略图弹窗预览全部图片
总价 :所有记录的 已支付人民币 之和 + 所有预购记录的 补款价格 之和
即:总价 = Σ已支付人民币 + Σ补款价格
含义:全部模型购买的总支出(含定金与补款)
待支付价格 :所有预购记录的 补款价格 之和
统计需根据当前筛选条件动态计算
按年筛选 :下拉选择年份(数据中最早年份 ~ 当前年份),默认全部
按月筛选 :下拉选择月份(1-12),默认全部;仅当选择具体年份后月份筛选生效
按购买状态筛选 :下拉选择 全部 / 预购 / 全款,默认全部
用于快速区分待补款记录(预购)与已结清记录(全款)
按是否有水贴筛选 :下拉选择 全部 / 有水贴 / 无水贴,默认全部
按模型名称搜索 :文本输入框,支持模糊匹配(不区分大小写)
输入内容实时过滤,无需点击搜索按钮
匹配规则:模型名称包含输入字符串即命中(使用 includes + toLowerCase)
支持中英文混合搜索
筛选/搜索依据:年月筛选基于支付日期 ,状态筛选基于购买状态 ,水贴筛选基于水贴记录是否为空 ,名称搜索基于模型名称
筛选条件为"与"关系:年月 + 状态 + 水贴 + 名称同时满足才显示
筛选结果实时更新表格与统计
图表类型 :柱状图(ECharts)
统计维度 :模型数量 + 补款总价(双系列分组柱状图)
联动筛选 :与 FilterBar 的年/月筛选条件联动,筛选变化时图表实时更新
X 轴动态规则 :
未选年份(全部):X 轴为各年份 (如 2024 / 2025 / 2026),统计每年的数量与补款总价
选了具体年份、未选月份:X 轴为 1-12 月 ,统计该年每月数据
选了具体年份 + 月份:X 轴为该月的各日期 (1-31),统计每日数据
Y 轴 :
左 Y 轴:模型数量(整数,柱状图系列 1)
右 Y 轴:补款总价(人民币,柱状图系列 2)
交互 :hover 显示 tooltip(年份/月份 + 数量 + 总价);图例可切换显示/隐藏某一系列
// 购买状态枚举
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 可存上万条,无压力
使用 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) 清理
导入文件为标准 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 # 主页面(组合上述组件)
// 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 }
} )
监听 status 字段变化
当 status === 'preorder' 时,动态显示 全款价格、邮费、补款日期 字段并设为必填
当切换为 fullpaid 时,清空上述字段
监听 channel 字段变化
当 channel === 'custom' 时,动态显示 自定义渠道名称 文本输入框并设为必填
切换为其他预置渠道时,清空 customChannel
补款价格在表单中实时预览 (只读展示),提交时由后端计算存储
使用 Element Plus Table 的 summary-method 自定义合计行
合计行显示在表格最下方,包含:总价、待支付价格
合计行根据当前筛选后的数据计算
FilterBar 组件维护 year、month、status、decal、keyword 五个响应式变量
通过 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,保证数据一致
使用 <input type="file" accept=".json"> 触发文件选择
通过 FileReader.readAsText 读取文件内容
解析与校验流程:
JSON.parse 解析,失败则提示"文件格式错误,请选择有效的 JSON 文件"
校验为数组,且每项包含 modelName / channel / paidAmount / status 必填字段
校验 channel 为合法枚举值,status 为 preorder / fullpaid
预购记录需有 fullPrice / shippingFee / supplementDate
缺失 id / createdAt 时自动补全;paymentDate 为空时填当前日期
导入策略:提供两种模式供用户选择
追加导入 (默认):新数据追加到现有记录末尾
覆盖导入 :清空现有数据后替换为导入内容(需二次确认)
导入成功后通过 ElMessage 提示导入条数,并自动持久化
顶部"导出"按钮提供下拉菜单,可选 导出 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):
选择图片后通过 createImageBitmap(file) 解码(性能优于 Image 对象)
格式智能选择 :
检测浏览器 canvas.toDataURL('image/webp') 是否真支持 webp
支持则优先 webp(同等质量体积比 jpeg 小 25-35%)
否则 jpeg;透明通道(png)保留为 png
原图压缩 :
最大边 ≤ 1600px(超出等比缩放)
质量档位动态调整:原图 ≤ 1MB 用 0.85,1-3MB 用 0.75,> 3MB 用 0.65
输出为 Blob(canvas.toBlob),避免 base64 膨胀
缩略图生成 :
固定最大边 200px,质量 0.6,jpeg 格式
输出为 base64(≤ 10KB),表格直接内联显示
生成图片 ID:img_${Date.now()}_${Math.random().toString(36).slice(2,8)}
组装 StoredImage 对象,localforage.setItem(id, stored) 写入 IndexedDB
将 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
组件 :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() 适配宽度
组件 :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'
┌─────────────────────────────────────────────────┐
│ 模型购买统计 [导入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
空数据 :首次进入无记录时,表格显示"暂无数据"占位
补款价格为负 :表单中红色提示"全款价格+邮费小于已支付,请检查",允许提交但表格中标记
localStorage 读取失败 :try-catch 兜底,初始化为空数组
日期默认值 :提交时若 paymentDate 为空,填充当前本地日期
删除确认 :使用 ElMessageBox.confirm 二次确认
JSON 导入异常 :
文件非 JSON / 解析失败 → 提示"文件格式错误"
字段缺失或类型不符 → 跳过该条并记录错误数,导入完成后提示"成功 X 条,失败 Y 条"
覆盖导入模式 → ElMessageBox.confirm 二次确认防止误操作清空数据
大文件导入(>1MB)→ 给出加载提示,避免界面卡死
数据导出异常 :
无记录时禁用导出按钮,提示"暂无可导出的数据"
Excel 导出失败(xlsx 库加载异常)→ 自动降级提示用户改用 JSON 导出
浏览器拦截下载 → 提示"请允许浏览器下载弹窗"
图片上传异常 :
单张原图 > 5MB → 拒绝上传并提示"单张图片不能超过 5MB"
超过 5 张 → 禁用继续上传并提示"最多上传 5 张图片"
格式不支持 → 提示"仅支持 jpg/jpeg/png/webp 格式"
createImageBitmap 解码失败(文件损坏)→ 提示"图片解码失败,请检查文件"
IndexedDB 写入失败(配额超限 QuotaExceededError)→ 提示用户清理无用图片或导出备份后删除部分记录
缩略图加载失败(IndexedDB 读取异常或数据丢失)→ 显示 PictureFilled 错误图标,不影响其他功能
Blob URL 未释放:组件卸载时统一 revokeObjectURL 清理已生成 URL,避免内存泄漏
孤儿图片清理:记录删除失败时可能残留图片 ID,提供"清理孤儿图片"维护入口(可选增强)
图表统计异常 :
无记录时图表显示空状态占位("暂无数据")
ECharts 实例未及时 dispose → 组件卸载时统一释放,避免内存泄漏
窗口 resize 未触发 chart.resize → 监听 window.resize 并 debounce 处理
X 轴数据量大(如选年+月按日,31 天)→ 自动旋转标签 45° 防止重叠
主题切换异常 :
用户禁用 localStorage(隐私模式)→ useDark 自动降级为仅内存状态,刷新后丢失偏好,不影响功能
暗黑模式下自定义硬编码颜色未跟随 → 统一使用 Element Plus CSS 变量,排查并替换硬编码色值
ECharts 切换主题时图表未刷新 → watch(isDark) 中调用 setOption 全量更新配色
系统主题变化时未同步(用户已手动切换过)→ 仅在用户未手动设置时跟随系统,手动设置后以用户偏好为准
可录入预购/全款两种状态记录,字段完整(含购买渠道、水贴记录、模型图片,自定义渠道可输入名称)
预购记录自动计算补款价格,全款记录补款价格显示 "-"
支付日期未填时默认当前本地日期
表格正确展示所有列,合计行显示总价与待支付价格
合计行根据年/月筛选、购买状态筛选、水贴筛选与模型名称搜索动态更新
刷新浏览器数据不丢失(localStorage 持久化)
支持编辑/删除记录
支持 JSON 文件导入,可选追加或覆盖模式,导入数据正确合并并持久化
支持数据导出 JSON 与 Excel 双格式,可导出当前筛选结果或全部记录,Excel 含合计行
支持按模型名称模糊搜索,与年月筛选联动,实时更新表格与统计
支持按购买状态(预购/全款)筛选,快速区分待补款与已结清记录
支持按是否有水贴筛选,快速查找已配备水贴的模型
支持上传模型图片(最多 5 张),智能压缩(原图 Blob + 缩略图 base64 双份)后存入 IndexedDB,表格显示缩略图并可点击预览大图
支持 ECharts 柱状图统计,双系列展示模型数量与补款总价,联动年月筛选,X 轴按年/月/日动态切换
支持明亮/暗黑主题切换,首次跟随系统偏好,手动切换后持久化;ECharts 配色与业务样式同步适配
纯本地运行,无需后端服务
图表扩展:饼图(渠道占比)、折线图(价格趋势)、堆叠柱状图(各状态分布)
自定义主题色(除明暗外,提供多套配色方案)
数据云同步(WebDAV / IndexedDB 导出导入自动化)