218 lines
6.5 KiB
JavaScript
218 lines
6.5 KiB
JavaScript
import { useMemo, useState } from "react";
|
|
import { parseForm } from "@/shared/forms/validation";
|
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
|
import {
|
|
createTaskDefinition,
|
|
listTaskDefinitions,
|
|
setTaskDefinitionStatus,
|
|
updateTaskDefinition,
|
|
} from "@/features/daily-tasks/api";
|
|
import { useDailyTaskAbilities } from "@/features/daily-tasks/permissions.js";
|
|
import { dailyTaskFormSchema } from "@/features/daily-tasks/schema";
|
|
|
|
const pageSize = 50;
|
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
|
const emptyForm = (task = {}) => ({
|
|
category: task.category || "",
|
|
description: task.description || "",
|
|
effectiveFrom: msToDatetimeLocal(task.effectiveFromMs),
|
|
effectiveTo: msToDatetimeLocal(task.effectiveToMs),
|
|
metricType: task.metricType || "",
|
|
rewardCoinAmount: task.rewardCoinAmount === 0 || task.rewardCoinAmount ? String(task.rewardCoinAmount) : "",
|
|
sortOrder: task.sortOrder === 0 || task.sortOrder ? String(task.sortOrder) : "0",
|
|
status: task.status || "draft",
|
|
targetUnit: task.targetUnit || "count",
|
|
targetValue: task.targetValue === 0 || task.targetValue ? String(task.targetValue) : "",
|
|
taskType: task.taskType || "daily",
|
|
title: task.title || "",
|
|
});
|
|
|
|
export function useDailyTasksPage() {
|
|
const abilities = useDailyTaskAbilities();
|
|
const { showToast } = useToast();
|
|
const [query, setQuery] = useState("");
|
|
const [status, setStatus] = useState("");
|
|
const [taskType, setTaskType] = useState("");
|
|
const [category, setCategory] = useState("");
|
|
const [page, setPage] = useState(1);
|
|
const [activeAction, setActiveAction] = useState("");
|
|
const [selectedTask, setSelectedTask] = useState(null);
|
|
const [form, setForm] = useState(emptyForm);
|
|
const [loadingAction, setLoadingAction] = useState("");
|
|
const filters = useMemo(
|
|
() => ({
|
|
category,
|
|
keyword: query,
|
|
status,
|
|
task_type: taskType,
|
|
}),
|
|
[category, query, status, taskType],
|
|
);
|
|
const {
|
|
data = emptyData,
|
|
error,
|
|
loading,
|
|
reload,
|
|
} = usePaginatedQuery({
|
|
errorMessage: "加载每日任务失败",
|
|
fetcher: listTaskDefinitions,
|
|
filters,
|
|
page,
|
|
pageSize,
|
|
queryKey: ["daily-tasks", filters, page],
|
|
});
|
|
|
|
const changeQuery = resetSetter(setQuery, setPage);
|
|
const changeStatus = resetSetter(setStatus, setPage);
|
|
const changeTaskType = resetSetter(setTaskType, setPage);
|
|
const changeCategory = resetSetter(setCategory, setPage);
|
|
|
|
const resetFilters = () => {
|
|
setQuery("");
|
|
setStatus("");
|
|
setTaskType("");
|
|
setCategory("");
|
|
setPage(1);
|
|
};
|
|
|
|
const openCreate = () => {
|
|
setSelectedTask(null);
|
|
setForm(emptyForm());
|
|
setActiveAction("create");
|
|
};
|
|
|
|
const openEdit = (task) => {
|
|
if (!task?.taskId) {
|
|
return;
|
|
}
|
|
setSelectedTask(task);
|
|
setForm(emptyForm(task));
|
|
setActiveAction("edit");
|
|
};
|
|
|
|
const closeAction = () => {
|
|
setActiveAction("");
|
|
setSelectedTask(null);
|
|
};
|
|
|
|
const submitTask = async (event) => {
|
|
event.preventDefault();
|
|
const editing = activeAction === "edit";
|
|
if (editing && !selectedTask?.taskId) {
|
|
return;
|
|
}
|
|
await runAction(editing ? "edit" : "create", editing ? "任务已更新" : "任务已创建", async () => {
|
|
const payload = buildTaskPayload(parseForm(dailyTaskFormSchema, form));
|
|
if (editing) {
|
|
await updateTaskDefinition(selectedTask.taskId, payload);
|
|
} else {
|
|
await createTaskDefinition(payload);
|
|
}
|
|
closeAction();
|
|
setForm(emptyForm());
|
|
await reload();
|
|
});
|
|
};
|
|
|
|
const toggleTask = async (task, nextEnabled = task.status !== "active") => {
|
|
if (!abilities.canStatus || !task?.taskId || task.status === "archived") {
|
|
return;
|
|
}
|
|
const nextStatus = nextEnabled ? "active" : "paused";
|
|
if (task.status === nextStatus) {
|
|
return;
|
|
}
|
|
await runAction(`status-${task.taskId}`, nextEnabled ? "任务已启用" : "任务已暂停", async () => {
|
|
await setTaskDefinitionStatus(task.taskId, nextStatus);
|
|
await reload();
|
|
});
|
|
};
|
|
|
|
const runAction = async (action, successMessage, submitter) => {
|
|
setLoadingAction(action);
|
|
try {
|
|
await submitter();
|
|
showToast(successMessage, "success");
|
|
} catch (err) {
|
|
showToast(err.message || "操作失败", "error");
|
|
} finally {
|
|
setLoadingAction("");
|
|
}
|
|
};
|
|
|
|
return {
|
|
abilities,
|
|
activeAction,
|
|
category,
|
|
changeCategory,
|
|
changeQuery,
|
|
changeStatus,
|
|
changeTaskType,
|
|
closeAction,
|
|
data,
|
|
error,
|
|
form,
|
|
loading,
|
|
loadingAction,
|
|
openCreate,
|
|
openEdit,
|
|
page,
|
|
query,
|
|
reload,
|
|
resetFilters,
|
|
selectedTask,
|
|
setForm,
|
|
setPage,
|
|
status,
|
|
submitTask,
|
|
taskType,
|
|
toggleTask,
|
|
};
|
|
}
|
|
|
|
function buildTaskPayload(form) {
|
|
return {
|
|
category: form.category.trim(),
|
|
description: form.description.trim(),
|
|
effective_from_ms: datetimeLocalToMs(form.effectiveFrom),
|
|
effective_to_ms: datetimeLocalToMs(form.effectiveTo),
|
|
metric_type: form.metricType.trim(),
|
|
reward_coin_amount: Number(form.rewardCoinAmount),
|
|
sort_order: Number(form.sortOrder || 0),
|
|
status: form.status,
|
|
target_unit: form.targetUnit,
|
|
target_value: Number(form.targetValue),
|
|
task_type: form.taskType,
|
|
title: form.title.trim(),
|
|
};
|
|
}
|
|
|
|
function resetSetter(setter, setPage) {
|
|
return (value) => {
|
|
setter(value);
|
|
setPage(1);
|
|
};
|
|
}
|
|
|
|
function msToDatetimeLocal(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
const date = new Date(Number(value));
|
|
if (Number.isNaN(date.getTime())) {
|
|
return "";
|
|
}
|
|
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
|
return local.toISOString().slice(0, 16);
|
|
}
|
|
|
|
function datetimeLocalToMs(value) {
|
|
const trimmed = String(value || "").trim();
|
|
if (!trimmed) {
|
|
return 0;
|
|
}
|
|
const parsed = new Date(trimmed).getTime();
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|