Files
jiapuapp/components/genealogy/RegionPickerDialog.vue
T

209 lines
6.3 KiB
Vue

<template>
<RegionPickerSheet
:visible="visible"
:columns="columns"
:indexes="indexes"
:indicator-style="indicatorStyle"
:close-on-mask="closeOnMask"
@change="changeSelection"
@cancel="close"
@confirm="confirmSelection"
/>
</template>
<script setup>
import { onUnmounted, ref } from "vue";
import RegionPickerSheet from "@/components/genealogy/RegionPickerSheet.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { regionApi } from "@/services/api/region-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
const props = defineProps({
closeOnMask: {
type: Boolean,
default: false,
},
maxLevels: {
type: Number,
default: 5,
validator: (levelCount) => Number.isInteger(levelCount) && levelCount > 0,
},
});
const emit = defineEmits([
"error-change",
"loading-change",
"select",
"transient-change",
]);
const indicatorStyle =
"height: 104rpx; border-top: 1px solid rgba(159, 23, 15, .46); border-bottom: 1px solid rgba(159, 23, 15, .46); background: rgba(159, 23, 15, .08);";
const visible = ref(false);
const columns = ref([]);
const indexes = ref([0]);
const selectedTrail = ref([]);
const preparedRegionCode = ref("");
const regionRequestController = createRequestController();
let componentActive = true;
let loading = false;
const setLoading = (nextLoading) => {
loading = nextLoading;
emit("loading-change", nextLoading);
};
const setError = (message = "") => {
emit("error-change", message);
};
const fetchRegionChildren = (parentCode) =>
regionApi.getRegionChildren(parentCode, {
requestController: regionRequestController,
});
const loadColumns = async (requestedIndexes = []) => {
const rootOptions = columns.value[0] || (await fetchRegionChildren("0"));
if (!rootOptions.length) return false;
const nextColumns = [rootOptions];
const nextIndexes = [];
let options = rootOptions;
for (let level = 0; level < props.maxLevels; level += 1) {
const requestedIndex = Number(requestedIndexes[level]);
const selectedIndex = Number.isInteger(requestedIndex)
? Math.min(Math.max(requestedIndex, 0), options.length - 1)
: 0;
const selectedOption = options[selectedIndex];
if (!selectedOption) break;
nextIndexes.push(selectedIndex);
if (level === props.maxLevels - 1) break;
const childOptions = await fetchRegionChildren(selectedOption.regionCode);
if (!childOptions.length) break;
nextColumns.push(childOptions);
options = childOptions;
}
if (!componentActive) return false;
columns.value = nextColumns;
indexes.value = nextIndexes;
selectedTrail.value = nextColumns
.map((column, level) => column[nextIndexes[level]])
.filter(Boolean);
return Boolean(selectedTrail.value.length);
};
const loadRegionPath = async (regionCode) => {
const regionPath = (await regionApi.getRegionPath(regionCode, {
requestController: regionRequestController,
})).filter((regionNode) => regionNode?.regionCode);
if (
!regionPath.length ||
regionPath[regionPath.length - 1].regionCode !== regionCode
) {
throw new Error("REGION_PATH_MISMATCH");
}
const nextColumns = [];
const nextIndexes = [];
const nextTrail = [];
let parentCode = "0";
for (const regionPathNode of regionPath.slice(0, props.maxLevels)) {
const options = await fetchRegionChildren(parentCode);
const selectedIndex = options.findIndex(
(option) => option.regionCode === regionPathNode.regionCode,
);
if (selectedIndex < 0) throw new Error("REGION_PATH_MISMATCH");
nextColumns.push(options);
nextIndexes.push(selectedIndex);
nextTrail.push(options[selectedIndex]);
parentCode = regionPathNode.regionCode;
}
if (!componentActive) return false;
columns.value = nextColumns;
indexes.value = nextIndexes;
selectedTrail.value = nextTrail;
return Boolean(nextTrail.length);
};
const prepare = async (initialRegionCode = "") => {
const normalizedRegionCode = String(initialRegionCode || "");
if (loading) return false;
if (
columns.value.length &&
preparedRegionCode.value === normalizedRegionCode
) {
return true;
}
regionRequestController.abort();
setLoading(true);
setError();
try {
const ready = normalizedRegionCode
? await loadRegionPath(normalizedRegionCode)
: await loadColumns([0]);
if (!componentActive || !ready) return false;
preparedRegionCode.value = normalizedRegionCode;
return true;
} catch (error) {
if (!componentActive || isRequestCancelled(error)) return false;
const fallback = normalizedRegionCode
? "暂时无法定位当前地区,请稍后重试。"
: "地区列表加载失败,请重试";
setError(getRequestErrorMessage(error, fallback));
return false;
} finally {
if (componentActive) setLoading(false);
}
};
const open = async (initialRegionCode = "") => {
if (!(await prepare(initialRegionCode))) return false;
visible.value = true;
emit("transient-change", true);
return true;
};
const close = () => {
visible.value = false;
emit("transient-change", false);
};
const changeSelection = async (event) => {
if (loading) return;
const requestedIndexes = (event?.detail?.value || []).map(
(index) => Number(index) || 0,
);
const changedLevel = requestedIndexes.findIndex(
(index, level) => index !== (indexes.value[level] || 0),
);
if (changedLevel < 0) return;
setLoading(true);
setError();
try {
const loaded = await loadColumns(requestedIndexes.slice(0, changedLevel + 1));
if (!componentActive || !loaded) return;
preparedRegionCode.value = "";
} catch (error) {
if (!componentActive || isRequestCancelled(error)) return;
setError(getRequestErrorMessage(error, "地区列表加载失败,请重试"));
} finally {
if (componentActive) setLoading(false);
}
};
const confirmSelection = () => {
const trail = columns.value
.map((column, level) => column[Number(indexes.value[level])])
.filter(Boolean);
const region = trail[trail.length - 1];
if (!region) return;
selectedTrail.value = trail;
preparedRegionCode.value = region.regionCode;
emit("select", { region, trail });
close();
};
defineExpose({ close, open, prepare });
onUnmounted(() => {
componentActive = false;
regionRequestController.abort();
});
</script>