import dayjs from 'dayjs'; const productStatusLabelMap = { active: '启用', paused: '暂停', archived: '归档', abandoned: '废弃' } as const satisfies Record; const activityTypeLabelMap = { product: '产品', status: '状态', member: '成员' } as const satisfies Record; export type ProductActivityFilterType = 'all' | Api.Product.ProductActivityType; export type ProductActivityTone = 'sky' | 'emerald' | 'amber' | 'rose' | 'slate'; export interface ProductActivityDisplayItem extends Api.Product.ProductActivityTimelineItem { tagLabel: string; timeText: string; actionText: string; displaySummary: string; compactText: string; operatorText: 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; 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 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; const actionLabel = item.actionType === 'add_member' ? '将成员加入产品' : '将成员移出产品'; return operatorText === '--' ? `${actionLabel}:${memberDetail}` : `${operatorText}${actionLabel}:${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 === '--' ? `调整成员:${memberText}${roleText}` : `${operatorText}调整成员:${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, operatorText: string, actionText: string ) { const summaryText = item.summary?.trim() || ''; const detailsRecord = parseActivityDetails(item.details); if (!isGenericActivitySummary(summaryText, actionText)) { return summaryText; } 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 (item.actionType === 'change_manager') { return buildManagerChangeSummary(detailsRecord, operatorText) || summaryText || actionText; } return summaryText || actionText; } export function buildProductActivityDisplayItem( item: Api.Product.ProductActivityTimelineItem ): ProductActivityDisplayItem { const operatorText = item.operatorName?.trim() || '--'; const actionText = operatorText === '--' ? `执行了【${item.actionName}】` : `${operatorText}执行了【${item.actionName}】`; const displaySummary = item.type === 'status' ? actionText : resolveDetailedSummary(item, operatorText, actionText); const compactText = displaySummary; return { ...item, tagLabel: activityTypeLabelMap[item.type], timeText: formatProductActivityTime(item.occurredAt) || '--', actionText, displaySummary, compactText, operatorText, 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); }