feat(平台): 补齐服务端持久化和AI拆解契约
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { loadServerData } from '@/lib/server-data';
|
||||
import type { Member } from '@/lib/members';
|
||||
import { useMemberStore } from './useMemberStore';
|
||||
|
||||
interface AuthUser {
|
||||
id: string;
|
||||
@@ -14,53 +17,63 @@ interface AuthUser {
|
||||
interface AuthState {
|
||||
user: AuthUser | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (phone: string, password: string, remember: boolean) => boolean;
|
||||
login: (phone: string, password: string, remember: boolean) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
checkAuth: () => void;
|
||||
refreshUser: () => void;
|
||||
refreshUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface StoredMembersData {
|
||||
members: Member[];
|
||||
}
|
||||
|
||||
const SESSION_KEY = 'ftb_auth_session';
|
||||
const PERSIST_KEY = 'ftb_auth_persist';
|
||||
|
||||
const FALLBACK_MEMBERS: Member[] = [
|
||||
{
|
||||
id: 'm-8',
|
||||
name: '\u8d85\u7ea7\u7ba1\u7406\u5458',
|
||||
departmentId: 'dept-1',
|
||||
roleId: 'role-admin',
|
||||
phone: '13200132008',
|
||||
email: 'chenshi@company.com',
|
||||
password: 'Ftb@2024',
|
||||
createdAt: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
async function loadMembersForAuth(): Promise<Member[]> {
|
||||
try {
|
||||
const stored = await loadServerData<StoredMembersData>('members');
|
||||
if (stored?.members?.length) return stored.members;
|
||||
} catch {}
|
||||
|
||||
const inMemoryMembers = useMemberStore.getState().members;
|
||||
return inMemoryMembers.length > 0 ? inMemoryMembers : FALLBACK_MEMBERS;
|
||||
}
|
||||
|
||||
function toAuthUser(member: Member): AuthUser {
|
||||
return {
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
roleId: member.roleId,
|
||||
departmentId: member.departmentId,
|
||||
phone: member.phone,
|
||||
email: member.email,
|
||||
};
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
|
||||
login: (phone, password, remember) => {
|
||||
const membersRaw = localStorage.getItem('ftb_members_v1');
|
||||
let members: any[] = [];
|
||||
if (membersRaw) {
|
||||
const parsed = JSON.parse(membersRaw);
|
||||
members = parsed.members || [];
|
||||
}
|
||||
|
||||
// fallback: 如果 localStorage 没有数据或为空,使用默认 mock
|
||||
if (members.length === 0) {
|
||||
members = [
|
||||
{ id: 'm-1', name: '张三', departmentId: 'dept-1', roleId: 'role-pm', phone: '13800138001', email: 'zhangsan@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-2', name: '李四', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13900139002', email: 'lisi@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-3', name: '王五', departmentId: 'dept-2-2', roleId: 'role-dev', phone: '13700137003', email: 'wangwu@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-4', name: '赵六', departmentId: 'dept-2-3', roleId: 'role-test', phone: '13600136004', email: 'zhaoliu@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-5', name: '孙七', departmentId: 'dept-3', roleId: 'role-design', phone: '13500135005', email: 'sunqi@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-6', name: '周八', departmentId: 'dept-4', roleId: 'role-pm', phone: '13400134006', email: 'zhouba@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-7', name: '吴九', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13300133007', email: 'wujiu@company.com', password: 'Ftb@2024' },
|
||||
{ id: 'm-8', name: '陈十', departmentId: 'dept-1', roleId: 'role-admin', phone: '13200132008', email: 'chenshi@company.com', password: 'Ftb@2024' },
|
||||
];
|
||||
}
|
||||
|
||||
const member = members.find((m: any) => m.phone === phone && (m.password || 'Ftb@2024') === password);
|
||||
login: async (phone, password, remember) => {
|
||||
const members = await loadMembersForAuth();
|
||||
const member = members.find((m) => m.phone === phone && (m.password || 'Ftb@2024') === password);
|
||||
if (!member) return false;
|
||||
|
||||
const user: AuthUser = {
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
roleId: member.roleId,
|
||||
departmentId: member.departmentId,
|
||||
phone: member.phone,
|
||||
email: member.email,
|
||||
};
|
||||
|
||||
const user = toAuthUser(member);
|
||||
set({ user, isAuthenticated: true });
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(user));
|
||||
if (remember) {
|
||||
@@ -90,16 +103,16 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
}
|
||||
},
|
||||
|
||||
refreshUser: () => {
|
||||
refreshUser: async () => {
|
||||
const cur = useAuthStore.getState().user;
|
||||
if (!cur) return;
|
||||
try {
|
||||
const raw = localStorage.getItem('ftb_members_v1');
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw);
|
||||
const m = parsed.members?.find((x: any) => x.id === cur.id);
|
||||
if (!m) return;
|
||||
const next: AuthUser = { id: m.id, name: m.name, roleId: m.roleId, departmentId: m.departmentId, phone: m.phone, email: m.email };
|
||||
const memoryMember = useMemberStore.getState().members.find((x) => x.id === cur.id);
|
||||
const members = memoryMember ? [memoryMember] : await loadMembersForAuth();
|
||||
const member = members.find((x) => x.id === cur.id);
|
||||
if (!member) return;
|
||||
|
||||
const next = toAuthUser(member);
|
||||
useAuthStore.setState({ user: next });
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(next));
|
||||
if (localStorage.getItem(PERSIST_KEY)) {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { create } from 'zustand';
|
||||
import type { Department, Member, RoleItem, PasswordRule } from '@/lib/members';
|
||||
import { DEFAULT_PASSWORD_RULE, generatePassword } from '@/lib/members';
|
||||
import { DEFAULT_ROLE_PERMISSIONS } from '@/lib/permissions';
|
||||
|
||||
const STORAGE_KEY = 'ftb_members_v1';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
const PRESET_DEPARTMENTS: Department[] = [
|
||||
{ id: 'dept-1', name: '产品部', order: 1, createdAt: '2024-01-01' },
|
||||
@@ -25,14 +24,7 @@ const PRESET_ROLES: RoleItem[] = [
|
||||
];
|
||||
|
||||
const MOCK_MEMBERS: Member[] = [
|
||||
{ id: 'm-1', name: '张三', departmentId: 'dept-1', roleId: 'role-pm', phone: '13800138001', email: 'zhangsan@company.com', password: 'Ftb@2024', createdAt: '2024-01-10' },
|
||||
{ id: 'm-2', name: '李四', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13900139002', email: 'lisi@company.com', password: 'Ftb@2024', createdAt: '2024-01-12' },
|
||||
{ id: 'm-3', name: '王五', departmentId: 'dept-2-2', roleId: 'role-dev', phone: '13700137003', email: 'wangwu@company.com', password: 'Ftb@2024', createdAt: '2024-01-15' },
|
||||
{ id: 'm-4', name: '赵六', departmentId: 'dept-2-3', roleId: 'role-test', phone: '13600136004', email: 'zhaoliu@company.com', password: 'Ftb@2024', createdAt: '2024-02-01' },
|
||||
{ id: 'm-5', name: '孙七', departmentId: 'dept-3', roleId: 'role-design', phone: '13500135005', email: 'sunqi@company.com', password: 'Ftb@2024', createdAt: '2024-02-10' },
|
||||
{ id: 'm-6', name: '周八', departmentId: 'dept-4', roleId: 'role-pm', phone: '13400134006', email: 'zhouba@company.com', password: 'Ftb@2024', createdAt: '2024-02-15' },
|
||||
{ id: 'm-7', name: '吴九', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13300133007', email: 'wujiu@company.com', password: 'Ftb@2024', createdAt: '2024-03-01' },
|
||||
{ id: 'm-8', name: '陈十', departmentId: 'dept-1', roleId: 'role-admin', phone: '13200132008', email: 'chenshi@company.com', password: 'Ftb@2024', createdAt: '2024-01-01' },
|
||||
{ id: 'm-8', name: '超级管理员', departmentId: 'dept-1', roleId: 'role-admin', phone: '13200132008', email: 'chenshi@company.com', password: 'Ftb@2024', createdAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
interface MemberState {
|
||||
@@ -54,14 +46,13 @@ interface MemberState {
|
||||
deleteRole: (id: string) => void;
|
||||
}
|
||||
|
||||
function saveLocal(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch {}
|
||||
function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
|
||||
saveServerData('members', state).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null {
|
||||
async function loadStored(): Promise<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }>('members');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
@@ -73,8 +64,8 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
passwordRule: DEFAULT_PASSWORD_RULE,
|
||||
loading: false,
|
||||
|
||||
fetchMembers: () => {
|
||||
const cached = loadLocal();
|
||||
fetchMembers: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) {
|
||||
// 迁移:旧 RoleItem 没 permissions 字段,补默认值
|
||||
const migratedRoles = cached.roles.map((r) => {
|
||||
@@ -89,41 +80,41 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
|
||||
updatePasswordRule: (rule) => {
|
||||
set({ passwordRule: rule });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule });
|
||||
saveStored({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule });
|
||||
},
|
||||
|
||||
createDepartment: (data) => {
|
||||
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const departments = [...get().departments, dept];
|
||||
set({ departments });
|
||||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
updateDepartment: (id, data) => {
|
||||
const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d);
|
||||
set({ departments });
|
||||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
deleteDepartment: (id) => {
|
||||
const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id);
|
||||
set({ departments });
|
||||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
|
||||
createMember: (data) => {
|
||||
const member: Member = { ...data, id: `m-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const members = [...get().members, member];
|
||||
set({ members });
|
||||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
updateMember: (id, data) => {
|
||||
const members = get().members.map((m) => m.id === id ? { ...m, ...data } : m);
|
||||
set({ members });
|
||||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
deleteMember: (id) => {
|
||||
const members = get().members.filter((m) => m.id !== id);
|
||||
set({ members });
|
||||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
|
||||
createRole: (data) => {
|
||||
@@ -135,7 +126,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
};
|
||||
const roles = [...get().roles, role];
|
||||
set({ roles });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
updateRole: (id, data) => {
|
||||
const roles = get().roles.map((r) => {
|
||||
@@ -148,11 +139,11 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
return { ...r, ...data };
|
||||
});
|
||||
set({ roles });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
deleteRole: (id) => {
|
||||
const roles = get().roles.filter((r) => r.id !== id);
|
||||
set({ roles });
|
||||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,8 +3,7 @@ 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';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
const PRESET_REASONS: DictItem[] = [
|
||||
{ id: 'reason-1', name: '需求变更', createdAt: '2024-01-01' },
|
||||
@@ -20,19 +19,13 @@ const PRESET_REASONS: DictItem[] = [
|
||||
{ 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' },
|
||||
];
|
||||
const MOCK_RECORDS: OvertimeRecord[] = [];
|
||||
|
||||
interface OvertimeState {
|
||||
records: OvertimeRecord[];
|
||||
reasons: DictItem[];
|
||||
loading: boolean;
|
||||
fetchRecords: () => void;
|
||||
fetchRecords: () => Promise<void>;
|
||||
createRecord: (data: Omit<OvertimeRecord, 'id' | 'duration' | 'createdAt'>) => void;
|
||||
updateRecord: (id: string, data: Partial<OvertimeRecord>) => void;
|
||||
deleteRecord: (id: string) => void;
|
||||
@@ -41,14 +34,13 @@ interface OvertimeState {
|
||||
deleteReason: (id: string) => void;
|
||||
}
|
||||
|
||||
function saveLocal(data: { records: OvertimeRecord[]; reasons: DictItem[] }) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
||||
function saveStored(data: { records: OvertimeRecord[]; reasons: DictItem[] }) {
|
||||
saveServerData('overtime', data).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): { records: OvertimeRecord[]; reasons: DictItem[] } | null {
|
||||
async function loadStored(): Promise<{ records: OvertimeRecord[]; reasons: DictItem[] } | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<{ records: OvertimeRecord[]; reasons: DictItem[] }>('overtime');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
@@ -58,8 +50,8 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
reasons: PRESET_REASONS,
|
||||
loading: false,
|
||||
|
||||
fetchRecords: () => {
|
||||
const cached = loadLocal();
|
||||
fetchRecords: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ records: cached.records, reasons: cached.reasons });
|
||||
},
|
||||
|
||||
@@ -73,7 +65,7 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
};
|
||||
const records = [...get().records, record];
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
saveStored({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
updateRecord: (id, data) => {
|
||||
@@ -86,31 +78,31 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
return updated;
|
||||
});
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
saveStored({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
deleteRecord: (id) => {
|
||||
const records = get().records.filter((r) => r.id !== id);
|
||||
set({ records });
|
||||
saveLocal({ records, reasons: get().reasons });
|
||||
saveStored({ 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 });
|
||||
saveStored({ 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 });
|
||||
saveStored({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
deleteReason: (id) => {
|
||||
const reasons = get().reasons.filter((r) => r.id !== id);
|
||||
set({ reasons });
|
||||
saveLocal({ records: get().records, reasons });
|
||||
saveStored({ records: get().records, reasons });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { create } from 'zustand';
|
||||
import { Product } from '@ftb/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { shouldPersistRemoteOverview } from '@/lib/product-overview-persistence';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import type { Stage, Role } from '@/lib/stage';
|
||||
import type { Priority, VersionLinks } from '@/lib/derive';
|
||||
|
||||
@@ -32,20 +34,7 @@ interface VersionItem {
|
||||
links?: VersionLinks;
|
||||
}
|
||||
|
||||
const MOCK_OVERVIEW: ProductOverview[] = [
|
||||
{
|
||||
id: 'mock-1',
|
||||
name: '翻台宝',
|
||||
description: '餐饮 SaaS 主产品,覆盖门店运营管理全流程',
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-06-01',
|
||||
projects: [
|
||||
{ id: 'p3', name: '值班', description: '门店值班排班与交接', createdAt: '2024-03-01' },
|
||||
],
|
||||
versions: [],
|
||||
_count: { requirements: 10, projects: 1, versions: 0 },
|
||||
},
|
||||
];
|
||||
const MOCK_OVERVIEW: ProductOverview[] = [];
|
||||
|
||||
interface ProductWithCount {
|
||||
id: string;
|
||||
@@ -101,8 +90,8 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
|
||||
fetchOverview: async () => {
|
||||
if (get().overview.length > 0) return;
|
||||
const cached = loadLocal();
|
||||
if (cached.length > 0) {
|
||||
const cached = await loadStoredOverview();
|
||||
if (cached) {
|
||||
// 有本地缓存,直接用,不再调远端覆盖(mock 模式核心数据在本地)
|
||||
set({ overview: cached, loading: false });
|
||||
return;
|
||||
@@ -111,10 +100,11 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
try {
|
||||
const overview = await api.get<ProductOverview[]>('/products/overview');
|
||||
set({ overview, loading: false });
|
||||
saveLocal(overview);
|
||||
if (shouldPersistRemoteOverview(overview)) {
|
||||
saveStoredOverview(overview);
|
||||
}
|
||||
} catch {
|
||||
set({ overview: MOCK_OVERVIEW, error: null, loading: false });
|
||||
saveLocal(MOCK_OVERVIEW);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -147,7 +137,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
};
|
||||
const updated = [newProduct, ...get().overview];
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
// 异步尝试同步远端(失败也无所谓,本地已更新)
|
||||
try { await api.post('/products', data); } catch {}
|
||||
},
|
||||
@@ -161,7 +151,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
p.id === id ? { ...p, ...data } : p,
|
||||
);
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,14 +164,14 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
products: get().products.filter((p) => p.id !== id),
|
||||
overview: updated,
|
||||
});
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
reorderProducts: (ids) => {
|
||||
const map = new Map(get().overview.map((p) => [p.id, p]));
|
||||
const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[];
|
||||
set({ overview: reordered });
|
||||
saveLocal(reordered);
|
||||
saveStoredOverview(reordered);
|
||||
},
|
||||
|
||||
migrateAndDeleteProduct: (sourceId, targetId) => {
|
||||
@@ -204,7 +194,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
.filter((p) => p.id !== sourceId)
|
||||
.map((p) => (p.id === targetId ? migratedTarget : p));
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
createProject: (productId, data) => {
|
||||
@@ -223,7 +213,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
createVersion: (productId, data) => {
|
||||
@@ -244,7 +234,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
updateVersion: (productId, versionId, data) => {
|
||||
@@ -258,7 +248,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
deleteVersion: (productId, versionId) => {
|
||||
@@ -271,7 +261,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
deleteProject: (productId, projectId) => {
|
||||
@@ -284,20 +274,17 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
saveLocal(updated);
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
}));
|
||||
|
||||
const STORAGE_KEY = 'ftb_products_overview_v4';
|
||||
|
||||
function saveLocal(data: ProductOverview[]) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
||||
function saveStoredOverview(data: ProductOverview[]) {
|
||||
saveServerData('products-overview', data).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): ProductOverview[] {
|
||||
async function loadStoredOverview(): Promise<ProductOverview[] | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<ProductOverview[]>('products-overview');
|
||||
} catch {}
|
||||
return MOCK_OVERVIEW;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Requirement, RequirementStatus, Effort, DictItem, SourceType, SourceTarget } from '@/lib/requirement';
|
||||
import type { Priority } from '@/lib/derive';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
interface RequirementState {
|
||||
requirements: Requirement[];
|
||||
@@ -10,7 +11,7 @@ interface RequirementState {
|
||||
types: DictItem[];
|
||||
platforms: DictItem[];
|
||||
loading: boolean;
|
||||
fetchRequirements: () => void;
|
||||
fetchRequirements: () => Promise<void>;
|
||||
createRequirement: (data: Omit<Requirement, 'id' | 'code' | 'createdAt'>) => void;
|
||||
updateRequirement: (id: string, data: Partial<Requirement>) => void;
|
||||
deleteRequirement: (id: string) => void;
|
||||
@@ -50,33 +51,17 @@ const PRESET_PLATFORMS: DictItem[] = [
|
||||
|
||||
// --- Mock requirements ---
|
||||
|
||||
const MOCK_REQUIREMENTS: Requirement[] = [
|
||||
{ id: 'req-1', code: 'REQ-001', title: '值班排班自动生成', description: '根据员工可用时间自动排班,支持轮换规则', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P1', effort: 'L', sourceType: 'internal', sourceTarget: '运营部', platforms: ['platform-1'], typeId: 'type-1', creator: '张三', createdAt: '2024-06-01' },
|
||||
{ id: 'req-2', code: 'REQ-002', title: '值班交接确认', description: '交接班时双方确认签到,记录交接内容', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P1', effort: 'M', sourceType: 'internal', sourceTarget: '门店经理', platforms: ['platform-1'], typeId: 'type-1', creator: '张三', createdAt: '2024-06-01' },
|
||||
{ id: 'req-3', code: 'REQ-003', title: '值班异常提醒', description: '迟到、缺勤自动推送提醒给店长', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P0', effort: 'M', sourceType: 'customer', sourceTarget: '连锁品牌A', platforms: ['platform-1', 'platform-2'], typeId: 'type-1', creator: '李四', createdAt: '2024-06-02' },
|
||||
{ id: 'req-4', code: 'REQ-004', title: '值班日历视图', description: '以日历形式展示当月值班安排', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P2', effort: 'L', sourceType: 'internal', sourceTarget: '产品部', platforms: ['platform-1'], typeId: 'type-2', creator: '张三', createdAt: '2024-06-03' },
|
||||
{ id: 'req-5', code: 'REQ-005', title: '值班换班申请', description: '员工可发起换班申请,经理审批', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P2', effort: 'M', sourceType: 'customer', sourceTarget: '连锁品牌B', platforms: ['platform-1'], typeId: 'type-1', creator: '李四', createdAt: '2024-06-03' },
|
||||
{ id: 'req-6', code: 'REQ-006', title: '值班统计报表', description: '按月/周统计各员工值班时长、缺勤次数', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P2', effort: 'L', sourceType: 'internal', sourceTarget: '运营部', platforms: ['platform-1'], typeId: 'type-3', creator: '张三', createdAt: '2024-06-04' },
|
||||
{ id: 'req-7', code: 'REQ-007', title: '多门店值班管理', description: '区域经理可查看管辖所有门店值班情况', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P1', effort: 'XL', sourceType: 'management', sourceTarget: '区域总监', platforms: ['platform-1', 'platform-3'], typeId: 'type-1', creator: '张三', createdAt: '2024-06-05' },
|
||||
{ id: 'req-8', code: 'REQ-008', title: '值班备注功能', description: '排班时可添加备注(如特殊事项提醒)', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P3', effort: 'S', sourceType: 'internal', sourceTarget: '产品部', platforms: ['platform-1'], typeId: 'type-2', creator: '李四', createdAt: '2024-06-05' },
|
||||
{ id: 'req-9', code: 'REQ-009', title: '法定节假日排班规则', description: '节假日自动应用特殊排班规则(加班费标记)', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P1', effort: 'M', sourceType: 'operation', sourceTarget: '合规部', platforms: ['platform-1'], typeId: 'type-1', creator: '张三', createdAt: '2024-06-06' },
|
||||
{ id: 'req-10', code: 'REQ-010', title: '值班模板管理', description: '创建值班模板供快速复用(如早中晚三班)', productId: 'mock-1', projectId: 'p3', status: 'pending_review', priority: 'P2', effort: 'M', sourceType: 'internal', sourceTarget: '产品部', platforms: ['platform-1'], typeId: 'type-1', creator: '张三', createdAt: '2024-06-06' },
|
||||
];
|
||||
const MOCK_REQUIREMENTS: Requirement[] = [];
|
||||
|
||||
// --- localStorage helpers ---
|
||||
// --- Server persistence helpers ---
|
||||
|
||||
const STORAGE_KEY = 'ftb_requirements_v3';
|
||||
|
||||
function saveLocal(state: { requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] }) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch {}
|
||||
function saveStored(state: { requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] }) {
|
||||
saveServerData('requirements', state).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): { requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] } | null {
|
||||
async function loadStored(): Promise<{ requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] } | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<{ requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] }>('requirements');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
@@ -91,8 +76,8 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
platforms: PRESET_PLATFORMS,
|
||||
loading: false,
|
||||
|
||||
fetchRequirements: () => {
|
||||
const cached = loadLocal();
|
||||
fetchRequirements: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) {
|
||||
set({ requirements: cached.requirements, sourceTargets: cached.sourceTargets, types: cached.types, platforms: cached.platforms });
|
||||
}
|
||||
@@ -110,21 +95,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
};
|
||||
const updated = [...requirements, newReq];
|
||||
set({ requirements: updated });
|
||||
saveLocal({ requirements: updated, sourceTargets, types, platforms });
|
||||
saveStored({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
updateRequirement: (id, data) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = requirements.map((r) => (r.id === id ? { ...r, ...data } : r));
|
||||
set({ requirements: updated });
|
||||
saveLocal({ requirements: updated, sourceTargets, types, platforms });
|
||||
saveStored({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
deleteRequirement: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = requirements.filter((r) => r.id !== id);
|
||||
set({ requirements: updated });
|
||||
saveLocal({ requirements: updated, sourceTargets, types, platforms });
|
||||
saveStored({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
// --- SourceTarget dict ---
|
||||
@@ -133,21 +118,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
const item: SourceTarget = { id: `st-${Date.now()}`, name, sourceType, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const updated = [...sourceTargets, item];
|
||||
set({ sourceTargets: updated });
|
||||
saveLocal({ requirements, sourceTargets: updated, types, platforms });
|
||||
saveStored({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
updateSourceTarget: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = sourceTargets.map((s) => (s.id === id ? { ...s, name } : s));
|
||||
set({ sourceTargets: updated });
|
||||
saveLocal({ requirements, sourceTargets: updated, types, platforms });
|
||||
saveStored({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
deleteSourceTarget: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = sourceTargets.filter((s) => s.id !== id);
|
||||
set({ sourceTargets: updated });
|
||||
saveLocal({ requirements, sourceTargets: updated, types, platforms });
|
||||
saveStored({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
// --- Type dict ---
|
||||
@@ -156,21 +141,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
const item: DictItem = { id: `type-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const updated = [...types, item];
|
||||
set({ types: updated });
|
||||
saveLocal({ requirements, sourceTargets, types: updated, platforms });
|
||||
saveStored({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
updateType: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = types.map((t) => (t.id === id ? { ...t, name } : t));
|
||||
set({ types: updated });
|
||||
saveLocal({ requirements, sourceTargets, types: updated, platforms });
|
||||
saveStored({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
deleteType: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = types.filter((t) => t.id !== id);
|
||||
set({ types: updated });
|
||||
saveLocal({ requirements, sourceTargets, types: updated, platforms });
|
||||
saveStored({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
// --- Platform dict ---
|
||||
@@ -179,21 +164,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
const item: DictItem = { id: `platform-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const updated = [...platforms, item];
|
||||
set({ platforms: updated });
|
||||
saveLocal({ requirements, sourceTargets, types, platforms: updated });
|
||||
saveStored({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
|
||||
updatePlatform: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = platforms.map((p) => (p.id === id ? { ...p, name } : p));
|
||||
set({ platforms: updated });
|
||||
saveLocal({ requirements, sourceTargets, types, platforms: updated });
|
||||
saveStored({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
|
||||
deletePlatform: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = platforms.filter((p) => p.id !== id);
|
||||
set({ platforms: updated });
|
||||
saveLocal({ requirements, sourceTargets, types, platforms: updated });
|
||||
saveStored({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { TaskCategory, CategoryGroup } from '@/lib/task-category';
|
||||
import { PRESET_CATEGORIES } from '@/lib/task-category';
|
||||
import { PRESET_CATEGORIES, normalizeTaskCategories } from '@/lib/task-category';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
const STORAGE_KEY = 'ftb_task_categories_v1';
|
||||
|
||||
function saveLocal(items: TaskCategory[]) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); } catch {}
|
||||
function saveStored(items: TaskCategory[]) {
|
||||
saveServerData('task-categories', items).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): TaskCategory[] | null {
|
||||
async function loadStored(): Promise<TaskCategory[] | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<TaskCategory[]>('task-categories');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface TaskCategoryState {
|
||||
categories: TaskCategory[];
|
||||
fetchCategories: () => void;
|
||||
fetchCategories: () => Promise<void>;
|
||||
addCategory: (name: string, group: CategoryGroup, color?: string) => void;
|
||||
updateCategory: (id: string, data: Partial<TaskCategory>) => void;
|
||||
deleteCategory: (id: string) => boolean;
|
||||
@@ -28,15 +26,16 @@ interface TaskCategoryState {
|
||||
export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
categories: PRESET_CATEGORIES,
|
||||
|
||||
fetchCategories: () => {
|
||||
const cached = loadLocal();
|
||||
if (cached) set({ categories: cached });
|
||||
fetchCategories: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ categories: normalizeTaskCategories(cached) });
|
||||
},
|
||||
|
||||
addCategory: (name, group, color) => {
|
||||
const list = get().categories;
|
||||
const item: TaskCategory = {
|
||||
id: `cat-${Date.now()}`,
|
||||
code: name.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '_').replace(/^_+|_+$/g, '') || `cat_${Date.now()}`,
|
||||
name,
|
||||
group,
|
||||
color,
|
||||
@@ -45,13 +44,13 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
};
|
||||
const updated = [...list, item];
|
||||
set({ categories: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
updateCategory: (id, data) => {
|
||||
const updated = get().categories.map((c) => (c.id === id ? { ...c, ...data } : c));
|
||||
set({ categories: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteCategory: (id) => {
|
||||
@@ -59,7 +58,7 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
if (!target || target.isSystem) return false;
|
||||
const updated = get().categories.filter((c) => c.id !== id);
|
||||
set({ categories: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { TaskWorklog } from '@/lib/task-worklog';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
const STORAGE_KEY = 'ftb_task_worklogs_v1';
|
||||
|
||||
function saveLocal(items: TaskWorklog[]) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); } catch {}
|
||||
function saveStored(items: TaskWorklog[]) {
|
||||
saveServerData('task-worklogs', items).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): TaskWorklog[] | null {
|
||||
async function loadStored(): Promise<TaskWorklog[] | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<TaskWorklog[]>('task-worklogs');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface TaskWorklogState {
|
||||
worklogs: TaskWorklog[];
|
||||
fetchWorklogs: () => void;
|
||||
fetchWorklogs: () => Promise<void>;
|
||||
addWorklog: (data: Omit<TaskWorklog, 'id' | 'createdAt'>) => void;
|
||||
deleteWorklog: (id: string) => void;
|
||||
getActualHours: (taskId: string) => number;
|
||||
@@ -27,8 +25,8 @@ interface TaskWorklogState {
|
||||
export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
|
||||
worklogs: [],
|
||||
|
||||
fetchWorklogs: () => {
|
||||
const cached = loadLocal();
|
||||
fetchWorklogs: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ worklogs: cached });
|
||||
},
|
||||
|
||||
@@ -40,13 +38,13 @@ export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
|
||||
};
|
||||
const updated = [...get().worklogs, item];
|
||||
set({ worklogs: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteWorklog: (id) => {
|
||||
const updated = get().worklogs.filter((w) => w.id !== id);
|
||||
set({ worklogs: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
getActualHours: (taskId) => {
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { VersionPlan, PlanType } from '@/lib/version-plan';
|
||||
|
||||
const STORAGE_KEY = 'ftb_version_plans_v1';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { getPlanCompletionState } from '@/lib/version-plan-workflow';
|
||||
|
||||
const MOCK_PLANS: VersionPlan[] = [];
|
||||
|
||||
function saveLocal(plans: VersionPlan[]) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(plans)); } catch {}
|
||||
function saveStored(plans: VersionPlan[]) {
|
||||
saveServerData('version-plans', plans).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): VersionPlan[] | null {
|
||||
async function loadStored(): Promise<VersionPlan[] | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<VersionPlan[]>('version-plans');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface VersionPlanState {
|
||||
plans: VersionPlan[];
|
||||
fetchPlans: () => void;
|
||||
fetchPlans: () => Promise<void>;
|
||||
createPlan: (data: Omit<VersionPlan, 'id' | 'createdAt'>) => void;
|
||||
updatePlan: (id: string, data: Partial<VersionPlan>) => void;
|
||||
completePlan: (id: string, result: { resultType: 'link' | 'file'; resultUrl?: string; resultFileName?: string; resultFileData?: string }) => void;
|
||||
completePlan: (id: string, result: { resultType: 'link' | 'file'; resultTitle: string; resultUrl?: string; resultFileName?: string; resultFileData?: string }) => { ok: boolean; message?: string };
|
||||
deletePlan: (id: string) => void;
|
||||
}
|
||||
|
||||
export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
plans: MOCK_PLANS,
|
||||
|
||||
fetchPlans: () => {
|
||||
const cached = loadLocal();
|
||||
fetchPlans: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ plans: cached });
|
||||
},
|
||||
|
||||
@@ -39,7 +38,7 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
const plan: VersionPlan = { ...data, id: `plan-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const plans = [...get().plans, plan];
|
||||
set({ plans });
|
||||
saveLocal(plans);
|
||||
saveStored(plans);
|
||||
},
|
||||
|
||||
updatePlan: (id, data) => {
|
||||
@@ -56,18 +55,30 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
return { ...p, ...patch };
|
||||
});
|
||||
set({ plans });
|
||||
saveLocal(plans);
|
||||
saveStored(plans);
|
||||
},
|
||||
|
||||
completePlan: (id, result) => {
|
||||
const plans = get().plans.map((p) => p.id === id ? { ...p, ...result, status: 'completed' as const, completedAt: new Date().toISOString() } : p);
|
||||
let response: { ok: boolean; message?: string } = { ok: false, message: '计划不存在' };
|
||||
const plans = get().plans.map((p) => {
|
||||
if (p.id !== id) return p;
|
||||
const next = { ...p, ...result, status: 'completed' as const, completedAt: new Date().toISOString() };
|
||||
const state = getPlanCompletionState(next);
|
||||
if (!state.canComplete) {
|
||||
response = { ok: false, message: state.missingReasons.join('、') || '计划未满足完成条件' };
|
||||
return p;
|
||||
}
|
||||
response = { ok: true };
|
||||
return next;
|
||||
});
|
||||
set({ plans });
|
||||
saveLocal(plans);
|
||||
saveStored(plans);
|
||||
return response;
|
||||
},
|
||||
|
||||
deletePlan: (id) => {
|
||||
const plans = get().plans.filter((p) => p.id !== id);
|
||||
set({ plans });
|
||||
saveLocal(plans);
|
||||
saveStored(plans);
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user