209 lines
6.3 KiB
Vue
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>
|