hyapp-admin-platform/src/features/host-org/hooks/useHostCountriesPage.js
2026-07-09 13:02:40 +08:00

241 lines
7.9 KiB
JavaScript

import { useCallback, useMemo, useState } from "react";
import { parseForm } from "@/shared/forms/validation";
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import {
createCountry,
disableCountry,
enableCountry,
getJob,
listCountries,
renameCountryCode,
updateCountry,
} from "@/features/host-org/api";
import { useCountryAbilities } from "@/features/host-org/permissions.js";
import { countryCreateSchema, countryRenameCodeSchema, countryUpdateSchema } from "@/features/host-org/schema";
const emptyCountryForm = () => ({
countryCode: "",
countryDisplayName: "",
countryName: "",
enabled: true,
flag: "",
isoAlpha3: "",
isoNumeric: "",
phoneCountryCode: "",
sortOrder: 0,
});
const emptyData = { items: [], total: 0 };
export function useHostCountriesPage() {
const abilities = useCountryAbilities();
const { showToast } = useToast();
const [query, setQuery] = useState("");
const [enabledStatus, setEnabledStatus] = useState("");
const [countryForm, setCountryForm] = useState(emptyCountryForm);
const [editingCountry, setEditingCountry] = useState(null);
const [loadingAction, setLoadingAction] = useState("");
const [activeAction, setActiveAction] = useState("");
const filters = useMemo(
() => ({
enabled: enabledStatus === "enabled" ? true : enabledStatus === "disabled" ? false : undefined,
}),
[enabledStatus],
);
const queryFn = useCallback(() => listCountries(filters), [filters]);
const {
data = emptyData,
error,
loading,
reload,
} = useAdminQuery(queryFn, {
errorMessage: "加载国家列表失败",
initialData: emptyData,
keepPreviousData: true,
queryKey: ["host-org", "countries", filters],
});
const items = useMemo(() => filterCountries(data?.items || [], query), [data?.items, query]);
const changeQuery = (value) => {
setQuery(value);
};
const changeEnabledStatus = (value) => {
setEnabledStatus(value);
};
const resetFilters = () => {
setQuery("");
setEnabledStatus("");
};
const openCreateCountry = () => {
setEditingCountry(null);
setCountryForm(emptyCountryForm());
setActiveAction("create");
};
const openEditCountry = (country) => {
setEditingCountry(country);
setCountryForm({
countryCode: country.countryCode || "",
countryDisplayName: country.countryDisplayName || "",
countryName: country.countryName || "",
enabled: Boolean(country.enabled),
flag: country.flag || "",
isoAlpha3: country.isoAlpha3 || "",
isoNumeric: country.isoNumeric || "",
phoneCountryCode: country.phoneCountryCode || "",
sortOrder: country.sortOrder || 0,
});
setActiveAction("edit");
};
const submitCountry = async (event) => {
event.preventDefault();
const isEdit = activeAction === "edit";
if (!isEdit) {
await runAction("country-create", "国家已创建", async () => {
const payload = parseForm(countryCreateSchema, countryForm);
const data = await createCountry(payload);
setCountryForm(emptyCountryForm());
setEditingCountry(null);
setActiveAction("");
await reload();
return data;
});
return;
}
const currentCountryCode = normalizeCountryCode(editingCountry?.countryCode);
const nextCountryCode = normalizeCountryCode(countryForm.countryCode);
if (currentCountryCode !== nextCountryCode) {
await runCountryCodeRename();
return;
}
await runAction("country-edit", "国家已更新", async () => {
const { countryCode: _countryCode, enabled: _enabled, ...formWithoutImmutableFields } = countryForm;
const payload = parseForm(countryUpdateSchema, formWithoutImmutableFields);
const data = await updateCountry(editingCountry.countryId, payload);
setCountryForm(emptyCountryForm());
setEditingCountry(null);
setActiveAction("");
await reload();
return data;
});
};
const runCountryCodeRename = async () => {
setLoadingAction("country-rename");
try {
const payload = parseForm(countryRenameCodeSchema, countryForm);
const job = await renameCountryCode(editingCountry.countryId, payload);
showToast(`国家码重命名任务已创建:#${job.jobId}`, "success");
setCountryForm(emptyCountryForm());
setEditingCountry(null);
setActiveAction("");
// 国家码会同步 user/admin/room 多个当前态,只有 job succeeded 后才刷新列表,避免短暂显示新旧码混杂。
await waitCountryCodeRenameJob(job.jobId);
await reload();
showToast("国家码重命名完成", "success");
} catch (err) {
showToast(err.message || "国家码重命名失败", "error");
} finally {
setLoadingAction("");
}
};
const toggleCountry = async (country, nextEnabled = !country.enabled) => {
if (!abilities.canStatus || Boolean(country.enabled) === nextEnabled) {
return;
}
await runAction(`country-status-${country.countryId}`, nextEnabled ? "国家已启用" : "国家已禁用", async () => {
const data = nextEnabled ? await enableCountry(country.countryId) : await disableCountry(country.countryId);
await reload();
return data;
});
};
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,
changeEnabledStatus,
changeQuery,
closeAction: () => setActiveAction(""),
countryForm,
data: { ...data, items, total: items.length },
editingCountry,
enabledStatus,
error,
loading,
loadingAction,
openCreateCountry,
openEditCountry,
query,
reload,
resetFilters,
setCountryForm,
submitCountry,
toggleCountry,
};
}
async function waitCountryCodeRenameJob(jobId) {
const deadline = Date.now() + 120000;
while (Date.now() < deadline) {
const job = await getJob(jobId);
if (job.status === "succeeded") {
return job;
}
if (job.status === "failed" || job.status === "canceled") {
throw new Error(job.error || "国家码重命名任务失败");
}
await delay(1500);
}
throw new Error("国家码重命名任务仍在处理中,请稍后刷新查看");
}
function delay(ms) {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
function normalizeCountryCode(value) {
return String(value || "")
.trim()
.toUpperCase();
}
function filterCountries(items, query) {
const keyword = query.trim().toLowerCase();
if (!keyword) {
return items;
}
return items.filter((item) => {
return [
item.countryCode,
item.countryDisplayName,
item.countryName,
item.isoAlpha3,
item.isoNumeric,
item.phoneCountryCode,
]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword));
});
}