241 lines
7.9 KiB
JavaScript
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));
|
|
});
|
|
}
|