feat(版本): 完善研发计划与预警已读

关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 16:48:58 +08:00
parent 04520bd8c5
commit 3d3d56697a
41 changed files with 1701 additions and 121 deletions

View File

@@ -9,7 +9,7 @@ import {
ensureSystemAdminMember,
sanitizeSystemAdminPatch,
} from '@/lib/member-system';
import { DEFAULT_ROLE_PERMISSIONS } from '@/lib/permissions';
import { DEFAULT_ROLE_PERMISSIONS, DEFAULT_ROLE_PRESETS } from '@/lib/permissions';
import { mergePresetRolePermissions } from '@/lib/role-permission-migration';
import { loadServerData, saveServerData } from '@/lib/server-data';
@@ -23,13 +23,7 @@ const PRESET_DEPARTMENTS: Department[] = [
{ id: 'dept-4', name: '运营部', order: 4, createdAt: '2024-01-01' },
];
const PRESET_ROLES: RoleItem[] = [
{ id: 'role-admin', name: '超级管理员', description: '拥有系统全部权限', createdAt: '2024-01-01', isSystem: true, permissions: DEFAULT_ROLE_PERMISSIONS['role-admin'] },
{ id: 'role-pm', name: '产品经理', description: '管理产品和需求', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-pm'] },
{ id: 'role-dev', name: '开发工程师', description: '负责开发任务', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-dev'] },
{ id: 'role-test', name: '测试工程师', description: '负责测试任务', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-test'] },
{ id: 'role-design', name: '设计师', description: '负责UI/UX设计', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-design'] },
];
const PRESET_ROLES: RoleItem[] = DEFAULT_ROLE_PRESETS;
const MOCK_MEMBERS: Member[] = [
SYSTEM_ADMIN_MEMBER,

View File

@@ -8,6 +8,7 @@ import {
makeVersionPlanCompletedActivity,
makeVersionPlanCreatedActivity,
makeVersionPlanRequirementProgressActivity,
makeVersionPlanResearchDirectionProgressActivity,
makeVersionPlanStartedActivity,
} from '@/lib/work-activity-factory';
import type { WorkActivityDraft } from '@/lib/work-activity';
@@ -73,8 +74,10 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
const previousLogIds = new Set((p.logs ?? []).map((log) => log.id));
for (const log of next.logs ?? []) {
if (previousLogIds.has(log.id)) continue;
const activity = makeVersionPlanRequirementProgressActivity(next, log);
if (activity) activities.push(activity);
const requirementActivity = makeVersionPlanRequirementProgressActivity(next, log);
if (requirementActivity) activities.push(requirementActivity);
const directionActivity = makeVersionPlanResearchDirectionProgressActivity(next, log);
if (directionActivity) activities.push(directionActivity);
}
return next;
});

View File

@@ -0,0 +1,94 @@
'use client';
import { create } from 'zustand';
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
import {
markXiaobaoWarningRead,
type XiaobaoWarningReadState,
} from '@/lib/xiaobao-warning-view';
import { loadServerData, saveServerData } from '@/lib/server-data';
interface XiaobaoWarningReadStoreState {
readStates: XiaobaoWarningReadState[];
readStateLoaded: boolean;
error?: string;
fetchReadStates: () => Promise<void>;
markRiskRead: (userId: string, risk: XiaobaoVersionRisk, readAt?: string) => Promise<void>;
}
let readStateSaveQueue: Promise<void> = Promise.resolve();
async function loadReadStates(): Promise<XiaobaoWarningReadState[] | null> {
try {
const rows = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views');
return normalizeReadStates(rows);
} catch {}
return null;
}
function normalizeReadStates(rows: unknown): XiaobaoWarningReadState[] {
if (!Array.isArray(rows)) return [];
return rows.filter(isReadState);
}
function isReadState(row: unknown): row is XiaobaoWarningReadState {
if (!row || typeof row !== 'object') return false;
const item = row as Partial<XiaobaoWarningReadState>;
return (
typeof item.userId === 'string' &&
typeof item.versionId === 'string' &&
typeof item.signature === 'string' &&
typeof item.readAt === 'string'
);
}
function mergeReadStates(
remoteRows: XiaobaoWarningReadState[] = [],
localRows: XiaobaoWarningReadState[] = [],
): XiaobaoWarningReadState[] {
const byKey = new Map<string, XiaobaoWarningReadState>();
for (const row of [...remoteRows, ...localRows]) {
const key = `${row.userId}::${row.versionId}`;
const existing = byKey.get(key);
if (!existing || row.readAt.localeCompare(existing.readAt) >= 0) {
byKey.set(key, row);
}
}
return Array.from(byKey.values()).sort((a, b) => b.readAt.localeCompare(a.readAt));
}
export const useXiaobaoWarningReadStore = create<XiaobaoWarningReadStoreState>((set, get) => ({
readStates: [],
readStateLoaded: false,
error: undefined,
fetchReadStates: async () => {
const rows = await loadReadStates();
set({
readStates: rows ? mergeReadStates(rows, get().readStates) : get().readStates,
readStateLoaded: rows !== null,
error: rows === null ? '小宝预警查看状态加载失败' : undefined,
});
},
markRiskRead: async (userId, risk, readAt = new Date().toISOString()) => {
if (!userId) return;
const optimistic = markXiaobaoWarningRead(get().readStates, userId, risk, readAt);
set({ readStates: optimistic, error: undefined });
const task = readStateSaveQueue.then(async () => {
const remote = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views');
const merged = mergeReadStates(normalizeReadStates(remote), get().readStates);
set({ readStates: merged, readStateLoaded: true, error: undefined });
await saveServerData('xiaobao-warning-views', merged);
});
readStateSaveQueue = task.catch(() => undefined);
try {
await task;
} catch (error) {
set({ error: '小宝预警查看状态保存失败' });
throw error;
}
},
}));