feat(workspace): 增加工作活动日报引擎

This commit is contained in:
Script Generator
2026-06-26 17:17:35 +08:00
parent 64491789ae
commit 4278e4d565
21 changed files with 1090 additions and 24 deletions

View File

@@ -4,6 +4,12 @@ import type { Bug, BugStatus, BugLog } from '@/lib/bug';
import { generateBugNo } from '@/lib/bug';
import { applyBugTransition } from '@/lib/bug-workflow';
import { loadServerData, saveServerData } from '@/lib/server-data';
import {
makeBugCreatedActivity,
makeBugStatusActivity,
makeBugTransferredActivity,
} from '@/lib/work-activity-factory';
import { useWorkActivityStore } from './useWorkActivityStore';
function saveStored(items: Bug[]) {
saveServerData('bugs', items).catch(() => {});
@@ -57,6 +63,7 @@ export const useBugStore = create<BugState>((set, get) => ({
const updated = [...list, bug];
set({ bugs: updated });
saveStored(updated);
useWorkActivityStore.getState().addActivity(makeBugCreatedActivity(bug, operator));
return bug;
},
@@ -83,6 +90,8 @@ export const useBugStore = create<BugState>((set, get) => ({
});
if (!result.ok || !result.patch) return { ok: false, message: result.message };
get().updateBug(id, result.patch);
const activity = makeBugStatusActivity(bug, bug.status, to, operator);
if (activity) useWorkActivityStore.getState().addActivity(activity);
return { ok: true };
},
@@ -92,6 +101,7 @@ export const useBugStore = create<BugState>((set, get) => ({
if (bug.assigneeId === newAssigneeId) return { ok: false, message: '已是当前负责人' };
const log = makeLog('transfer', operator, bug.assigneeId, newAssigneeId, remark);
get().updateBug(id, { assigneeId: newAssigneeId, logs: [...(bug.logs || []), log] });
useWorkActivityStore.getState().addActivity(makeBugTransferredActivity(bug, operator, newAssigneeId));
return { ok: true };
},

View File

@@ -5,6 +5,13 @@ import { generateTaskNo, isLegacyTask } from '@/lib/dev-task';
import { applyDevTaskTransition, normalizeDevTaskOnCreate } from '@/lib/dev-task-workflow';
import { createEntityId, dedupeEntityIds } from '@/lib/entity-id';
import { loadServerData, saveServerData } from '@/lib/server-data';
import {
makeDevTaskBlockedActivity,
makeDevTaskCreatedActivity,
makeDevTaskStatusActivity,
makeDevTaskUnblockedActivity,
} from '@/lib/work-activity-factory';
import { useWorkActivityStore } from './useWorkActivityStore';
function saveStored(items: DevTask[]) {
saveServerData('dev-tasks', items).catch(() => {});
@@ -60,6 +67,7 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
const updated = [...list, task];
set({ tasks: updated });
saveStored(updated);
useWorkActivityStore.getState().addActivity(makeDevTaskCreatedActivity(task, task.createdBy || task.assigneeId));
return task;
},
@@ -86,15 +94,23 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
});
if (!result.ok || !result.patch) return { ok: false, message: result.message };
get().updateTask(id, result.patch);
const activity = makeDevTaskStatusActivity(task, task.status, to, task.assigneeId);
if (activity) useWorkActivityStore.getState().addActivity(activity);
return { ok: true };
},
setBlocked: (id, blocked, reason, blockedById) => {
const task = get().tasks.find((t) => t.id === id);
if (!task) return;
get().updateTask(id, {
isBlocked: blocked,
blockReason: blocked ? reason : undefined,
blockedById: blocked ? blockedById : undefined,
});
const activity = blocked
? makeDevTaskBlockedActivity(task, task.assigneeId, reason, blockedById)
: makeDevTaskUnblockedActivity(task, task.assigneeId);
useWorkActivityStore.getState().addActivity(activity);
},
getByRequirement: (requirementId) => {

View File

@@ -5,6 +5,11 @@ import { generateCaseNo, normalizeTestCases } from '@/lib/test-case';
import { applyTestCaseTransition, normalizeTestCaseOnCreate } from '@/lib/test-case-workflow';
import { createEntityId, dedupeEntityIds } from '@/lib/entity-id';
import { loadServerData, saveServerData } from '@/lib/server-data';
import {
makeTestCaseCreatedActivity,
makeTestCaseStatusActivity,
} from '@/lib/work-activity-factory';
import { useWorkActivityStore } from './useWorkActivityStore';
function saveStored(items: TestCase[]) {
saveServerData('test-cases', items).catch(() => {});
@@ -56,6 +61,7 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
const updated = [...list, tc];
set({ testCases: updated });
saveStored(updated);
useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy));
return tc;
},
@@ -78,6 +84,9 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
}
set({ testCases: list });
saveStored(list);
created.forEach((tc) => {
useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy));
});
return created;
},
@@ -105,6 +114,9 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
});
if (!result.ok || !result.patch) return { ok: false, message: result.message };
get().updateTestCase(id, result.patch);
const actorId = tc.assigneeId || tc.executedBy || tc.createdBy;
const activity = makeTestCaseStatusActivity(tc, tc.status, to, actorId);
if (activity) useWorkActivityStore.getState().addActivity(activity);
return { ok: true };
},

