feat: 地区选择器改走接口、订单市价重算与资金流水、同步日志内容复用

region-address-picker 接 region-picker API;订单支持按进价重算 market_price、追踪页资金流水;MES/ERP/中台同步日志共用内容查看组件;地区管理支持 pid 下钻与拼音检索。
This commit is contained in:
李琦
2026-10-09 10:02:33 +08:00
parent 87c480f391
commit 8df1ffb80a
26 changed files with 2256 additions and 1109 deletions

View File

@@ -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