Files
cn-rdms-web/src/views/product/dashboard/product-activity.ts

400 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import dayjs from 'dayjs';
const productStatusLabelMap = {
active: '启用',
paused: '暂停',
archived: '归档',
abandoned: '废弃'
} as const satisfies Record<Api.Product.ProductStatusCode, string>;
const activityTypeLabelMap = {
product: '产品',
status: '状态',
member: '成员'
} as const satisfies Record<Api.Product.ProductActivityType, string>;
export type ProductActivityFilterType = 'all' | Api.Product.ProductActivityType;
export type ProductActivityTone = 'sky' | 'emerald' | 'amber' | 'rose' | 'slate';
export interface ProductActivityTextPart {
text: string;
strong?: boolean;
}
export interface ProductActivityDisplayItem extends Api.Product.ProductActivityTimelineItem {
tagLabel: string;
timeText: string;
actionText: string;
displaySummary: string;
compactText: string;
compactTextParts: ProductActivityTextPart[];
operatorText: string;
subjectText: string;
reasonText: string;
statusTransition: string;
tone: ProductActivityTone;
}
export const PRODUCT_ACTIVITY_TYPE_OPTIONS: Array<{ label: string; value: ProductActivityFilterType }> = [
{ label: '全部', value: 'all' },
{ label: '产品', value: 'product' },
{ label: '状态', value: 'status' },
{ label: '成员', value: 'member' }
];
export const PRODUCT_ACTIVITY_ACTION_OPTIONS: Array<{
label: string;
value: Api.Product.ProductActivityActionType;
type: Api.Product.ProductActivityType;
}> = [
{ label: '产品创建', value: 'create', type: 'product' },
{ label: '产品经理变更', value: 'change_manager', type: 'product' },
{ label: '暂停', value: 'pause', type: 'status' },
{ label: '恢复', value: 'resume', type: 'status' },
{ label: '归档', value: 'archive', type: 'status' },
{ label: '废弃', value: 'abandon', type: 'status' },
{ label: '成员加入', value: 'add_member', type: 'member' },
{ label: '成员调整', value: 'update_member', type: 'member' },
{ label: '成员移出', value: 'remove_member', type: 'member' }
];
export const PRODUCT_ACTIVITY_TIME_SHORTCUTS = [
{ label: '最近7天', days: 7 },
{ label: '最近30天', days: 30 },
{ label: '最近90天', days: 90 }
] as const;
export const DEFAULT_PRODUCT_ACTIVITY_PAGE_SIZE = 10;
type ActivityDetailRecord = Record<string, unknown>;
function getStatusLabel(status: Api.Product.ProductStatusCode | null | undefined) {
if (!status) {
return '--';
}
return productStatusLabelMap[status] || '--';
}
function getActivityTone(item: Api.Product.ProductActivityTimelineItem): ProductActivityTone {
if (item.type === 'status') {
if (item.actionType === 'resume') {
return 'emerald';
}
if (item.actionType === 'pause') {
return 'amber';
}
if (item.actionType === 'abandon') {
return 'rose';
}
return 'slate';
}
if (item.type === 'product') {
return item.actionType === 'change_manager' ? 'emerald' : 'sky';
}
return item.actionType === 'remove_member' ? 'rose' : 'sky';
}
export function formatProductActivityTime(occurredAt: number | null | undefined) {
if (!Number.isFinite(occurredAt)) {
return '';
}
const parsed = dayjs(Number(occurredAt));
return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm') : '';
}
export function buildProductActivityRange(days: number): [string, string] {
const end = dayjs().endOf('day');
const start = dayjs()
.subtract(Math.max(days - 1, 0), 'day')
.startOf('day');
return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')];
}
export function getProductActivityActionOptions(activityType: ProductActivityFilterType) {
if (activityType === 'all') {
return PRODUCT_ACTIVITY_ACTION_OPTIONS;
}
return PRODUCT_ACTIVITY_ACTION_OPTIONS.filter(item => item.type === activityType);
}
export function normalizeProductActivityActionTypes(
activityType: ProductActivityFilterType,
actionTypes: readonly Api.Product.ProductActivityActionType[]
) {
const allowed = new Set(getProductActivityActionOptions(activityType).map(item => item.value));
return actionTypes.filter(actionType => allowed.has(actionType));
}
function parseActivityDetails(details: string | null | undefined): ActivityDetailRecord | null {
if (!details?.trim()) {
return null;
}
try {
const parsed = JSON.parse(details);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const normalized = parsed as ActivityDetailRecord;
const fieldChanges = normalized.fieldChanges;
if (fieldChanges && typeof fieldChanges === 'object' && !Array.isArray(fieldChanges)) {
return {
...normalized,
...(fieldChanges as ActivityDetailRecord)
};
}
return normalized;
}
} catch {}
return null;
}
function getRecordValue(record: ActivityDetailRecord | null, keys: readonly string[]) {
if (!record) {
return undefined;
}
const matchedKey = keys.find(key => key in record);
return matchedKey ? record[matchedKey] : undefined;
}
function getFieldChangeText(
record: ActivityDetailRecord | null,
keys: readonly string[],
preferredSide: 'before' | 'after'
) {
const rawValue = getRecordValue(record, keys);
if (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
const fieldChange = rawValue as { before?: unknown; after?: unknown };
const preferredValue = fieldChange[preferredSide];
if (preferredValue !== null && preferredValue !== undefined && String(preferredValue).trim()) {
return String(preferredValue).trim();
}
const fallbackSide = preferredSide === 'after' ? fieldChange.before : fieldChange.after;
if (fallbackSide !== null && fallbackSide !== undefined && String(fallbackSide).trim()) {
return String(fallbackSide).trim();
}
}
if (rawValue !== null && rawValue !== undefined && String(rawValue).trim()) {
return String(rawValue).trim();
}
return '';
}
function getActivityTargetUserName(
item: Api.Product.ProductActivityTimelineItem,
detailsRecord: ActivityDetailRecord | null
) {
const targetUserName = item.targetUserName?.trim() || '';
if (targetUserName) {
return targetUserName;
}
const preferredSide = item.actionType === 'remove_member' ? 'before' : 'after';
return getFieldChangeText(
detailsRecord,
[
'memberUserName',
'memberUserNickname',
'memberName',
'userNickname',
'userName',
'targetUserName',
'targetUserNickname'
],
preferredSide
);
}
function getActivityTargetRoleName(
item: Api.Product.ProductActivityTimelineItem,
detailsRecord: ActivityDetailRecord | null
) {
const preferredSide = item.actionType === 'remove_member' ? 'before' : 'after';
return getFieldChangeText(detailsRecord, ['roleName', 'memberRoleName', 'targetRoleName'], preferredSide);
}
function getRoleTransitionText(detailsRecord: ActivityDetailRecord | null) {
const beforeRoleName = getFieldChangeText(detailsRecord, ['roleName', 'memberRoleName', 'targetRoleName'], 'before');
const afterRoleName = getFieldChangeText(detailsRecord, ['roleName', 'memberRoleName', 'targetRoleName'], 'after');
if (beforeRoleName && afterRoleName && beforeRoleName !== afterRoleName) {
return `${beforeRoleName} -> ${afterRoleName}`;
}
return afterRoleName || beforeRoleName;
}
function isGenericActivitySummary(summaryText: string, actionText: string) {
if (!summaryText) {
return true;
}
return summaryText === actionText || summaryText === actionText.replace('执行了', '执行了');
}
function isMemberActivityAction(actionType: Api.Product.ProductActivityActionType) {
return actionType === 'add_member' || actionType === 'remove_member' || actionType === 'update_member';
}
function buildMemberChangeSummary(
item: Api.Product.ProductActivityTimelineItem,
detailsRecord: ActivityDetailRecord | null,
operatorText: string
) {
const memberName = getActivityTargetUserName(item, detailsRecord);
const roleName = getActivityTargetRoleName(item, detailsRecord);
if (!memberName) {
return '';
}
const memberDetail = roleName ? `${memberName}${roleName}` : memberName;
return operatorText === '--'
? `执行了【${item.actionName}】:${memberDetail}`
: `${operatorText}执行了【${item.actionName}】:${memberDetail}`;
}
function buildMemberUpdateSummary(
item: Api.Product.ProductActivityTimelineItem,
detailsRecord: ActivityDetailRecord | null,
operatorText: string
) {
const memberName = getActivityTargetUserName(item, detailsRecord);
const roleTransitionText = getRoleTransitionText(detailsRecord);
const memberText = memberName || '成员';
const roleText = roleTransitionText ? `,角色:${roleTransitionText}` : '';
return operatorText === '--'
? `执行了【${item.actionName}】:${memberText}${roleText}`
: `${operatorText}执行了【${item.actionName}】:${memberText}${roleText}`;
}
function buildManagerChangeSummary(detailsRecord: ActivityDetailRecord | null, operatorText: string) {
const beforeManagerName = getFieldChangeText(
detailsRecord,
['beforeManagerUserName', 'beforeManagerUserNickname', 'managerUserName', 'managerUserNickname', 'managerName'],
'before'
);
const afterManagerName = getFieldChangeText(
detailsRecord,
['afterManagerUserName', 'afterManagerUserNickname', 'managerUserName', 'managerUserNickname', 'managerName'],
'after'
);
if (!beforeManagerName && !afterManagerName) {
return '';
}
const transitionText =
beforeManagerName && afterManagerName
? `${beforeManagerName} -> ${afterManagerName}`
: afterManagerName || beforeManagerName;
return operatorText === '--' ? `变更产品经理:${transitionText}` : `${operatorText}变更产品经理:${transitionText}`;
}
function resolveDetailedSummary(
item: Api.Product.ProductActivityTimelineItem,
detailsRecord: ActivityDetailRecord | null,
texts: { operatorText: string; actionText: string }
) {
const { operatorText, actionText } = texts;
const summaryText = item.summary?.trim() || '';
if (item.actionType === 'add_member' || item.actionType === 'remove_member') {
return buildMemberChangeSummary(item, detailsRecord, operatorText) || summaryText || actionText;
}
if (item.actionType === 'update_member') {
return buildMemberUpdateSummary(item, detailsRecord, operatorText);
}
if (!isGenericActivitySummary(summaryText, actionText)) {
return summaryText;
}
if (item.actionType === 'change_manager') {
return buildManagerChangeSummary(detailsRecord, operatorText) || summaryText || actionText;
}
return summaryText || actionText;
}
function buildProductActivityTextParts(text: string, subjectText: string): ProductActivityTextPart[] {
const normalizedSubject = subjectText.trim();
const subjectIndex = normalizedSubject ? text.indexOf(normalizedSubject) : -1;
if (subjectIndex < 0) {
return [{ text }];
}
return [
{ text: text.slice(0, subjectIndex) },
{ text: normalizedSubject, strong: true },
{ text: text.slice(subjectIndex + normalizedSubject.length) }
].filter(part => part.text);
}
export function buildProductActivityDisplayItem(
item: Api.Product.ProductActivityTimelineItem
): ProductActivityDisplayItem {
const operatorText = item.operatorName?.trim() || '--';
const actionText =
operatorText === '--' ? `执行了【${item.actionName}` : `${operatorText}执行了【${item.actionName}`;
const detailsRecord = parseActivityDetails(item.details);
const subjectText = isMemberActivityAction(item.actionType) ? getActivityTargetUserName(item, detailsRecord) : '';
const displaySummary =
item.type === 'status' ? actionText : resolveDetailedSummary(item, detailsRecord, { operatorText, actionText });
const compactText = displaySummary;
return {
...item,
tagLabel: activityTypeLabelMap[item.type],
timeText: formatProductActivityTime(item.occurredAt) || '--',
actionText,
displaySummary,
compactText,
compactTextParts: buildProductActivityTextParts(compactText, subjectText),
operatorText,
subjectText,
reasonText: item.reason?.trim() || '',
statusTransition:
item.type === 'status' && item.fromStatus && item.toStatus
? `${getStatusLabel(item.fromStatus)} -> ${getStatusLabel(item.toStatus)}`
: '',
tone: getActivityTone(item)
};
}
export function buildProductActivityDisplayItems(
items: readonly Api.Product.ProductActivityTimelineItem[] | null | undefined
) {
return [...(items || [])].map(buildProductActivityDisplayItem);
}