1. 管理员管理模块拆分

2. 图片上传自动压缩(大于850kb)
3. 省市区选择组件重构,支持搜索
This commit is contained in:
李琦
2026-05-27 09:34:08 +08:00
parent 68fd7b0045
commit 402da17e3d
56 changed files with 4188 additions and 13664 deletions

View File

@@ -0,0 +1,664 @@
import { addressOption } from '#/util/address';
export interface AddressNode {
value: number;
label: string;
pvalue?: number;
}
export interface ProvinceIndex {
province: AddressNode;
children: AddressNode[];
isMunicipality: boolean;
}
export interface ResolvedAddress {
provinceId?: number;
cityId?: number;
districtId?: number;
province?: AddressNode;
city?: AddressNode;
district?: AddressNode;
}
export type AddressValue = [number, number];
export type RegionLevel = 'province' | 'city' | 'district';
export interface BreadcrumbItem {
level: RegionLevel;
id: number;
label: string;
}
export interface SelectionContext {
provinceId?: number;
cityId?: number;
districtId?: number;
}
/** 跨级搜索结果 */
export interface AddressSearchHit {
level: RegionLevel;
node: AddressNode;
pathLabel: string;
complete: boolean;
}
/** 列表面板统一行 */
export interface AddressListRow {
level: RegionLevel;
node: AddressNode;
label: string;
complete: boolean;
}
const MUNICIPALITY_LABELS = new Set([
'北京市',
'天津市',
'上海市',
'重庆市',
]);
let provinceIndexCache: ProvinceIndex[] | null = null;
function isPrefectureCityLabel(label: string): boolean {
return label.endsWith('市') && !label.endsWith('自治区');
}
function isDistrictLikeLabel(label: string): boolean {
return /(?:区|县|旗)$/.test(label);
}
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 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 值 */
export function buildAddressOutput(
provinceId?: number,
cityId?: number,
districtId?: number,
): AddressValue | undefined {
if (!provinceId) {
return undefined;
}
const leafId = districtId ?? cityId;
if (!leafId) {
return undefined;
}
return [provinceId, leafId];
}
/** 由 address 反查省/市/区 */
export function resolveAddressValue(
value?: AddressValue | null,
): ResolvedAddress {
if (!value || value.length < 2) {
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 {
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[] {
const items: BreadcrumbItem[] = [];
if (resolved.provinceId && resolved.province) {
items.push({
level: 'province',
id: resolved.provinceId,
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) {
items.push({
level: 'city',
id: resolved.cityId,
label: resolved.city.label,
});
}
if (resolved.districtId && resolved.district) {
items.push({
level: 'district',
id: resolved.districtId,
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,
): 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),
});
}
/** 根据面板内选中生成 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(
level: RegionLevel,
ctx: SelectionContext,
): AddressListRow[] {
return getLevelOptions(level, ctx).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,
),
}));
}
/** 跨级模糊搜索(含第三级区/县) */
export function searchAddressHits(
keyword: string,
ctx: SelectionContext,
): AddressSearchHit[] {
const q = keyword.trim().toLowerCase();
if (!q) {
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;
}
/** 搜索命中转列表行 */
export function searchHitsToRows(hits: AddressSearchHit[]): AddressListRow[] {
return hits.map((hit) => ({
level: hit.level,
node: hit.node,
label: hit.pathLabel,
complete: hit.complete,
}));
}
/** 应用跨级搜索选中 */
export function applySearchHit(
hit: AddressSearchHit,
ctx: SelectionContext,
): SelectionContext {
if (hit.level === 'province') {
return {
provinceId: hit.node.value,
cityId: undefined,
districtId: undefined,
};
}
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,
};
}
/** 高亮项是否有下一级 */
export function rowHasNextLevel(
row: AddressListRow,
ctx: SelectionContext,
): boolean {
if (row.level === 'province') {
return !!getNextLevel('province', {
provinceId: row.node.value,
cityId: undefined,
districtId: undefined,
});
}
if (row.level === 'city') {
return !!getNextLevel('city', {
provinceId: ctx.provinceId,
cityId: row.node.value,
districtId: undefined,
});
}
return false;
}
/** 点击面包屑项后截断选中并返回新上下文 */
export function truncateContextToBreadcrumb(
ctx: SelectionContext,
item: BreadcrumbItem,
): SelectionContext {
if (item.level === 'province') {
return {
provinceId: item.id,
cityId: undefined,
districtId: undefined,
};
}
if (item.level === 'city') {
return {
provinceId: ctx.provinceId,
cityId: item.id,
districtId: undefined,
};
}
return {
provinceId: ctx.provinceId,
cityId: ctx.cityId,
districtId: item.id,
};
}