feat(@vben/web-antd): add finance cost management pages
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import type { FinanceCostFilterState } from '../../types';
|
||||
|
||||
/**
|
||||
* 文件职责:成本管理页面纯函数与格式化工具。
|
||||
*/
|
||||
import type {
|
||||
FinanceCostCategoryCode,
|
||||
FinanceCostEntryCategoryDto,
|
||||
SaveFinanceCostCategoryPayload,
|
||||
} from '#/api/finance/cost';
|
||||
|
||||
/** 解析为有限数字。 */
|
||||
export function toFiniteNumber(value: unknown, fallback = 0) {
|
||||
const normalized = Number(value);
|
||||
return Number.isFinite(normalized) ? normalized : fallback;
|
||||
}
|
||||
|
||||
/** 金额保留两位小数。 */
|
||||
export function roundAmount(value: unknown) {
|
||||
const normalized = toFiniteNumber(value, 0);
|
||||
return Math.round(normalized * 100) / 100;
|
||||
}
|
||||
|
||||
/** 货币格式化。 */
|
||||
export function formatCurrency(value: unknown) {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(roundAmount(value));
|
||||
}
|
||||
|
||||
/** 百分比格式化。 */
|
||||
export function formatPercent(value: unknown) {
|
||||
return `${roundAmount(value).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
/** 获取当前月份(yyyy-MM)。 */
|
||||
export function getCurrentMonthString() {
|
||||
const now = new Date();
|
||||
const month = `${now.getMonth() + 1}`.padStart(2, '0');
|
||||
return `${now.getFullYear()}-${month}`;
|
||||
}
|
||||
|
||||
/** 月份字符串转标题文本。 */
|
||||
export function formatMonthTitle(month: string) {
|
||||
const normalized = month.trim();
|
||||
const [year, monthValue] = normalized.split('-');
|
||||
if (!year || !monthValue) {
|
||||
return normalized || '--';
|
||||
}
|
||||
return `${year}年${Number(monthValue)}月`;
|
||||
}
|
||||
|
||||
/** 月份位移。 */
|
||||
export function shiftMonth(month: string, offset: number) {
|
||||
const parsed = parseMonth(month);
|
||||
if (!parsed) {
|
||||
return getCurrentMonthString();
|
||||
}
|
||||
parsed.setMonth(parsed.getMonth() + offset);
|
||||
return formatMonth(parsed);
|
||||
}
|
||||
|
||||
/** 构建查询作用域参数。 */
|
||||
export function buildScopeQueryPayload(filters: FinanceCostFilterState) {
|
||||
return {
|
||||
dimension: filters.dimension,
|
||||
month: filters.month || undefined,
|
||||
storeId:
|
||||
filters.dimension === 'store' ? filters.storeId || undefined : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 将 API 分类数据映射为页面视图模型。 */
|
||||
export function mapCategoriesToViewModel(
|
||||
categories: FinanceCostEntryCategoryDto[],
|
||||
expandedMap: Map<FinanceCostCategoryCode, boolean>,
|
||||
) {
|
||||
return (categories ?? []).map((item) => ({
|
||||
...item,
|
||||
items: [...(item.items ?? [])],
|
||||
expanded: expandedMap.get(item.category) ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 计算分类总金额。 */
|
||||
export function sumCategoryItems(
|
||||
category: Pick<FinanceCostEntryCategoryDto, 'category' | 'items'>,
|
||||
) {
|
||||
let total = 0;
|
||||
for (const item of category.items ?? []) {
|
||||
if (category.category === 'labor') {
|
||||
const quantity = toFiniteNumber(item.quantity, 0);
|
||||
const unitPrice = toFiniteNumber(item.unitPrice, 0);
|
||||
total += roundAmount(quantity * unitPrice);
|
||||
continue;
|
||||
}
|
||||
total += roundAmount(item.amount);
|
||||
}
|
||||
return roundAmount(total);
|
||||
}
|
||||
|
||||
/** 计算全部分类总成本。 */
|
||||
export function sumAllCategoryTotal(categories: FinanceCostEntryCategoryDto[]) {
|
||||
let total = 0;
|
||||
for (const category of categories ?? []) {
|
||||
total += roundAmount(category.totalAmount);
|
||||
}
|
||||
return roundAmount(total);
|
||||
}
|
||||
|
||||
/** 构建保存请求分类数组。 */
|
||||
export function buildSaveCategoryPayload(
|
||||
categories: FinanceCostEntryCategoryDto[],
|
||||
): SaveFinanceCostCategoryPayload[] {
|
||||
return (categories ?? []).map((category) => ({
|
||||
category: category.category,
|
||||
totalAmount: roundAmount(category.totalAmount),
|
||||
items: (category.items ?? []).map((item, index) => ({
|
||||
itemId: item.itemId,
|
||||
itemName: item.itemName.trim(),
|
||||
amount: roundAmount(item.amount),
|
||||
quantity:
|
||||
item.quantity === undefined ? undefined : roundAmount(item.quantity),
|
||||
unitPrice:
|
||||
item.unitPrice === undefined ? undefined : roundAmount(item.unitPrice),
|
||||
sortOrder: item.sortOrder > 0 ? item.sortOrder : index + 1,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
function parseMonth(value: string) {
|
||||
const normalized = value.trim();
|
||||
const [year, month] = normalized.split('-');
|
||||
const yearValue = Number(year);
|
||||
const monthValue = Number(month);
|
||||
if (
|
||||
!Number.isInteger(yearValue) ||
|
||||
!Number.isInteger(monthValue) ||
|
||||
monthValue < 1 ||
|
||||
monthValue > 12
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return new Date(yearValue, monthValue - 1, 1);
|
||||
}
|
||||
|
||||
function formatMonth(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
Reference in New Issue
Block a user