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

400 lines
12 KiB
TypeScript
Raw Normal View History

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);
}