2026-08-11 00:49:41 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 功能:统一资源列表与详情页面的字段名称、关系、金额、状态和时间展示。
|
|
|
|
|
|
* 版本:v1.1.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2026-08-11 19:15:53 +08:00
|
|
|
|
import { staffRoleLabel } from './resource-staff-relation';
|
2026-08-11 00:49:41 +08:00
|
|
|
|
import type { ResourceField, ResourceUiDefinition } from './resources';
|
|
|
|
|
|
import type { RecordPageMode, ResourceRow } from './resource-page-rules';
|
|
|
|
|
|
|
|
|
|
|
|
const aliases: Record<string, string> = {
|
|
|
|
|
|
identity: '唯一标识',
|
|
|
|
|
|
id: 'ID',
|
|
|
|
|
|
created_at: '创建时间',
|
|
|
|
|
|
updated_at: '更新时间',
|
|
|
|
|
|
deleted_at: '删除时间',
|
|
|
|
|
|
DeletedAt: '删除时间',
|
|
|
|
|
|
status: '状态',
|
|
|
|
|
|
version: '版本',
|
|
|
|
|
|
items: '订单明细',
|
|
|
|
|
|
assignments: '分配记录',
|
|
|
|
|
|
statuses: '状态记录',
|
|
|
|
|
|
tracks: '运行轨迹',
|
|
|
|
|
|
confirmations: '确认记录',
|
|
|
|
|
|
payments: '支付记录',
|
|
|
|
|
|
revisions: '修订记录',
|
|
|
|
|
|
products: '合同气瓶',
|
|
|
|
|
|
order: '订单',
|
|
|
|
|
|
contract: '合同',
|
|
|
|
|
|
wallet: '钱包',
|
|
|
|
|
|
is_system: '系统内置',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const amountKeys = new Set([
|
|
|
|
|
|
'amount',
|
|
|
|
|
|
'unit_price',
|
|
|
|
|
|
'balance',
|
|
|
|
|
|
'withdrawal_balance',
|
|
|
|
|
|
'default_delivery_fee',
|
|
|
|
|
|
'discount_amount',
|
|
|
|
|
|
'total_amount',
|
|
|
|
|
|
'delivery_fee',
|
|
|
|
|
|
'price_amount',
|
|
|
|
|
|
'sale_amount',
|
|
|
|
|
|
'difference_amount',
|
|
|
|
|
|
'fee',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
/** 返回资源独立页面的模式名称。 */
|
|
|
|
|
|
export function recordPageModeLabel(mode: RecordPageMode) {
|
|
|
|
|
|
return { create: '新建', detail: '详情', edit: '编辑' }[mode];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 返回资源独立页面的错误标题。 */
|
|
|
|
|
|
export function recordPageErrorTitle(status: '403' | '404' | 'error') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'403': '无法执行此操作',
|
|
|
|
|
|
'404': '记录不存在',
|
|
|
|
|
|
error: '页面加载失败',
|
|
|
|
|
|
}[status];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 返回聚合详情中的主记录。 */
|
|
|
|
|
|
export function primaryRecord(detail: ResourceRow): ResourceRow {
|
|
|
|
|
|
if (detail.order && typeof detail.order === 'object')
|
|
|
|
|
|
return detail.order as ResourceRow;
|
|
|
|
|
|
if (detail.contract && typeof detail.contract === 'object')
|
|
|
|
|
|
return detail.contract as ResourceRow;
|
|
|
|
|
|
return detail;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 返回字段中文名称,脱敏字段沿用原字段名称。 */
|
|
|
|
|
|
export function resourceFieldLabel(
|
|
|
|
|
|
definition: ResourceUiDefinition,
|
|
|
|
|
|
key: string,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const normalized = key.endsWith('_masked') ? key.slice(0, -7) : key;
|
|
|
|
|
|
return (
|
|
|
|
|
|
definition.fields.find((field) => field.key === normalized)?.label ??
|
|
|
|
|
|
aliases[key] ??
|
|
|
|
|
|
key
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 将标准实体状态转换为稳定的中文展示。 */
|
|
|
|
|
|
export function recordStatusLabel(status: number) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
{ 0: '待审核', 1: '启用', 2: '停用', 3: '已归档', 4: '已冻结' }[status] ??
|
|
|
|
|
|
`未知(${status})`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function recordStatusColor(status: number) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
{ 0: 'orange', 1: 'green', 2: 'red', 3: 'gray', 4: 'purple' }[status] ??
|
|
|
|
|
|
'gray'
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 选择关系记录的首选可读名称。 */
|
|
|
|
|
|
export function optionLabel(option: ResourceRow) {
|
|
|
|
|
|
return String(
|
|
|
|
|
|
option.name ??
|
|
|
|
|
|
option.title ??
|
|
|
|
|
|
option.display_name ??
|
|
|
|
|
|
option.code ??
|
|
|
|
|
|
option.username ??
|
|
|
|
|
|
option.contract_no ??
|
|
|
|
|
|
option.order_no ??
|
|
|
|
|
|
option.identity ??
|
|
|
|
|
|
'-',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 19:15:53 +08:00
|
|
|
|
/** 工作人员关系额外展示角色,其他关系保持原有可读名称。 */
|
|
|
|
|
|
export function relationOptionLabel(field: ResourceField, option: ResourceRow) {
|
|
|
|
|
|
const label = optionLabel(option);
|
|
|
|
|
|
return field.relation === '/staff_account'
|
|
|
|
|
|
? `${label}(${staffRoleLabel(option.role_code)})`
|
|
|
|
|
|
: label;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 00:49:41 +08:00
|
|
|
|
function relationLabel(
|
|
|
|
|
|
field: ResourceField,
|
|
|
|
|
|
identity: string,
|
|
|
|
|
|
relationOptions: Record<string, ResourceRow[]>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
|
|
|
|
|
(option) => String(option.identity) === identity,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!match) return identity;
|
2026-08-11 19:15:53 +08:00
|
|
|
|
if (field.relation === '/staff_account') {
|
|
|
|
|
|
return relationOptionLabel(field, match);
|
|
|
|
|
|
}
|
2026-08-11 00:49:41 +08:00
|
|
|
|
return field.displayRelationLabel
|
|
|
|
|
|
? optionLabel(match)
|
|
|
|
|
|
: `${optionLabel(match)} · ${identity}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 格式化不依赖字段定义的通用值。 */
|
|
|
|
|
|
export function displayRawValue(key: string, value: unknown) {
|
|
|
|
|
|
if (value == null || value === '') return '-';
|
|
|
|
|
|
if (typeof value === 'boolean') return value ? '是' : '否';
|
|
|
|
|
|
if (key === 'status') return recordStatusLabel(Number(value));
|
|
|
|
|
|
if (
|
|
|
|
|
|
amountKeys.has(key) ||
|
|
|
|
|
|
key.endsWith('_amount') ||
|
|
|
|
|
|
key.endsWith('_balance_after')
|
|
|
|
|
|
) {
|
|
|
|
|
|
const amount = Number(value);
|
|
|
|
|
|
return Number.isFinite(amount)
|
|
|
|
|
|
? `¥${(amount / 100).toFixed(2)}`
|
|
|
|
|
|
: String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (key.endsWith('_at') || ['created_at', 'updated_at'].includes(key)) {
|
|
|
|
|
|
const date = dayjs(String(value));
|
|
|
|
|
|
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
['deleted_at', 'DeletedAt'].includes(key) &&
|
|
|
|
|
|
typeof value === 'object' &&
|
|
|
|
|
|
value &&
|
|
|
|
|
|
'Time' in value
|
|
|
|
|
|
) {
|
|
|
|
|
|
const date = dayjs(String((value as { Time?: unknown }).Time ?? ''));
|
|
|
|
|
|
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
|
|
|
|
|
return String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 按字段选项和关系配置格式化值。 */
|
|
|
|
|
|
export function displayResourceField(
|
|
|
|
|
|
field: ResourceField,
|
|
|
|
|
|
row: ResourceRow,
|
|
|
|
|
|
relationOptions: Record<string, ResourceRow[]>,
|
2026-08-11 19:15:53 +08:00
|
|
|
|
fieldOptions: Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
Array<{ label: string; value: string | number }>
|
|
|
|
|
|
> = {},
|
2026-08-11 00:49:41 +08:00
|
|
|
|
) {
|
|
|
|
|
|
const value = row[field.key] ?? row[`${field.key}_masked`];
|
|
|
|
|
|
if (value == null || value === '') return field.emptyText ?? '-';
|
2026-08-11 02:01:00 +08:00
|
|
|
|
const hasOptionSource =
|
|
|
|
|
|
Object.prototype.hasOwnProperty.call(fieldOptions, field.key) ||
|
|
|
|
|
|
Boolean(field.options);
|
|
|
|
|
|
const options = fieldOptions[field.key] ?? field.options;
|
2026-08-11 19:15:53 +08:00
|
|
|
|
const option = options?.find((item) => String(item.value) === String(value));
|
2026-08-11 00:49:41 +08:00
|
|
|
|
if (option) return option.label;
|
2026-08-11 02:01:00 +08:00
|
|
|
|
if (hasOptionSource && field.unknownValueLabel) {
|
|
|
|
|
|
return `${field.unknownValueLabel}(${String(value)})`;
|
|
|
|
|
|
}
|
2026-08-11 20:49:28 +08:00
|
|
|
|
if (field.displayPrecision != null) {
|
|
|
|
|
|
const numericValue = Number(value);
|
|
|
|
|
|
return Number.isFinite(numericValue)
|
|
|
|
|
|
? numericValue.toFixed(field.displayPrecision)
|
|
|
|
|
|
: String(value);
|
|
|
|
|
|
}
|
2026-08-11 00:49:41 +08:00
|
|
|
|
if (field.type === 'identity' && typeof value === 'string') {
|
|
|
|
|
|
return relationLabel(field, value, relationOptions);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (field.type === 'identity-list' && Array.isArray(value)) {
|
|
|
|
|
|
return value
|
|
|
|
|
|
.map((item) => relationLabel(field, String(item), relationOptions))
|
|
|
|
|
|
.join('、');
|
|
|
|
|
|
}
|
|
|
|
|
|
return displayRawValue(field.key, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 判断软删除结构是否为空。 */
|
|
|
|
|
|
export function isEmptyDeletedAt(value: unknown) {
|
|
|
|
|
|
if (value == null || value === '') return true;
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
typeof value === 'object' &&
|
|
|
|
|
|
value &&
|
|
|
|
|
|
'Valid' in value &&
|
|
|
|
|
|
!(value as { Valid?: boolean }).Valid,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|