feat: 地区选择器改走接口、订单市价重算与资金流水、同步日志内容复用
region-address-picker 接 region-picker API;订单支持按进价重算 market_price、追踪页资金流水;MES/ERP/中台同步日志共用内容查看组件;地区管理支持 pid 下钻与拼音检索。
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import { addressOption } from '#/util/address';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
getRegionChildren,
|
||||
resolveRegionPair,
|
||||
searchRegions,
|
||||
type RegionPickerChild,
|
||||
type RegionPickerSearchHit,
|
||||
} from '#/api/region-picker';
|
||||
|
||||
export interface AddressNode {
|
||||
value: number;
|
||||
label: string;
|
||||
pvalue?: number;
|
||||
}
|
||||
|
||||
export interface ProvinceIndex {
|
||||
province: AddressNode;
|
||||
children: AddressNode[];
|
||||
isMunicipality: boolean;
|
||||
hasChildren?: boolean;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface ResolvedAddress {
|
||||
@@ -19,6 +23,7 @@ export interface ResolvedAddress {
|
||||
province?: AddressNode;
|
||||
city?: AddressNode;
|
||||
district?: AddressNode;
|
||||
pathLabel?: string;
|
||||
}
|
||||
|
||||
export type AddressValue = [number, number];
|
||||
@@ -37,15 +42,14 @@ export interface SelectionContext {
|
||||
districtId?: number;
|
||||
}
|
||||
|
||||
/** 跨级搜索结果 */
|
||||
export interface AddressSearchHit {
|
||||
level: RegionLevel;
|
||||
node: AddressNode;
|
||||
pathLabel: string;
|
||||
complete: boolean;
|
||||
path?: { id: number; name: string; level: number }[];
|
||||
}
|
||||
|
||||
/** 列表面板统一行 */
|
||||
export interface AddressListRow {
|
||||
level: RegionLevel;
|
||||
node: AddressNode;
|
||||
@@ -53,186 +57,39 @@ export interface AddressListRow {
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
const MUNICIPALITY_LABELS = new Set([
|
||||
'北京市',
|
||||
'天津市',
|
||||
'上海市',
|
||||
'重庆市',
|
||||
]);
|
||||
/** 回显文案缓存变更计数,供 computed 依赖 */
|
||||
export const addressDisplayTick = ref(0);
|
||||
|
||||
let provinceIndexCache: ProvinceIndex[] | null = null;
|
||||
const displayCache = new Map<string, string>();
|
||||
const pairCache = new Map<string, ResolvedAddress>();
|
||||
|
||||
function isPrefectureCityLabel(label: string): boolean {
|
||||
return label.endsWith('市') && !label.endsWith('自治区');
|
||||
function cacheKey(provinceId: number, leafId: number) {
|
||||
return `${provinceId}_${leafId}`;
|
||||
}
|
||||
|
||||
function isDistrictLikeLabel(label: string): boolean {
|
||||
return /(?:区|县|旗)$/.test(label);
|
||||
function levelFromNumber(level: number): RegionLevel {
|
||||
if (level <= 1) return 'province';
|
||||
if (level === 2) return 'city';
|
||||
return 'district';
|
||||
}
|
||||
|
||||
function detectMunicipality(province: AddressNode, children: AddressNode[]): boolean {
|
||||
if (MUNICIPALITY_LABELS.has(province.label)) {
|
||||
return true;
|
||||
}
|
||||
if (!province.label.endsWith('市') || children.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const districtCount = children.filter((c) => isDistrictLikeLabel(c.label)).length;
|
||||
return districtCount / children.length >= 0.8;
|
||||
function toNode(row: {
|
||||
id: number;
|
||||
name: string;
|
||||
pid?: number;
|
||||
level?: number;
|
||||
has_children?: boolean;
|
||||
}): AddressNode {
|
||||
return {
|
||||
value: row.id,
|
||||
label: row.name,
|
||||
pvalue: row.pid,
|
||||
level: row.level,
|
||||
hasChildren: !!row.has_children,
|
||||
};
|
||||
}
|
||||
|
||||
function buildProvinceIndexList(): ProvinceIndex[] {
|
||||
return (addressOption as AddressNode[]).map((province) => {
|
||||
const children = (province as AddressNode & { children?: AddressNode[] })
|
||||
.children ?? [];
|
||||
return {
|
||||
province: {
|
||||
value: province.value,
|
||||
label: province.label,
|
||||
pvalue: province.pvalue,
|
||||
},
|
||||
children: children.map((c) => ({
|
||||
value: c.value,
|
||||
label: c.label,
|
||||
pvalue: c.pvalue,
|
||||
})),
|
||||
isMunicipality: detectMunicipality(province, children),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getProvinceIndexList(): ProvinceIndex[] {
|
||||
if (!provinceIndexCache) {
|
||||
provinceIndexCache = buildProvinceIndexList();
|
||||
}
|
||||
return provinceIndexCache;
|
||||
}
|
||||
|
||||
function findProvinceIndex(provinceId?: number): ProvinceIndex | undefined {
|
||||
if (!provinceId) {
|
||||
return undefined;
|
||||
}
|
||||
return getProvinceIndexList().find((p) => p.province.value === provinceId);
|
||||
}
|
||||
|
||||
function findChild(
|
||||
provinceIndex: ProvinceIndex,
|
||||
childId?: number,
|
||||
): AddressNode | undefined {
|
||||
if (!childId) {
|
||||
return undefined;
|
||||
}
|
||||
return provinceIndex.children.find((c) => c.value === childId);
|
||||
}
|
||||
|
||||
function getCityNamePrefix(label: string): string {
|
||||
return label.replace(/市$/, '');
|
||||
}
|
||||
|
||||
/** 省级列表 */
|
||||
export function getProvinces(): AddressNode[] {
|
||||
return getProvinceIndexList().map((p) => p.province);
|
||||
}
|
||||
|
||||
/** 市级列表(直辖市返回空) */
|
||||
export function getCities(provinceId?: number): AddressNode[] {
|
||||
const index = findProvinceIndex(provinceId);
|
||||
if (!index || index.isMunicipality) {
|
||||
return [];
|
||||
}
|
||||
return index.children;
|
||||
}
|
||||
|
||||
/** 区级列表 */
|
||||
export function getDistricts(
|
||||
provinceId?: number,
|
||||
cityId?: number,
|
||||
): AddressNode[] {
|
||||
const index = findProvinceIndex(provinceId);
|
||||
if (!index) {
|
||||
return [];
|
||||
}
|
||||
if (index.isMunicipality) {
|
||||
return index.children;
|
||||
}
|
||||
if (!cityId) {
|
||||
return [];
|
||||
}
|
||||
const city = findChild(index, cityId);
|
||||
if (!city || !isPrefectureCityLabel(city.label)) {
|
||||
return [];
|
||||
}
|
||||
const prefix = getCityNamePrefix(city.label);
|
||||
return index.children.filter(
|
||||
(c) =>
|
||||
c.value !== cityId &&
|
||||
!isPrefectureCityLabel(c.label) &&
|
||||
c.label.startsWith(prefix),
|
||||
);
|
||||
}
|
||||
|
||||
/** 当前层级候选列表 */
|
||||
export function getLevelOptions(
|
||||
level: RegionLevel,
|
||||
ctx: SelectionContext,
|
||||
): AddressNode[] {
|
||||
if (level === 'province') {
|
||||
return getProvinces();
|
||||
}
|
||||
if (level === 'city') {
|
||||
return getCities(ctx.provinceId);
|
||||
}
|
||||
return getDistricts(ctx.provinceId, ctx.cityId);
|
||||
}
|
||||
|
||||
/** 选择某级后的下一层级(无则 undefined) */
|
||||
export function getNextLevel(
|
||||
currentLevel: RegionLevel,
|
||||
ctx: SelectionContext,
|
||||
): RegionLevel | undefined {
|
||||
const index = findProvinceIndex(ctx.provinceId);
|
||||
if (!index) {
|
||||
return undefined;
|
||||
}
|
||||
if (currentLevel === 'province') {
|
||||
return index.isMunicipality ? 'district' : 'city';
|
||||
}
|
||||
if (currentLevel === 'city') {
|
||||
if (!ctx.cityId) {
|
||||
return undefined;
|
||||
}
|
||||
const districts = getDistricts(ctx.provinceId, ctx.cityId);
|
||||
return districts.length > 0 ? 'district' : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 当前层级选中后是否可直接完成(叶节点) */
|
||||
export function canCompleteAfterSelect(
|
||||
level: RegionLevel,
|
||||
ctx: SelectionContext,
|
||||
item: AddressNode,
|
||||
): boolean {
|
||||
const index = findProvinceIndex(ctx.provinceId);
|
||||
if (!index) {
|
||||
return false;
|
||||
}
|
||||
if (level === 'district') {
|
||||
return true;
|
||||
}
|
||||
if (level === 'city') {
|
||||
if (index.isMunicipality) {
|
||||
return false;
|
||||
}
|
||||
if (!isPrefectureCityLabel(item.label)) {
|
||||
return true;
|
||||
}
|
||||
return getDistricts(ctx.provinceId, item.value).length === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 组装对外 address 值 */
|
||||
/** 组装对外 address 值 [provinceId, leafId] */
|
||||
export function buildAddressOutput(
|
||||
provinceId?: number,
|
||||
cityId?: number,
|
||||
@@ -248,7 +105,10 @@ export function buildAddressOutput(
|
||||
return [provinceId, leafId];
|
||||
}
|
||||
|
||||
/** 由 address 反查省/市/区 */
|
||||
/**
|
||||
* 同步解析:只读本地缓存(由 ensureResolvedAddress 填充)
|
||||
* 为什么:表格 formatter 是同步的,不能直接 await
|
||||
*/
|
||||
export function resolveAddressValue(
|
||||
value?: AddressValue | null,
|
||||
): ResolvedAddress {
|
||||
@@ -256,323 +116,191 @@ export function resolveAddressValue(
|
||||
return {};
|
||||
}
|
||||
const [provinceId, leafId] = value;
|
||||
const index = findProvinceIndex(provinceId);
|
||||
if (!index) {
|
||||
return { provinceId };
|
||||
}
|
||||
const leaf = findChild(index, leafId);
|
||||
if (!leaf) {
|
||||
return { provinceId, province: index.province };
|
||||
}
|
||||
|
||||
if (index.isMunicipality) {
|
||||
return {
|
||||
provinceId,
|
||||
districtId: leafId,
|
||||
province: index.province,
|
||||
district: leaf,
|
||||
};
|
||||
}
|
||||
|
||||
const isLeafDistrict =
|
||||
!isPrefectureCityLabel(leaf.label) && isDistrictLikeLabel(leaf.label);
|
||||
|
||||
if (isPrefectureCityLabel(leaf.label)) {
|
||||
return {
|
||||
return (
|
||||
pairCache.get(cacheKey(provinceId, leafId)) ?? {
|
||||
provinceId,
|
||||
cityId: leafId,
|
||||
province: index.province,
|
||||
city: leaf,
|
||||
};
|
||||
}
|
||||
|
||||
if (isLeafDistrict) {
|
||||
for (const city of index.children.filter((c) =>
|
||||
isPrefectureCityLabel(c.label),
|
||||
)) {
|
||||
const prefix = getCityNamePrefix(city.label);
|
||||
if (leaf.label.startsWith(prefix)) {
|
||||
return {
|
||||
provinceId,
|
||||
cityId: city.value,
|
||||
districtId: leafId,
|
||||
province: index.province,
|
||||
city,
|
||||
district: leaf,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
provinceId,
|
||||
cityId: leafId,
|
||||
province: index.province,
|
||||
city: leaf,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
provinceId,
|
||||
cityId: leafId,
|
||||
province: index.province,
|
||||
city: leaf,
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
/** 面包屑展示项 */
|
||||
export function formatBreadcrumbItems(
|
||||
resolved: ResolvedAddress,
|
||||
): BreadcrumbItem[] {
|
||||
/**
|
||||
* 异步解析并写入缓存(含软删节点,后端 resolve 不过滤 deleted_at)
|
||||
*/
|
||||
export async function ensureResolvedAddress(
|
||||
value?: AddressValue | null,
|
||||
): Promise<ResolvedAddress> {
|
||||
if (!value || value.length < 2) {
|
||||
return {};
|
||||
}
|
||||
const [provinceId, leafId] = value;
|
||||
const key = cacheKey(provinceId, leafId);
|
||||
if (pairCache.has(key)) {
|
||||
return pairCache.get(key)!;
|
||||
}
|
||||
try {
|
||||
const res = await resolveRegionPair(provinceId, leafId);
|
||||
const resolved: ResolvedAddress = {
|
||||
provinceId,
|
||||
cityId: res.city?.id ?? (res.district ? undefined : leafId),
|
||||
districtId: res.district?.id,
|
||||
province: res.province
|
||||
? toNode(res.province as RegionPickerChild)
|
||||
: undefined,
|
||||
city: res.city
|
||||
? {
|
||||
value: res.city.id,
|
||||
label: res.city.name,
|
||||
pvalue: res.city.pid,
|
||||
level: res.city.level,
|
||||
}
|
||||
: undefined,
|
||||
district: res.district
|
||||
? {
|
||||
value: res.district.id,
|
||||
label: res.district.name,
|
||||
pvalue: res.district.pid,
|
||||
level: res.district.level,
|
||||
}
|
||||
: undefined,
|
||||
pathLabel: res.path_label || '',
|
||||
};
|
||||
// 两段存法下 city_id 就是叶子:若只有 district,把 leaf 记在 districtId
|
||||
if (res.district && !res.city) {
|
||||
resolved.districtId = leafId;
|
||||
} else if (res.district) {
|
||||
resolved.cityId = res.city?.id;
|
||||
resolved.districtId = res.district.id;
|
||||
} else {
|
||||
resolved.cityId = leafId;
|
||||
}
|
||||
pairCache.set(key, resolved);
|
||||
if (resolved.pathLabel) {
|
||||
displayCache.set(key, resolved.pathLabel);
|
||||
}
|
||||
addressDisplayTick.value += 1;
|
||||
return resolved;
|
||||
} catch {
|
||||
return { provinceId, cityId: leafId };
|
||||
}
|
||||
}
|
||||
|
||||
/** 同步展示:优先缓存文案;未命中时触发异步加载并先返回空串 */
|
||||
export function formatAddressDisplay(value?: AddressValue | null): string {
|
||||
// 依赖 tick,缓存更新后 computed 会重算
|
||||
void addressDisplayTick.value;
|
||||
if (!value || value.length < 2) {
|
||||
return '';
|
||||
}
|
||||
const [provinceId, leafId] = value;
|
||||
const key = cacheKey(provinceId, leafId);
|
||||
if (displayCache.has(key)) {
|
||||
return displayCache.get(key)!;
|
||||
}
|
||||
void ensureResolvedAddress(value);
|
||||
return '';
|
||||
}
|
||||
|
||||
export function formatBreadcrumbItems(resolved: ResolvedAddress): BreadcrumbItem[] {
|
||||
const items: BreadcrumbItem[] = [];
|
||||
if (resolved.provinceId && resolved.province) {
|
||||
if (resolved.province) {
|
||||
items.push({
|
||||
level: 'province',
|
||||
id: resolved.provinceId,
|
||||
id: resolved.province.value,
|
||||
label: resolved.province.label,
|
||||
});
|
||||
}
|
||||
const index = findProvinceIndex(resolved.provinceId);
|
||||
if (index?.isMunicipality) {
|
||||
if (resolved.districtId && resolved.district) {
|
||||
items.push({
|
||||
level: 'district',
|
||||
id: resolved.districtId,
|
||||
label: resolved.district.label,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
if (resolved.cityId && resolved.city) {
|
||||
if (resolved.city) {
|
||||
items.push({
|
||||
level: 'city',
|
||||
id: resolved.cityId,
|
||||
id: resolved.city.value,
|
||||
label: resolved.city.label,
|
||||
});
|
||||
}
|
||||
if (resolved.districtId && resolved.district) {
|
||||
if (resolved.district) {
|
||||
items.push({
|
||||
level: 'district',
|
||||
id: resolved.districtId,
|
||||
id: resolved.district.value,
|
||||
label: resolved.district.label,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** 触发器展示文案 */
|
||||
export function formatAddressDisplay(value?: AddressValue | null): string {
|
||||
const items = formatBreadcrumbItems(resolveAddressValue(value));
|
||||
return items.map((i) => i.label).join(' / ');
|
||||
}
|
||||
|
||||
/** 当前层级搜索占位 */
|
||||
export function getLevelSearchPlaceholder(level: RegionLevel): string {
|
||||
const map: Record<RegionLevel, string> = {
|
||||
province: '搜索省',
|
||||
city: '搜索市/县',
|
||||
district: '搜索区',
|
||||
};
|
||||
return map[level];
|
||||
}
|
||||
|
||||
/** 模糊筛选 */
|
||||
export function filterAddressOptions(
|
||||
options: AddressNode[],
|
||||
keyword: string,
|
||||
): AddressNode[] {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
if (!q) {
|
||||
return options;
|
||||
}
|
||||
return options.filter((o) => o.label.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
/** 从已选上下文生成面包屑(面板内) */
|
||||
export function buildBreadcrumbFromContext(
|
||||
ctx: SelectionContext,
|
||||
labels?: { province?: string; city?: string; district?: string },
|
||||
): BreadcrumbItem[] {
|
||||
return formatBreadcrumbItems({
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: ctx.cityId,
|
||||
districtId: ctx.districtId,
|
||||
province: findProvinceIndex(ctx.provinceId)?.province,
|
||||
city: findChild(findProvinceIndex(ctx.provinceId)!, ctx.cityId),
|
||||
district: findChild(findProvinceIndex(ctx.provinceId)!, ctx.districtId),
|
||||
});
|
||||
const items: BreadcrumbItem[] = [];
|
||||
if (ctx.provinceId) {
|
||||
items.push({
|
||||
level: 'province',
|
||||
id: ctx.provinceId,
|
||||
label: labels?.province || String(ctx.provinceId),
|
||||
});
|
||||
}
|
||||
if (ctx.cityId) {
|
||||
items.push({
|
||||
level: 'city',
|
||||
id: ctx.cityId,
|
||||
label: labels?.city || String(ctx.cityId),
|
||||
});
|
||||
}
|
||||
if (ctx.districtId) {
|
||||
items.push({
|
||||
level: 'district',
|
||||
id: ctx.districtId,
|
||||
label: labels?.district || String(ctx.districtId),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** 根据面板内选中生成 ResolvedAddress */
|
||||
export function contextToResolved(ctx: SelectionContext): ResolvedAddress {
|
||||
const index = findProvinceIndex(ctx.provinceId);
|
||||
return {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: ctx.cityId,
|
||||
districtId: ctx.districtId,
|
||||
province: index?.province,
|
||||
city: index ? findChild(index, ctx.cityId) : undefined,
|
||||
district: index ? findChild(index, ctx.districtId) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否直辖市 */
|
||||
export function isMunicipalityProvince(provinceId?: number): boolean {
|
||||
return !!findProvinceIndex(provinceId)?.isMunicipality;
|
||||
}
|
||||
|
||||
/** 打开面板时的初始层级(有回填则进入待选级,否则省) */
|
||||
export function getInitialPickerLevel(
|
||||
resolved: ResolvedAddress,
|
||||
): RegionLevel {
|
||||
const index = findProvinceIndex(resolved.provinceId);
|
||||
if (!resolved.provinceId || !index) {
|
||||
return 'province';
|
||||
}
|
||||
if (index.isMunicipality) {
|
||||
return 'district';
|
||||
}
|
||||
if (!resolved.cityId) {
|
||||
return 'city';
|
||||
}
|
||||
const districts = getDistricts(resolved.provinceId, resolved.cityId);
|
||||
if (!resolved.districtId && districts.length > 0) {
|
||||
return 'district';
|
||||
}
|
||||
return 'city';
|
||||
}
|
||||
|
||||
/** 当前级候选转列表行 */
|
||||
export function levelOptionsToRows(
|
||||
/** 拉取某一层选项 */
|
||||
export async function fetchLevelOptions(
|
||||
level: RegionLevel,
|
||||
ctx: SelectionContext,
|
||||
): Promise<AddressNode[]> {
|
||||
let pid = 0;
|
||||
if (level === 'city') {
|
||||
pid = ctx.provinceId ?? 0;
|
||||
} else if (level === 'district') {
|
||||
pid = ctx.cityId ?? 0;
|
||||
}
|
||||
const rows = await getRegionChildren(pid);
|
||||
return rows.map((r) => toNode(r));
|
||||
}
|
||||
|
||||
export function levelOptionsToRows(
|
||||
level: RegionLevel,
|
||||
options: AddressNode[],
|
||||
): AddressListRow[] {
|
||||
return getLevelOptions(level, ctx).map((node) => ({
|
||||
return options.map((node) => ({
|
||||
level,
|
||||
node,
|
||||
label: node.label,
|
||||
complete: canCompleteAfterSelect(
|
||||
level,
|
||||
level === 'city'
|
||||
? { provinceId: ctx.provinceId, cityId: node.value }
|
||||
: level === 'district'
|
||||
? {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: ctx.cityId,
|
||||
districtId: node.value,
|
||||
}
|
||||
: ctx,
|
||||
node,
|
||||
),
|
||||
// 无子节点则可完成;有子节点继续下钻
|
||||
complete: !node.hasChildren,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 跨级模糊搜索(含第三级区/县) */
|
||||
export function searchAddressHits(
|
||||
export async function searchAddressHits(
|
||||
keyword: string,
|
||||
ctx: SelectionContext,
|
||||
): AddressSearchHit[] {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
if (!q) {
|
||||
): Promise<AddressSearchHit[]> {
|
||||
const kw = keyword.trim();
|
||||
if (!kw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hits: AddressSearchHit[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const push = (hit: AddressSearchHit) => {
|
||||
const key = `${hit.level}-${hit.node.value}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
hits.push(hit);
|
||||
}
|
||||
};
|
||||
|
||||
if (!ctx.provinceId) {
|
||||
for (const p of getProvinces()) {
|
||||
if (p.label.toLowerCase().includes(q)) {
|
||||
push({
|
||||
level: 'province',
|
||||
node: p,
|
||||
pathLabel: p.label,
|
||||
complete: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
const index = findProvinceIndex(ctx.provinceId);
|
||||
if (!index) {
|
||||
return hits;
|
||||
}
|
||||
|
||||
if (index.isMunicipality) {
|
||||
for (const d of index.children) {
|
||||
if (d.label.toLowerCase().includes(q)) {
|
||||
push({
|
||||
level: 'district',
|
||||
node: d,
|
||||
pathLabel: `${index.province.label} / ${d.label}`,
|
||||
complete: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
const cityNode = ctx.cityId ? findChild(index, ctx.cityId) : undefined;
|
||||
|
||||
if (!ctx.cityId) {
|
||||
for (const child of index.children) {
|
||||
if (!child.label.toLowerCase().includes(q)) {
|
||||
continue;
|
||||
}
|
||||
const rowCtx = { provinceId: ctx.provinceId, cityId: child.value };
|
||||
push({
|
||||
level: 'city',
|
||||
node: child,
|
||||
pathLabel: `${index.province.label} / ${child.label}`,
|
||||
complete: canCompleteAfterSelect('city', rowCtx, child),
|
||||
});
|
||||
}
|
||||
for (const child of index.children.filter((c) => isPrefectureCityLabel(c.label))) {
|
||||
for (const d of getDistricts(ctx.provinceId, child.value)) {
|
||||
if (!d.label.toLowerCase().includes(q)) {
|
||||
continue;
|
||||
}
|
||||
push({
|
||||
level: 'district',
|
||||
node: d,
|
||||
pathLabel: `${index.province.label} / ${child.label} / ${d.label}`,
|
||||
complete: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
if (cityNode && cityNode.label.toLowerCase().includes(q)) {
|
||||
push({
|
||||
level: 'city',
|
||||
node: cityNode,
|
||||
pathLabel: `${index.province.label} / ${cityNode.label}`,
|
||||
complete: canCompleteAfterSelect('city', ctx, cityNode),
|
||||
});
|
||||
}
|
||||
|
||||
for (const d of getDistricts(ctx.provinceId, ctx.cityId)) {
|
||||
if (d.label.toLowerCase().includes(q)) {
|
||||
push({
|
||||
level: 'district',
|
||||
node: d,
|
||||
pathLabel: `${index.province.label} / ${cityNode?.label ?? ''} / ${d.label}`,
|
||||
complete: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hits;
|
||||
const hits = await searchRegions(kw, 30);
|
||||
return hits.map((h: RegionPickerSearchHit) => ({
|
||||
level: levelFromNumber(h.level),
|
||||
node: toNode(h),
|
||||
pathLabel: h.path_label || h.name,
|
||||
complete: !!h.complete,
|
||||
path: h.path,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 搜索命中转列表行 */
|
||||
export function searchHitsToRows(hits: AddressSearchHit[]): AddressListRow[] {
|
||||
return hits.map((hit) => ({
|
||||
level: hit.level,
|
||||
@@ -582,79 +310,89 @@ export function searchHitsToRows(hits: AddressSearchHit[]): AddressListRow[] {
|
||||
}));
|
||||
}
|
||||
|
||||
/** 应用跨级搜索选中 */
|
||||
export function applySearchHit(
|
||||
hit: AddressSearchHit,
|
||||
ctx: SelectionContext,
|
||||
_ctx: SelectionContext,
|
||||
): SelectionContext {
|
||||
const next: SelectionContext = {};
|
||||
const path = hit.path ?? [];
|
||||
for (const node of path) {
|
||||
if (node.level === 1) {
|
||||
next.provinceId = node.id;
|
||||
} else if (node.level === 2) {
|
||||
next.cityId = node.id;
|
||||
} else if (node.level === 3) {
|
||||
next.districtId = node.id;
|
||||
}
|
||||
}
|
||||
// 兜底:path 不完整时用命中节点本身
|
||||
if (hit.level === 'province') {
|
||||
return {
|
||||
provinceId: hit.node.value,
|
||||
cityId: undefined,
|
||||
districtId: undefined,
|
||||
};
|
||||
next.provinceId = hit.node.value;
|
||||
} else if (hit.level === 'city') {
|
||||
next.cityId = hit.node.value;
|
||||
if (!next.provinceId) {
|
||||
next.provinceId = hit.node.pvalue;
|
||||
}
|
||||
} else {
|
||||
next.districtId = hit.node.value;
|
||||
if (!next.cityId) {
|
||||
next.cityId = hit.node.pvalue;
|
||||
}
|
||||
}
|
||||
if (hit.level === 'city') {
|
||||
return {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: hit.node.value,
|
||||
districtId: undefined,
|
||||
};
|
||||
}
|
||||
if (hit.level === 'district' && findProvinceIndex(ctx.provinceId)?.isMunicipality) {
|
||||
return {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: undefined,
|
||||
districtId: hit.node.value,
|
||||
};
|
||||
}
|
||||
return {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: ctx.cityId,
|
||||
districtId: hit.node.value,
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 高亮项是否有下一级 */
|
||||
export function rowHasNextLevel(
|
||||
row: AddressListRow,
|
||||
ctx: SelectionContext,
|
||||
export function getNextLevel(
|
||||
level: RegionLevel,
|
||||
_ctx: SelectionContext,
|
||||
hasChildren?: boolean,
|
||||
): RegionLevel | undefined {
|
||||
if (hasChildren === false) {
|
||||
return undefined;
|
||||
}
|
||||
if (level === 'province') {
|
||||
return 'city';
|
||||
}
|
||||
if (level === 'city') {
|
||||
return 'district';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getInitialPickerLevel(resolved: ResolvedAddress): RegionLevel {
|
||||
if (resolved.districtId) {
|
||||
return 'district';
|
||||
}
|
||||
if (resolved.cityId) {
|
||||
return 'city';
|
||||
}
|
||||
return 'province';
|
||||
}
|
||||
|
||||
export function canCompleteAfterSelect(
|
||||
level: RegionLevel,
|
||||
_ctx: SelectionContext,
|
||||
item: AddressNode,
|
||||
): boolean {
|
||||
if (row.level === 'province') {
|
||||
return !!getNextLevel('province', {
|
||||
provinceId: row.node.value,
|
||||
cityId: undefined,
|
||||
districtId: undefined,
|
||||
});
|
||||
if (level === 'district') {
|
||||
return true;
|
||||
}
|
||||
if (row.level === 'city') {
|
||||
return !!getNextLevel('city', {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: row.node.value,
|
||||
districtId: undefined,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
return !item.hasChildren;
|
||||
}
|
||||
|
||||
export function rowHasNextLevel(row: AddressListRow): boolean {
|
||||
return !row.complete && !!row.node.hasChildren;
|
||||
}
|
||||
|
||||
/** 点击面包屑项后截断选中并返回新上下文 */
|
||||
export function truncateContextToBreadcrumb(
|
||||
ctx: SelectionContext,
|
||||
item: BreadcrumbItem,
|
||||
): SelectionContext {
|
||||
if (item.level === 'province') {
|
||||
return {
|
||||
provinceId: item.id,
|
||||
cityId: undefined,
|
||||
districtId: undefined,
|
||||
};
|
||||
return { provinceId: item.id };
|
||||
}
|
||||
if (item.level === 'city') {
|
||||
return {
|
||||
provinceId: ctx.provinceId,
|
||||
cityId: item.id,
|
||||
districtId: undefined,
|
||||
};
|
||||
return { provinceId: ctx.provinceId, cityId: item.id };
|
||||
}
|
||||
return {
|
||||
provinceId: ctx.provinceId,
|
||||
@@ -662,3 +400,18 @@ export function truncateContextToBreadcrumb(
|
||||
districtId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function getLevelSearchPlaceholder(level: RegionLevel): string {
|
||||
if (level === 'province') {
|
||||
return '搜索省(地名/首拼/全拼)';
|
||||
}
|
||||
if (level === 'city') {
|
||||
return '搜索市(地名/首拼/全拼)';
|
||||
}
|
||||
return '搜索区县(地名/首拼/全拼)';
|
||||
}
|
||||
|
||||
/** 兼容旧调用:直辖市判断改为「是否直接有区」由 hasChildren 驱动,此函数保留空实现 */
|
||||
export function isMunicipalityProvince(_provinceId?: number): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user