155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
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}`;
|
||
}
|