资产管理模块
🏦 概述
资产管理模块是 Penny Lens 应用的核心功能模块,负责用户各类资产账户的管理、余额跟踪、资产变化记录等功能。支持现金、银行卡、信用卡、投资账户等多种资产类型。
🏗️ 模块架构
核心组件
src/├── pages/assets/ # 资产管理页面│ ├── index.vue # 资产列表主页│ ├── addAssetForm.vue # 添加资产表单│ ├── addAssetType.vue # 添加资产类型│ └── assetDetail.vue # 资产详情页面├── components/AssetSelector/ # 资产选择器组件├── store/asset.ts # 资产状态管理├── services/api/asset.ts # 资产API服务├── services/asset.ts # 资产服务├── utils/assetUtils.ts # 资产工具函数├── types/asset.ts # 资产类型定义├── types/assetTypeModel.ts # 资产类型模型└── types/assetChange.ts # 资产变化类型💰 资产类型
支持的资产类型
// 资产类型枚举
const AssetType = {
CASH: 'cash', // 现金
BANK: 'bank', // 银行卡
CREDIT: 'credit', // 信用卡
INVESTMENT: 'investment', // 投资账户
OTHER: 'other' // 其他
} as const
type AssetType = typeof AssetType[keyof typeof AssetType]资产数据结构
// 资产接口定义
interface Asset {
id: string; // 资产ID
name: string; // 资产名称
type: AssetType; // 资产类型
balanceCents: number; // 余额(分)
currency: string; // 货币类型
description?: string; // 描述
isActive: boolean; // 是否激活
createdAt: number; // 创建时间
updatedAt: number; // 更新时间
}
// 资产类型配置
interface AssetTypeConfig {
type: AssetType;
name: string;
icon: string;
color: string;
description: string;
defaultCurrency: string;
}🔧 核心功能
1. 资产列表管理
功能描述: 显示和管理用户的所有资产账户
实现流程:
- 从后端获取用户资产列表
- 按类型分组显示
- 计算总余额
- 提供添加、编辑、删除操作
代码示例:
// 获取资产列表
async function getAssetList(): Promise<Asset[]> {
const response = await assetApi.getAssetList()
return response.data.list
}
// 计算总余额
function calculateTotalBalance(assets: Asset[]): number {
return assets.reduce((total, asset) => {
return total + (asset.type === AssetType.CREDIT ? -asset.balanceCents : asset.balanceCents)
}, 0)
}
// 按类型分组
function groupAssetsByType(assets: Asset[]): Record<AssetType, Asset[]> {
return assets.reduce((groups, asset) => {
if (!groups[asset.type]) {
groups[asset.type] = []
}
groups[asset.type].push(asset)
return groups
}, {} as Record<AssetType, Asset[]>)
}2. 添加资产
功能描述: 创建新的资产账户
实现流程:
- 用户选择资产类型
- 填写资产信息
- 设置初始余额
- 保存到后端
代码示例:
// 添加资产请求
interface CreateAssetRequest {
name: string;
type: AssetType;
balanceCents: number;
currency: string;
description?: string;
}
// 添加资产实现
async function createAsset(assetData: CreateAssetRequest): Promise<Asset> {
const response = await assetApi.createAsset(assetData)
if (response.code === 200) {
// 更新本地状态
await refreshAssetList()
return response.data
}
throw new Error(response.message)
}
// 表单验证
function validateAssetForm(data: CreateAssetRequest): ValidationResult {
const errors: string[] = []
if (!data.name.trim()) {
errors.push('资产名称不能为空')
}
if (data.name.length > 20) {
errors.push('资产名称不能超过20个字符')
}
if (data.balanceCents < 0) {
errors.push('初始余额不能为负数')
}
return {
isValid: errors.length === 0,
errors
}
}3. 资产详情
功能描述: 查看单个资产的详细信息和交易记录
实现流程:
- 获取资产基本信息
- 获取资产交易记录
- 计算资产变化趋势
- 显示余额历史
代码示例:
// 资产详情请求
interface AssetDetailRequest {
id: string;
startDate?: number;
endDate?: number;
}
// 资产详情响应
interface AssetDetailResponse {
asset: Asset;
transactions: AccountingRecord[];
balanceHistory: BalanceHistory[];
totalIncome: number;
totalExpense: number;
netChange: number;
}
// 获取资产详情
async function getAssetDetail(params: AssetDetailRequest): Promise<AssetDetailResponse> {
const response = await assetApi.getAssetDetail(params)
return response.data
}
// 计算余额历史
function calculateBalanceHistory(
transactions: AccountingRecord[],
initialBalance: number
): BalanceHistory[] {
let currentBalance = initialBalance
const history: BalanceHistory[] = []
// 按时间排序
const sortedTransactions = transactions.sort((a, b) => a.date - b.date)
sortedTransactions.forEach(transaction => {
if (transaction.type === 'income') {
currentBalance += transaction.amountCents
} else if (transaction.type === 'expense') {
currentBalance -= transaction.amountCents
}
history.push({
date: transaction.date,
balance: currentBalance,
change: transaction.amountCents,
type: transaction.type
})
})
return history
}4. 资产转账
功能描述: 在不同资产账户之间进行转账
实现流程:
- 选择转出和转入资产
- 输入转账金额
- 添加转账备注
- 创建转账记录
- 更新资产余额
代码示例:
// 转账请求
interface TransferRequest {
fromAssetId: string;
toAssetId: string;
amountCents: number;
remark?: string;
date: number;
}
// 转账实现
async function transferAssets(transferData: TransferRequest): Promise<void> {
// 验证转账条件
const validation = validateTransfer(transferData)
if (!validation.isValid) {
throw new Error(validation.errors.join(', '))
}
// 创建转账记录
const response = await accountingApi.createTransfer(transferData)
if (response.code === 200) {
// 更新资产余额
await refreshAssetList()
return
}
throw new Error(response.message)
}
// 转账验证
function validateTransfer(data: TransferRequest): ValidationResult {
const errors: string[] = []
if (data.fromAssetId === data.toAssetId) {
errors.push('转出和转入资产不能相同')
}
if (data.amountCents <= 0) {
errors.push('转账金额必须大于0')
}
// 检查转出资产余额
const fromAsset = getAssetById(data.fromAssetId)
if (fromAsset && fromAsset.balanceCents < data.amountCents) {
errors.push('转出资产余额不足')
}
return {
isValid: errors.length === 0,
errors
}
}🎯 资产选择器组件
功能描述: 提供统一的资产选择界面
代码示例:
<template>
<view class="asset-selector">
<view class="selector-header">
<text class="title">{{ title }}</text>
<text class="subtitle">{{ subtitle }}</text>
</view>
<view class="asset-list">
<view
v-for="asset in filteredAssets"
:key="asset.id"
class="asset-item"
:class="{ active: selectedAssetId === asset.id }"
@click="selectAsset(asset)"
>
<view class="asset-icon">
<Iconify :icon="getAssetIcon(asset.type)" size="24" />
</view>
<view class="asset-info">
<text class="asset-name">{{ asset.name }}</text>
<text class="asset-balance">{{ formatBalance(asset.balanceCents) }}</text>
</view>
<view class="asset-type">
<text class="type-text">{{ getAssetTypeName(asset.type) }}</text>
</view>
</view>
</view>
<view class="selector-actions">
<button class="btn-secondary" @click="handleCancel">取消</button>
<button class="btn-primary" @click="handleConfirm">确定</button>
</view>
</view>
</template>
<script setup lang="ts">
interface Props {
title: string;
subtitle?: string;
selectedAssetId?: string;
filterType?: AssetType[];
showBalance?: boolean;
}
interface Emits {
select: [asset: Asset];
cancel: [];
}
const props = withDefaults(defineProps<Props>(), {
showBalance: true
})
const emit = defineEmits<Emits>()
const assetStore = useAssetStore()
const { assets } = storeToRefs(assetStore)
// 过滤资产
const filteredAssets = computed(() => {
let filtered = assets.value
if (props.filterType && props.filterType.length > 0) {
filtered = filtered.filter(asset => props.filterType!.includes(asset.type))
}
return filtered
})
// 选择资产
function selectAsset(asset: Asset) {
emit('select', asset)
}
// 确认选择
function handleConfirm() {
if (props.selectedAssetId) {
const selectedAsset = assets.value.find(asset => asset.id === props.selectedAssetId)
if (selectedAsset) {
emit('select', selectedAsset)
}
}
}
// 取消选择
function handleCancel() {
emit('cancel')
}
</script>📊 资产统计
功能描述: 提供资产相关的统计分析
代码示例:
// 资产统计接口
interface AssetStatistics {
totalBalance: number; // 总余额
totalAssets: number; // 资产总数
assetTypeDistribution: { // 资产类型分布
type: AssetType;
count: number;
totalBalance: number;
percentage: number;
}[];
balanceTrend: { // 余额趋势
date: number;
balance: number;
}[];
topAssets: Asset[]; // 余额最高的资产
}
// 计算资产统计
function calculateAssetStatistics(assets: Asset[]): AssetStatistics {
const totalBalance = calculateTotalBalance(assets)
const totalAssets = assets.length
// 按类型分组统计
const typeGroups = groupAssetsByType(assets)
const assetTypeDistribution = Object.entries(typeGroups).map(([type, typeAssets]) => {
const typeBalance = typeAssets.reduce((sum, asset) => sum + asset.balanceCents, 0)
return {
type: type as AssetType,
count: typeAssets.length,
totalBalance: typeBalance,
percentage: totalBalance > 0 ? (typeBalance / totalBalance) * 100 : 0
}
})
// 余额最高的资产
const topAssets = [...assets]
.sort((a, b) => b.balanceCents - a.balanceCents)
.slice(0, 5)
return {
totalBalance,
totalAssets,
assetTypeDistribution,
balanceTrend: [], // 需要从历史数据计算
topAssets
}
}🔄 状态管理
Pinia Store
功能描述: 使用Pinia管理资产状态
代码示例:
// 资产状态管理
export const useAssetStore = defineStore('asset', () => {
// 状态
const assets = ref<Asset[]>([])
const isLoading = ref(false)
const totalBalance = computed(() => calculateTotalBalance(assets.value))
// 操作
const fetchAssets = async (): Promise<void> => {
isLoading.value = true
try {
const assetList = await assetService.getAssetList()
assets.value = assetList
} catch (error) {
console.error('获取资产列表失败:', error)
throw error
} finally {
isLoading.value = false
}
}
const addAsset = async (assetData: CreateAssetRequest): Promise<Asset> => {
try {
const newAsset = await assetService.createAsset(assetData)
assets.value.push(newAsset)
return newAsset
} catch (error) {
console.error('添加资产失败:', error)
throw error
}
}
const updateAsset = async (id: string, data: Partial<Asset>): Promise<Asset> => {
try {
const updatedAsset = await assetService.updateAsset(id, data)
const index = assets.value.findIndex(asset => asset.id === id)
if (index !== -1) {
assets.value[index] = updatedAsset
}
return updatedAsset
} catch (error) {
console.error('更新资产失败:', error)
throw error
}
}
const deleteAsset = async (id: string): Promise<void> => {
try {
await assetService.deleteAsset(id)
assets.value = assets.value.filter(asset => asset.id !== id)
} catch (error) {
console.error('删除资产失败:', error)
throw error
}
}
const getAssetById = (id: string): Asset | undefined => {
return assets.value.find(asset => asset.id === id)
}
const getAssetsByType = (type: AssetType): Asset[] => {
return assets.value.filter(asset => asset.type === type)
}
return {
assets,
isLoading,
totalBalance,
fetchAssets,
addAsset,
updateAsset,
deleteAsset,
getAssetById,
getAssetsByType
}
})🛠️ 工具函数
资产工具
功能描述: 提供资产相关的工具函数
代码示例:
// 资产工具函数
export const assetUtils = {
// 格式化余额显示
formatBalance(cents: number, currency: string = 'CNY'): string {
const amount = cents / 100
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2
}).format(amount)
},
// 获取资产类型配置
getAssetTypeConfig(type: AssetType): AssetTypeConfig {
const configs: Record<AssetType, AssetTypeConfig> = {
[AssetType.CASH]: {
type: AssetType.CASH,
name: '现金',
icon: 'i-mdi-cash',
color: '#10b981',
description: '现金账户',
defaultCurrency: 'CNY'
},
[AssetType.BANK]: {
type: AssetType.BANK,
name: '银行卡',
icon: 'i-mdi-credit-card',
color: '#3b82f6',
description: '银行储蓄卡',
defaultCurrency: 'CNY'
},
[AssetType.CREDIT]: {
type: AssetType.CREDIT,
name: '信用卡',
icon: 'i-mdi-credit-card-outline',
color: '#ef4444',
description: '信用卡账户',
defaultCurrency: 'CNY'
},
[AssetType.INVESTMENT]: {
type: AssetType.INVESTMENT,
name: '投资账户',
icon: 'i-mdi-trending-up',
color: '#8b5cf6',
description: '投资理财账户',
defaultCurrency: 'CNY'
},
[AssetType.OTHER]: {
type: AssetType.OTHER,
name: '其他',
icon: 'i-mdi-wallet',
color: '#6b7280',
description: '其他类型资产',
defaultCurrency: 'CNY'
}
}
return configs[type]
},
// 获取资产图标
getAssetIcon(type: AssetType): string {
return this.getAssetTypeConfig(type).icon
},
// 获取资产类型名称
getAssetTypeName(type: AssetType): string {
return this.getAssetTypeConfig(type).name
},
// 计算资产净值
calculateNetWorth(assets: Asset[]): number {
return assets.reduce((total, asset) => {
if (asset.type === AssetType.CREDIT) {
return total - asset.balanceCents
}
return total + asset.balanceCents
}, 0)
},
// 验证资产名称
validateAssetName(name: string): ValidationResult {
const errors: string[] = []
if (!name.trim()) {
errors.push('资产名称不能为空')
}
if (name.length > 20) {
errors.push('资产名称不能超过20个字符')
}
if (!/^[\u4e00-\u9fa5a-zA-Z0-9\s]+$/.test(name)) {
errors.push('资产名称只能包含中文、英文、数字和空格')
}
return {
isValid: errors.length === 0,
errors
}
}
}📚 最佳实践
1. 数据一致性
- 实时更新资产余额
- 同步记账记录和资产变化
- 处理并发操作
- 数据验证和错误处理
2. 用户体验
- 快速加载资产列表
- 直观的资产类型图标
- 清晰的余额显示
- 便捷的资产操作
3. 性能优化
- 缓存资产数据
- 懒加载资产详情
- 分页加载交易记录
- 优化图片资源
4. 安全考虑
- 验证用户权限
- 防止数据篡改
- 敏感信息加密
- 操作日志记录
Penny Lens 资产管理模块 - 让资产账户管理更清晰、更高效 🏦