View File

@@ -4,6 +4,12 @@ import type { VersionPlan, PlanType } from '@/lib/version-plan';
import { loadServerData, saveServerData } from '@/lib/server-data';
import { getPlanCompletionState } from '@/lib/version-plan-workflow';
import type { PlanResultPayload } from '@/lib/version-plan-workflow';
import {
makeVersionPlanCompletedActivity,
makeVersionPlanCreatedActivity,
makeVersionPlanStartedActivity,
} from '@/lib/work-activity-factory';
import { useWorkActivityStore } from './useWorkActivityStore';
const MOCK_PLANS: VersionPlan[] = [];
@@ -40,10 +46,12 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
const plans = [...get().plans, plan];
set({ plans });
saveStored(plans);
useWorkActivityStore.getState().addActivity(makeVersionPlanCreatedActivity(plan, plan.addedBy || plan.owner));
},
updatePlan: (id, data) => {
const now = new Date().toISOString();
const activities: ReturnType<typeof makeVersionPlanStartedActivity>[] = [];
const plans = get().plans.map((p) => {
if (p.id !== id) return p;
const patch = { ...data };
@@ -53,10 +61,18 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
if (patch.status === 'completed' && !p.completedAt) {
(patch as any).completedAt = now;
}
return { ...p, ...patch };
const next = { ...p, ...patch };
if (p.status !== 'in_progress' && next.status === 'in_progress') {
activities.push(makeVersionPlanStartedActivity(next, next.owner));
}
if (p.status !== 'completed' && next.status === 'completed') {
activities.push(makeVersionPlanCompletedActivity(next, next.owner));
}
return next;
});
set({ plans });
saveStored(plans);
activities.forEach((activity) => useWorkActivityStore.getState().addActivity(activity));
},
completePlan: (id, result) => {
@@ -70,6 +86,7 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
return p;
}
response = { ok: true };
useWorkActivityStore.getState().addActivity(makeVersionPlanCompletedActivity(next, next.owner));
return next;
});
set({ plans });

View File

@@ -0,0 +1,108 @@
'use client';
import { create } from 'zustand';
import { formatLocalDate } from '@/lib/format';
import { mergeWorkActivities, type WorkActivity, type WorkActivityDraft, type WorkActivitySourceType } from '@/lib/work-activity';
import { loadServerData, saveServerData } from '@/lib/server-data';
interface ProgressNoteInput {
actorId: string;
sourceType: WorkActivitySourceType;
sourceId: string;
title: string;
note: string;
blocker?: string;
helperId?: string;
delayRisk?: string;
}
interface WorkActivityState {
activities: WorkActivity[];
fetchActivities: () => Promise<void>;
addActivity: (data: WorkActivityDraft) => WorkActivity;
addProgressNote: (data: ProgressNoteInput) => WorkActivity;
deleteActivity: (id: string) => void;
}
async function saveStored(
items: WorkActivity[],
setActivities?: (items: WorkActivity[]) => void,
options: { mergeRemote?: boolean } = {},
) {
try {
if (options.mergeRemote === false) {
await saveServerData('work-activities', items);
return;
}
const remote = await loadServerData<WorkActivity[]>('work-activities');
const merged = mergeWorkActivities(Array.isArray(remote) ? remote : [], items);
setActivities?.(merged);
await saveServerData('work-activities', merged);
} catch {
saveServerData('work-activities', items).catch(() => {});
}
}
async function loadStored(): Promise<WorkActivity[] | null> {
try {
return await loadServerData<WorkActivity[]>('work-activities');
} catch {}
return null;
}
function createActivityId(): string {
return `act-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
}
export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
activities: [],
fetchActivities: async () => {
const cached = await loadStored();
if (cached) set({ activities: cached });
},
addActivity: (data) => {
const now = new Date();
const item: WorkActivity = {
...data,
id: createActivityId(),
date: data.date ?? formatLocalDate(now),
occurredAt: data.occurredAt ?? now.toISOString(),
};
const updated = [...get().activities, item];
set({ activities: updated });
saveStored(updated, (activities) => set({ activities }));
return item;
},
addProgressNote: (data) => {
const details = [
data.note.trim(),
data.blocker?.trim() ? `阻塞:${data.blocker.trim()}` : '',
data.helperId?.trim() ? `需协助:${data.helperId.trim()}` : '',
data.delayRisk?.trim() ? `延期风险:${data.delayRisk.trim()}` : '',
].filter(Boolean);
return get().addActivity({
actorId: data.actorId,
sourceType: data.sourceType,
sourceId: data.sourceId,
action: 'progress_note_added',
category: data.blocker?.trim() || data.delayRisk?.trim() ? 'risk' : 'note',
title: data.title,
summary: `补充进展:${details.join('')}`,
metadata: {
note: data.note.trim(),
blocker: data.blocker?.trim() || undefined,
helperId: data.helperId?.trim() || undefined,
delayRisk: data.delayRisk?.trim() || undefined,
},
});
},
deleteActivity: (id) => {
const updated = get().activities.filter((activity) => activity.id !== id);
set({ activities: updated });
saveStored(updated, (activities) => set({ activities }), { mergeRemote: false });
},
}));