feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
apps/web/stores/useOvertimeStore.ts
Normal file
116
apps/web/stores/useOvertimeStore.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { OvertimeRecord } from '@/lib/overtime';
|
||||
import type { DictItem } from '@/lib/requirement';
|
||||
import { calcDuration } from '@/lib/overtime';
|
||||
|
||||
const STORAGE_KEY = 'ftb_overtime_v1';
|
||||
|
||||
const PRESET_REASONS: DictItem[] = [
|
||||
{ id: 'reason-1', name: '需求变更', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-2', name: '需求新增', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-3', name: '版本冲刺', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-4', name: 'Bug修复', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-5', name: '线上问题修复', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-6', name: '测试问题整改', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-7', name: '技术难点攻关', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-8', name: '联调问题', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-9', name: '上游交付延误', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-10', name: '工作量超负荷', createdAt: '2024-01-01' },
|
||||
{ id: 'reason-11', name: '返工修改', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
const MOCK_RECORDS: OvertimeRecord[] = [
|
||||
{ id: 'ot-1', projectId: 'p1', versionId: 'v2', person: '张三', startTime: '2024-12-10T19:00', endTime: '2024-12-10T22:30', duration: 3.5, reasonId: 'reason-3', createdAt: '2024-12-10' },
|
||||
{ id: 'ot-2', projectId: 'p2', versionId: 'v5', person: '李四', startTime: '2024-12-11T18:30', endTime: '2024-12-11T21:00', duration: 2.5, reasonId: 'reason-1', createdAt: '2024-12-11' },
|
||||
{ id: 'ot-3', projectId: 'p1', versionId: 'v2', person: '王五', startTime: '2024-12-12T19:00', endTime: '2024-12-12T23:00', duration: 4, reasonId: 'reason-4', createdAt: '2024-12-12' },
|
||||
{ id: 'ot-4', projectId: 'p3', person: '张三', startTime: '2024-12-13T18:00', endTime: '2024-12-13T20:30', duration: 2.5, reasonId: 'reason-7', createdAt: '2024-12-13' },
|
||||
{ id: 'ot-5', projectId: 'p2', versionId: 'v5', person: '赵六', startTime: '2024-12-14T19:30', endTime: '2024-12-15T01:00', duration: 5.5, reasonId: 'reason-5', remark: '线上紧急事故', createdAt: '2024-12-14' },
|
||||
];
|
||||
|
||||
interface OvertimeState {
|
||||
records: OvertimeRecord[];
|
||||
reasons: DictItem[];
|
||||
loading: boolean;
|
||||
fetchRecords: () => void;
|
||||
createRecord: (data: Omit<OvertimeRecord, 'id' | 'duration' | 'createdAt'>) => void;
|
||||
updateRecord: (id: string, data: Partial<OvertimeRecord>) => void;
|
||||
deleteRecord: (id: string) => void;
|
||||
addReason: (name: string) => void;
|
||||
updateReason: (id: string, name: string) => void;
|
||||
deleteReason: (id: string) => void;
|
||||
}
|
||||
|
||||
function saveLocal(data: { records: OvertimeRecord[]; reasons: DictItem[] }) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
||||
}
|
||||
|
||||
function loadLocal(): { records: OvertimeRecord[]; reasons: DictItem[] } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
records: MOCK_RECORDS,
|
||||
reasons: PRESET_REASONS,
|
||||
loading: false,
|
||||
|
||||
fetchRecords: () => {
|
||||
const cached = loadLocal();
|
||||
if (cached) set({ records: cached.records, reasons: cached.reasons });
|
||||
},
|
||||
|
||||
createRecord: (data) => {
|
||||
const duration = calcDuration(data.startTime, data.endTime);
|
||||
const record: OvertimeRecord = {
|
||||
...data,
|
||||
id: `ot-${Date.now()}`,
|
||||
duration,
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
const records = [...get().records, record];
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
updateRecord: (id, data) => {
|
||||
const records = get().records.map((r) => {
|
||||
if (r.id !== id) return r;
|
||||
const updated = { ...r, ...data };
|
||||
if (data.startTime || data.endTime) {
|
||||
updated.duration = calcDuration(updated.startTime, updated.endTime);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
deleteRecord: (id) => {
|
||||
const records = get().records.filter((r) => r.id !== id);
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
addReason: (name) => {
|
||||
const item: DictItem = { id: `reason-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const reasons = [...get().reasons, item];
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
updateReason: (id, name) => {
|
||||
const reasons = get().reasons.map((r) => r.id === id ? { ...r, name } : r);
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
deleteReason: (id) => {
|
||||
const reasons = get().reasons.filter((r) => r.id !== id);
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user