20260706170339

This commit is contained in:
oneao committed 2026-07-06 17:03:39 +08:00
1 parent ae99f50f6d
commit 4d1e6c8b8f
50 files changed
+3045 -2249

No files matched your search

+277
View File
@@ -0,0 +1,277 @@
import { create } from "zustand";
import { loadDataApi, saveDataApi, getUniqueIdApi, s3UploadFileApi } from "@/request/api";
import { scheduleTodoReminder, cancelTodoReminders, requestNotificationPermission } from "@/helpers/notification";
import {
Todo,
TodoFile,
TodoLog,
TodoLogType,
TodoRating,
TodoRepeatType,
TodoSaveChanges,
TodoStatus,
TodoFamilyMember,
} from "@/types/todo";
const TABLES = {
todo: "b_todo",
todoLog: "b_todo_log",
todoRating: "b_todo_rating",
todoFile: "b_todo_file",
} as const;
interface TodoState {
todos: Todo[];
todoLogs: TodoLog[];
todoRatings: TodoRating[];
todoFiles: TodoFile[];
members: TodoFamilyMember[];
clear: () => void;
fetchAll: (familyId: number) => Promise<void>;
fetchTodos: (familyId: number) => Promise<void>;
fetchTodoLogs: (familyId: number, todoId?: number) => Promise<void>;
fetchTodoRatings: (familyId: number, todoId?: number) => Promise<void>;
fetchTodoFiles: (familyId: number, todoId?: number) => Promise<void>;
fetchMembers: (familyId: number) => Promise<void>;
saveTodoChanges: (changes: TodoSaveChanges<Todo>) => Promise<boolean>;
saveTodoLogChanges: (changes: TodoSaveChanges<TodoLog>) => Promise<boolean>;
saveTodoRatingChanges: (changes: TodoSaveChanges<TodoRating>) => Promise<boolean>;
saveTodoFileChanges: (changes: TodoSaveChanges<TodoFile>) => Promise<boolean>;
uploadFile: (familyId: number, file: any) => Promise<string | null>;
createTodo: (params: {
familyId: number;
userId: number;
name: string;
description?: string;
priority: number;
assigneeId?: number | null;
deadline?: string | null;
repeatType?: number;
remindAt?: string | null;
}) => Promise<number | null>;
claimTodo: (params: { todoId: number; familyId: number; userId: number }) => Promise<boolean>;
rejectTodo: (params: { todoId: number; familyId: number; userId: number }) => Promise<boolean>;
transferTodo: (params: { todoId: number; familyId: number; userId: number; newAssigneeId: number | null }) => Promise<boolean>;
completeTodo: (params: { todoId: number; familyId: number; userId: number; completeNote?: string; fileInserts?: Partial<TodoFile>[] }) => Promise<boolean>;
rateTodo: (params: { todoId: number; familyId: number; userId: number; rating: number; content?: string }) => Promise<boolean>;
}
function normalizeChanges<T>(changes: TodoSaveChanges<T>) {
return { inserts: changes.inserts ?? [], updates: changes.updates ?? [], deletes: changes.deletes ?? [] };
}
function nextDeadline(now: Date, repeatType: TodoRepeatType, oldDeadline?: string | null): string | null {
if (repeatType === TodoRepeatType.None || !oldDeadline) return null;
const d = new Date(oldDeadline);
switch (repeatType) {
case TodoRepeatType.Daily: d.setDate(d.getDate() + 1); break;
case TodoRepeatType.Weekly: d.setDate(d.getDate() + 7); break;
case TodoRepeatType.Monthly: d.setMonth(d.getMonth() + 1); break;
}
return d.toISOString();
}
export const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
todoLogs: [],
todoRatings: [],
todoFiles: [],
members: [],
clear: () => set({ todos: [], todoLogs: [], todoRatings: [], todoFiles: [], members: [] }),
fetchAll: async (familyId: number) => {
if (!familyId) return;
await Promise.all([
get().fetchTodos(familyId), get().fetchTodoLogs(familyId),
get().fetchTodoRatings(familyId), get().fetchTodoFiles(familyId), get().fetchMembers(familyId),
]);
},
fetchTodos: async (familyId: number) => {
if (!familyId) return;
const res = await loadDataApi({ view_name: TABLES.todo, search_condition: "family_id = ?", search_columns: [], order_by: "sort_number asc, id asc", args: [familyId] });
if (res.isSuccess) set({ todos: res.data as Todo[] });
},
fetchTodoLogs: async (familyId: number, todoId?: number) => {
if (!familyId) return;
const conditions = ["family_id = ?"]; const args: any[] = [familyId];
if (todoId !== undefined) { conditions.push("todo_id = ?"); args.push(todoId); }
const res = await loadDataApi({ view_name: TABLES.todoLog, search_condition: conditions.join(" and "), search_columns: [], order_by: "create_time asc, id asc", args });
if (res.isSuccess) set({ todoLogs: res.data as TodoLog[] });
},
fetchTodoRatings: async (familyId: number, todoId?: number) => {
if (!familyId) return;
const conditions = ["family_id = ?"]; const args: any[] = [familyId];
if (todoId !== undefined) { conditions.push("todo_id = ?"); args.push(todoId); }
const res = await loadDataApi({ view_name: TABLES.todoRating, search_condition: conditions.join(" and "), search_columns: [], order_by: "id asc", args });
if (res.isSuccess) set({ todoRatings: res.data as TodoRating[] });
},
fetchTodoFiles: async (familyId: number, todoId?: number) => {
if (!familyId) return;
const conditions = ["family_id = ?"]; const args: any[] = [familyId];
if (todoId !== undefined) { conditions.push("todo_id = ?"); args.push(todoId); }
const res = await loadDataApi({ view_name: TABLES.todoFile, search_condition: conditions.join(" and "), search_columns: [], order_by: "sort_order asc, id asc", args });
if (res.isSuccess) set({ todoFiles: res.data as TodoFile[] });
},
fetchMembers: async (familyId: number) => {
if (!familyId) return;
const res = await loadDataApi({ view_name: "v_family_member", search_condition: "family_id = ?", search_columns: ["user_id", "nickname", "avatar"], order_by: "id asc", args: [familyId] });
if (res.isSuccess) set({ members: (res.data as TodoFamilyMember[]) ?? [] });
},
saveTodoChanges: async (changes: TodoSaveChanges<Todo>) => {
const res = await saveDataApi([{ table_name: TABLES.todo, key_field: "id", ...normalizeChanges(changes) }]);
return res.isSuccess;
},
saveTodoLogChanges: async (changes: TodoSaveChanges<TodoLog>) => {
const res = await saveDataApi([{ table_name: TABLES.todoLog, key_field: "id", ...normalizeChanges(changes) }]);
return res.isSuccess;
},
saveTodoRatingChanges: async (changes: TodoSaveChanges<TodoRating>) => {
const res = await saveDataApi([{ table_name: TABLES.todoRating, key_field: "id", ...normalizeChanges(changes) }]);
return res.isSuccess;
},
saveTodoFileChanges: async (changes: TodoSaveChanges<TodoFile>) => {
const res = await saveDataApi([{ table_name: TABLES.todoFile, key_field: "id", ...normalizeChanges(changes) }]);
return res.isSuccess;
},
uploadFile: async (familyId: number, file: any) => {
if (!familyId) return null;
const res = await s3UploadFileApi([file], familyId);
if (!res.isSuccess) return null;
return res.data?.[0] ?? null;
},
createTodo: async ({ familyId, userId, name, description, priority, assigneeId, deadline, repeatType, remindAt }) => {
if (!familyId) return null;
const [idRes, logIdRes] = await Promise.all([getUniqueIdApi(1), getUniqueIdApi(1)]);
const todoId = idRes.data?.[0] ?? 0; const logId = logIdRes.data?.[0] ?? 0;
if (!todoId) return null;
const now = new Date().toISOString();
const ops: any[] = [{
table_name: TABLES.todo, key_field: "id",
inserts: [{
id: todoId, family_id: familyId, name, description: description ?? "", priority,
assignee_id: assigneeId ?? null, deadline: deadline ?? null, repeat_type: repeatType ?? 0,
remind_at: remindAt ?? null, status: assigneeId ? TodoStatus.InProgress : TodoStatus.Unclaimed,
create_time: now, create_by: userId,
}],
updates: [], deletes: [],
}];
if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [{ id: logId, family_id: familyId, todo_id: todoId, type: assigneeId ? TodoLogType.Assign : TodoLogType.Create, content: assigneeId ? String(assigneeId) : name, create_by: userId, create_time: now }], updates: [], deletes: [] });
const res = await saveDataApi(ops);
if (res.isSuccess) {
set((s) => ({ todos: [...s.todos, { id: todoId, family_id: familyId, name, description: description ?? "", status: assigneeId ? TodoStatus.InProgress : TodoStatus.Unclaimed, priority, repeat_type: repeatType ?? 0, remind_at: remindAt ?? null, assignee_id: assigneeId ?? null, deadline: deadline ?? null, sort_number: s.todos.length, create_time: now, create_by: userId } as Todo] }));
// 预定提醒通知
if (remindAt) {
requestNotificationPermission().then((ok) => { if (ok) scheduleTodoReminder(todoId, name, remindAt); });
}
return todoId;
}
return null;
},
claimTodo: async ({ todoId, familyId, userId }) => {
if (!familyId) return false;
const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString();
const ops: any[] = [{ table_name: TABLES.todo, key_field: "id", inserts: [], updates: [{ id: todoId, status: TodoStatus.InProgress, assignee_id: userId, update_time: now, update_by: userId }], deletes: [] }];
if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [{ id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Claim, create_by: userId, create_time: now }], updates: [], deletes: [] });
const res = await saveDataApi(ops);
if (res.isSuccess) set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: TodoStatus.InProgress, assignee_id: userId } : t) }));
return res.isSuccess;
},
rejectTodo: async ({ todoId, familyId, userId }) => {
if (!familyId) return false;
const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString();
const ops: any[] = [{ table_name: TABLES.todo, key_field: "id", inserts: [], updates: [{ id: todoId, status: TodoStatus.Unclaimed, assignee_id: null, update_time: now, update_by: userId }], deletes: [] }];
if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [{ id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Reject, create_by: userId, create_time: now }], updates: [], deletes: [] });
const res = await saveDataApi(ops);
if (res.isSuccess) set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: TodoStatus.Unclaimed, assignee_id: null } : t) }));
return res.isSuccess;
},
transferTodo: async ({ todoId, familyId, userId, newAssigneeId }) => {
if (!familyId) return false;
const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; const now = new Date().toISOString();
const newStatus = newAssigneeId ? TodoStatus.InProgress : TodoStatus.Unclaimed;
const ops: any[] = [{ table_name: TABLES.todo, key_field: "id", inserts: [], updates: [{ id: todoId, status: newStatus, assignee_id: newAssigneeId, update_time: now, update_by: userId }], deletes: [] }];
if (logId) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [{ id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Transfer, content: String(newAssigneeId ?? ""), create_by: userId, create_time: now }], updates: [], deletes: [] });
const res = await saveDataApi(ops);
if (res.isSuccess) set((s) => ({ todos: s.todos.map((t) => t.id === todoId ? { ...t, status: newStatus, assignee_id: newAssigneeId } : t) }));
return res.isSuccess;
},
completeTodo: async ({ todoId, familyId, userId, completeNote, fileInserts }) => {
if (!familyId) return false;
const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0;
const now = new Date().toISOString();
const ops: any[] = [{
table_name: TABLES.todo, key_field: "id",
inserts: [], updates: [{ id: todoId, status: TodoStatus.Completed, completed_at: now, complete_note: completeNote ?? "", update_time: now, update_by: userId }], deletes: [],
}];
if (logId) {
ops.push({
table_name: TABLES.todoLog, key_field: "id",
inserts: [{ id: logId, family_id: familyId, todo_id: todoId, type: TodoLogType.Complete, content: completeNote ?? "", create_by: userId, create_time: now }],
updates: [], deletes: [],
});
}
// 附件
if (fileInserts && fileInserts.length > 0) {
ops.push({
table_name: TABLES.todoFile, key_field: "id",
inserts: fileInserts.map((f) => ({ ...f, todo_log_id: logId, todo_id: todoId, create_by: userId, create_time: now })),
updates: [], deletes: [],
});
}
const res = await saveDataApi(ops);
if (res.isSuccess) {
set((s) => ({
todos: s.todos.map((t) => t.id === todoId ? { ...t, status: TodoStatus.Completed, completed_at: now, complete_note: completeNote ?? "" } : t),
}));
// 取消提醒通知
cancelTodoReminders(todoId);
// 重复任务:自动创建下一个实例
const todo = get().todos.find((t) => t.id === todoId);
if (todo && todo.repeat_type !== TodoRepeatType.None) {
const nextDl = nextDeadline(new Date(now), todo.repeat_type, todo.deadline);
const nextRemind = todo.remind_at ? nextDeadline(new Date(now), todo.repeat_type, todo.remind_at) : null;
get().createTodo({
familyId, userId, name: todo.name, description: todo.description,
priority: todo.priority, assigneeId: todo.assignee_id,
deadline: nextDl, repeatType: todo.repeat_type, remindAt: nextRemind,
});
}
}
return res.isSuccess;
},
rateTodo: async ({ todoId, familyId, userId, rating, content }) => {
if (!familyId) return false;
const [rateId, logId] = await Promise.all([getUniqueIdApi(1), getUniqueIdApi(1)]);
const rid = rateId.data?.[0] ?? 0; const lid = logId.data?.[0] ?? 0;
if (!rid) return false;
const now = new Date().toISOString();
const ops: any[] = [{ table_name: TABLES.todoRating, key_field: "id", inserts: [{ id: rid, family_id: familyId, todo_id: todoId, rater_id: userId, rating, content: content ?? "", create_by: userId, create_time: now }], updates: [], deletes: [] }];
if (lid) ops.push({ table_name: TABLES.todoLog, key_field: "id", inserts: [{ id: lid, family_id: familyId, todo_id: todoId, type: TodoLogType.Rate, ref_id: rid, create_by: userId, create_time: now }], updates: [], deletes: [] });
const res = await saveDataApi(ops);
if (res.isSuccess) set((s) => ({ todoRatings: [...s.todoRatings, { id: rid, family_id: familyId, todo_id: todoId, rater_id: userId, rating, content: content ?? "", create_by: userId, create_time: now } as TodoRating] }));
return res.isSuccess;
},
}));