feat(版本): 完善研发计划与预警已读
关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
94
apps/web/stores/useXiaobaoWarningReadStore.ts
Normal file
94
apps/web/stores/useXiaobaoWarningReadStore.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user