fix: bind knowledge maps to explicit regions

This commit is contained in:
xuelong committed 2026-08-26 02:26:24 -07:00
1 parent 329a94d457
commit 90f75d41ae
15 files changed
+1126 -275

No files matched your search

+44 -8
View File
@@ -10,6 +10,7 @@ import {
Input,
List,
Modal,
Select,
Space,
Spin,
Steps,
@@ -47,6 +48,11 @@ import {
type ProjectSummary,
type GraphReleaseSummary,
} from "./api";
import {
SPATIAL_MAP_REGIONS,
spatialMapConfigForRegion,
spatialMapRegionByAdcode,
} from "./spatialMapRegions";
import {
mergeProjectCatalogItem,
notifyProjectCatalogChanged,
@@ -59,6 +65,7 @@ type BasicProjectValues = {
project_id: string;
display_name: string;
map_enabled: boolean;
map_region_adcode?: string;
};
type GraphImportData = {
@@ -659,6 +666,7 @@ export default function ProjectLanding() {
const [deleting, setDeleting] = useState(false);
const [basicForm] = Form.useForm<BasicProjectValues>();
const mapEnabled = Form.useWatch("map_enabled", basicForm);
const [renameForm] = Form.useForm<{ display_name: string }>();
const impactRequest = useRef(0);
const navigate = useNavigate();
@@ -747,6 +755,9 @@ export default function ProjectLanding() {
project_id: values.project_id.trim(),
display_name: values.display_name.trim(),
map_enabled: Boolean(values.map_enabled),
map_region_adcode: values.map_enabled
? String(values.map_region_adcode || "").trim()
: undefined,
};
setCheckingBasics(true);
try {
@@ -855,10 +866,10 @@ export default function ProjectLanding() {
const { data } = await provisionGraphProject({
project_id: basicValues.project_id,
display_name: basicValues.display_name,
spatial_map: {
enabled: basicValues.map_enabled,
scope: "libo",
},
spatial_map: spatialMapConfigForRegion(
basicValues.map_enabled,
basicValues.map_region_adcode,
),
schema: schemaResult.value,
graph_data: graphResult.value,
});
@@ -1116,10 +1127,30 @@ export default function ProjectLanding() {
</Form.Item>
</div>
<div className="project-map-option">
<Text strong>知识地图</Text>
<Form.Item name="map_enabled" valuePropName="checked" noStyle>
<Switch checkedChildren="显示" unCheckedChildren="不显示" />
</Form.Item>
<div className="project-map-option-header">
<Text strong>知识地图</Text>
<Form.Item name="map_enabled" valuePropName="checked" noStyle>
<Switch checkedChildren="显示" unCheckedChildren="不显示" />
</Form.Item>
</div>
{mapEnabled && (
<Form.Item
label="地图区域"
name="map_region_adcode"
rules={[{ required: true, message: "请选择地图区域" }]}
className="project-map-region"
>
<Select
showSearch
optionFilterProp="label"
placeholder="请选择省、市或区县"
options={SPATIAL_MAP_REGIONS.map((region) => ({
value: region.adcode,
label: region.label,
}))}
/>
</Form.Item>
)}
</div>
<Alert
type="info"
@@ -1234,6 +1265,11 @@ export default function ProjectLanding() {
<Descriptions.Item label="知识地图" span={2}>
{basicValues?.map_enabled ? "显示" : "不显示"}
</Descriptions.Item>
{basicValues?.map_enabled && (
<Descriptions.Item label="地图区域" span={2}>
{spatialMapRegionByAdcode(basicValues.map_region_adcode)?.name || "-"}
</Descriptions.Item>
)}
</Descriptions>
<Alert
type="warning"
+4
View File
@@ -143,10 +143,14 @@ export type ProjectSummary = {
};
export type SpatialMapScope = "libo" | "guizhou";
export type SpatialMapRegionLevel = "province" | "city" | "district";
export type SpatialMapConfig = {
enabled: boolean;
scope: SpatialMapScope;
region_name: string;
region_adcode: string;
region_level: SpatialMapRegionLevel;
};
export type GraphReleaseSummary = {
+48 -10
View File
@@ -16,7 +16,7 @@ import {
type ProjectSummary,
type SpatialMapConfig,
} from "../../api";
import SuperAgentPanel, { KnowledgeMap, type CoverageSnapshot } from "./SuperAgentPanel";
import { KnowledgeMap, type CoverageSnapshot } from "./SuperAgentPanel";
import {
buildGenericGraphCategories,
resolveOverviewCategoryView,
@@ -81,19 +81,49 @@ const resolveSpatialMap = (
context: KgProjectContext,
): SpatialMapConfig => {
const configured = project?.metadata_jsonb?.spatial_map;
const legacyLiboProject = isLiboContext(context);
const legacyCityProject = context.graphName === DEFAULT_PROJECT_CONTEXT.graphName
|| context.projectId === DEFAULT_PROJECT_CONTEXT.projectId;
const fallback = legacyLiboProject
? {
scope: "libo" as const,
region_name: "荔波县",
region_adcode: "522722",
region_level: "district" as const,
}
: legacyCityProject
? {
scope: "guizhou" as const,
region_name: "贵阳市",
region_adcode: "520100",
region_level: "city" as const,
}
: {
scope: "guizhou" as const,
region_name: "贵州省",
region_adcode: "520000",
region_level: "province" as const,
};
if (configured && typeof configured.enabled === "boolean") {
const regionAdcode = String(configured.region_adcode || fallback.region_adcode);
return {
enabled: configured.enabled,
scope: configured.scope === "guizhou" ? "guizhou" : "libo",
scope: regionAdcode === "522722" ? "libo" : "guizhou",
region_name: String(configured.region_name || fallback.region_name),
region_adcode: regionAdcode,
region_level: configured.region_level === "province"
|| configured.region_level === "city"
|| configured.region_level === "district"
? configured.region_level
: fallback.region_level,
};
}
// Compatibility only: projects created before the explicit map option did
// not persist this capability. New projects never depend on their id/name.
const legacyLiboProject = isLiboContext(context);
return {
enabled: legacyLiboProject,
scope: legacyLiboProject ? "libo" : "guizhou",
enabled: legacyLiboProject || legacyCityProject,
...fallback,
};
};
@@ -129,12 +159,11 @@ export default function PlazaOverview() {
: [];
const currentProject = projects.find((project) => project.project_id === currentContext.projectId) || null;
const currentSpatialMap = resolveSpatialMap(currentProject, currentContext);
const isCityProject = currentContext.graphName === DEFAULT_PROJECT_CONTEXT.graphName;
const isKnowledgeMapProject = isCityProject || currentSpatialMap.enabled;
const isKnowledgeMapProject = currentSpatialMap.enabled;
const placeTypeRows = isKnowledgeMapProject && currentGraphData.connected === true
? await graphQuery(GRAPH_CATEGORY_CYPHER).then((res) => res.data.rows || []).catch(() => [])
: [];
const coverage = currentSpatialMap.enabled
const coverage = currentContext.graphName === DEFAULT_PROJECT_CONTEXT.graphName
? await superAgentCoverage().then((res) => res.data).catch(() => null)
: null;
setProjectContextState(currentContext);
@@ -189,8 +218,17 @@ export default function PlazaOverview() {
<div>
<h2>{pageTitle}</h2>
{isCityProject && <SuperAgentPanel />}
{isKnowledgeMapProject && <KnowledgeMap cov={mapCoverage} scope={spatialMap.scope} />}
{isKnowledgeMapProject && (
<KnowledgeMap
cov={mapCoverage}
projectId={projectContext.projectId}
projectName={projectSummary?.display_name || projectSummary?.name || projectContext.projectId}
scope={spatialMap.scope}
regionName={spatialMap.region_name}
regionAdcode={spatialMap.region_adcode}
regionLevel={spatialMap.region_level}
/>
)}
{/* Stat cards */}
<Row gutter={16} style={{ marginBottom: 24 }}>
+117 -139
View File
@@ -52,8 +52,10 @@ type MapPoi = {
lng: number;
lat: number;
address: string;
province?: string;
district: string;
city: string;
adcode?: string;
towncode?: string;
town_name?: string;
zone_id?: string;
@@ -215,18 +217,8 @@ const DIANPING_APP_ICON_URL =
const poiCategoryColor = (category?: string) =>
POI_CATEGORY_COLORS[category || ""] || "#597ef7";
// 高德式搜索面板的分类快捷入口图标(按用户提供的 logo:披萨/酒店/城堡/出租车)。
const POI_CATEGORY_EMOJI: Record<string, string> = {
美食: "🍕",
酒店: "🏨",
交通设施: "🚕",
景点: "🎡",
};
const poiCategoryEmoji = (category?: string) =>
POI_CATEGORY_EMOJI[category || ""] || "📍";
// 分类快捷入口的自定义 logo 图片(放 admin-web/public/category-icons/ 即生效)。
// 已预接路径:图片存在→<img> 显示插画;不存在→onError 自动回退到上面的 emoji。
// 分类快捷入口的自定义图片;未配置的动态业态使用现有 Ant 图标库,
// 保证所有地图项目共用同一套模板时不会出现缺图或表情符号占位。
// ?v=N 作为缓存刷新戳:换了图标就把 N +1,强制浏览器重新拉取(否则同名文件走旧缓存)。
const CATEGORY_ICON_VER = "3";
const POI_CATEGORY_ICON: Record<string, string> = {
@@ -237,8 +229,7 @@ const POI_CATEGORY_ICON: Record<string, string> = {
};
const poiCategoryIcon = (category?: string) =>
POI_CATEGORY_ICON[category || ""] || "";
const LIBO_RECENT_SEARCH_KEY = "znkg:libo-poi-recent";
const MAP_SHORTCUT_CATEGORIES = ["美食", "酒店", "景点", "交通设施"] as const;
/** 荔波县实时天气(高德 AMap.Weather 插件返回,getLive + getForecast 合成)。 */
type LiboWeather = {
@@ -2010,24 +2001,22 @@ const loadAmapSdk = (jsKey: string, securityJsCode?: string) => {
return amapSdkPromise;
};
const GUIZHOU_CITIES = [
{ name: "贵阳", coord: [106.6302, 26.6470], active: true },
{ name: "遵义", coord: [106.9373, 27.7066] },
{ name: "六盘水", coord: [104.8304, 26.5934] },
{ name: "安顺", coord: [105.9476, 26.2531] },
{ name: "毕节", coord: [105.2916, 27.2836] },
{ name: "铜仁", coord: [109.1916, 27.7183] },
{ name: "黔东南", coord: [107.9829, 26.5834] },
{ name: "黔南", coord: [107.5170, 26.2582] },
{ name: "黔西南", coord: [104.9064, 25.0878] },
];
export function KnowledgeMap({
cov,
cov = null,
scope = "guizhou",
projectId = "default",
projectName = "当前项目",
regionName = "贵州省",
regionAdcode = "520000",
regionLevel = "province",
}: {
cov: CoverageSnapshot | null;
cov?: CoverageSnapshot | null;
scope?: "guizhou" | "libo";
projectId?: string;
projectName?: string;
regionName?: string;
regionAdcode?: string;
regionLevel?: "province" | "city" | "district";
}) {
const mapRef = useRef<HTMLDivElement | null>(null);
const mapShellRef = useRef<HTMLDivElement | null>(null);
@@ -2060,11 +2049,12 @@ export function KnowledgeMap({
// 底图图层:标准 / 卫星影像 + 可叠加实时路况
const [satelliteOn, setSatelliteOn] = useState(false);
const [trafficOn, setTrafficOn] = useState(false);
// 自定义分类 logo 加载失败(文件未放)时回退 emoji 的记录。
// 自定义分类图片加载失败时,回退到项目现有的 Ant 图标库。
const [catIconErr, setCatIconErr] = useState<Record<string, boolean>>({});
const recentSearchKey = `znkg:map-poi-recent:${projectId || "default"}`;
const [recentSearches, setRecentSearches] = useState<MapPoi[]>(() => {
try {
const raw = localStorage.getItem(LIBO_RECENT_SEARCH_KEY);
const raw = localStorage.getItem(recentSearchKey);
return raw ? (JSON.parse(raw) as MapPoi[]) : [];
} catch {
return [];
@@ -2091,8 +2081,19 @@ export function KnowledgeMap({
const [zonePickerOpen, setZonePickerOpen] = useState(false);
const [selectedTowncode, setSelectedTowncode] = useState("");
const [selectedZoneId, setSelectedZoneId] = useState("");
const isLibo = scope === "libo";
const isLibo = scope === "libo" || regionAdcode === LIBO_ADCODE;
const isFullscreen = fullscreenMode !== "none";
const activeRegionName = isLibo ? "荔波县" : regionName;
const mapTitle = isLibo ? "贵州省荔波县知识地图" : `${activeRegionName}知识地图`;
useEffect(() => {
try {
const raw = localStorage.getItem(recentSearchKey);
setRecentSearches(raw ? (JSON.parse(raw) as MapPoi[]) : []);
} catch {
setRecentSearches([]);
}
}, [recentSearchKey]);
const selectedTown = useMemo(
() => zoneTowns.find((town) => town.towncode === selectedTowncode) || null,
@@ -2108,12 +2109,28 @@ export function KnowledgeMap({
[townZones, selectedZoneId],
);
/** 按行政区(法定)→ 片区(业务)逐层收窄,两套口径互不推导。 */
const configuredRegionPois = useMemo(() => {
const expected = String(regionAdcode || "").trim();
if (!expected) return mapPois;
const prefixLength = regionLevel === "province" ? 2 : regionLevel === "city" ? 4 : 6;
return mapPois.filter((poi) => {
const adcode = String(poi.adcode || "").trim();
if (adcode) return adcode.slice(0, prefixLength) === expected.slice(0, prefixLength);
const regionField = regionLevel === "province"
? poi.province
: regionLevel === "city"
? poi.city
: poi.district;
const candidate = String(regionField || "").trim();
return Boolean(candidate && (candidate.includes(regionName) || regionName.includes(candidate)));
});
}, [mapPois, regionAdcode, regionLevel, regionName]);
const regionPois = useMemo(() => {
if (!selectedTowncode) return mapPois;
const inTown = mapPois.filter((poi) => poi.towncode === selectedTowncode);
if (!selectedTowncode) return configuredRegionPois;
const inTown = configuredRegionPois.filter((poi) => poi.towncode === selectedTowncode);
if (!selectedZoneId) return inTown;
return inTown.filter((poi) => poi.zone_id === selectedZoneId);
}, [mapPois, selectedTowncode, selectedZoneId]);
}, [configuredRegionPois, selectedTowncode, selectedZoneId]);
const poiCategories = useMemo(() => {
const counts = new Map<string, number>();
regionPois.forEach((poi) => {
@@ -2126,10 +2143,8 @@ export function KnowledgeMap({
.sort((a, b) => b.count - a.count || a.category.localeCompare(b.category, "zh-CN"));
}, [regionPois]);
const topCats = useMemo(
() => isLibo
? poiCategories.slice(0, 4).map((item) => ({ cat: item.category, current: item.count }))
: (cov?.items || []).slice(0, 4),
[cov, isLibo, poiCategories],
() => poiCategories.slice(0, 4).map((item) => ({ cat: item.category, current: item.count })),
[poiCategories],
);
const visiblePois = useMemo(
() => regionPois.filter((poi) => (
@@ -2282,11 +2297,6 @@ export function KnowledgeMap({
}, [fullscreenMode]);
useEffect(() => {
if (!isLibo) {
setMapPois([]);
setSelectedCategories([]);
return;
}
let disposed = false;
setPoiLoading(true);
setPoiError("");
@@ -2311,7 +2321,7 @@ export function KnowledgeMap({
return () => {
disposed = true;
};
}, [isLibo]);
}, [projectId, scope]);
useEffect(() => {
if (!isLibo) {
@@ -2479,22 +2489,22 @@ export function KnowledgeMap({
setRecentSearches((current) => {
const next = [poi, ...current.filter((item) => item.id !== poi.id)].slice(0, 6);
try {
localStorage.setItem(LIBO_RECENT_SEARCH_KEY, JSON.stringify(next));
localStorage.setItem(recentSearchKey, JSON.stringify(next));
} catch {
// localStorage 不可用时忽略,仅内存态。
}
return next;
});
}, [openPoi]);
}, [openPoi, recentSearchKey]);
const clearRecentSearches = useCallback(() => {
setRecentSearches([]);
try {
localStorage.removeItem(LIBO_RECENT_SEARCH_KEY);
localStorage.removeItem(recentSearchKey);
} catch {
// 忽略。
}
}, []);
}, [recentSearchKey]);
// 点击搜索框外部关闭下拉。
useEffect(() => {
@@ -2588,7 +2598,7 @@ export function KnowledgeMap({
const { data } = await plazaAmapConfig();
if (!data?.configured || !data?.js_key) {
setMapStatus("error");
setMapMessage(`高德 JS API Key 未配置,无法加载${isLibo ? "荔波县" : "贵州省"}地图。`);
setMapMessage(`高德 JS API Key 未配置,无法加载${activeRegionName}地图。`);
return;
}
const AMap = await loadAmapSdk(data.js_key, data.security_jscode);
@@ -2609,64 +2619,25 @@ export function KnowledgeMap({
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({ position: { right: "12px", top: "56px" } }));
const cityOverlays: any[] = [];
if (!isLibo) {
GUIZHOU_CITIES.forEach((city) => {
const active = city.active;
const countText = active && cov ? `${cov.total.toLocaleString()} 条` : "待接入";
const marker = new AMap.CircleMarker({
center: city.coord,
radius: active ? 12 : 7,
strokeColor: active ? "#1677ff" : "#7aa7c7",
strokeWeight: active ? 3 : 1,
strokeOpacity: active ? 0.9 : 0.55,
fillColor: active ? "#16a085" : "#d8ecfb",
fillOpacity: active ? 0.9 : 0.65,
zIndex: active ? 20 : 10,
});
const label = new AMap.Text({
text: `${city.name} · ${countText}`,
position: city.coord,
anchor: "bottom-center",
offset: new AMap.Pixel(0, -12),
style: {
padding: "4px 8px",
borderRadius: "999px",
border: active ? "1px solid #1677ff" : "1px solid #d6e4ff",
background: active ? "rgba(255,255,255,0.96)" : "rgba(255,255,255,0.82)",
color: active ? "#1f2329" : "#6b7a90",
fontSize: "12px",
boxShadow: active ? "0 6px 16px rgba(22,119,255,0.16)" : "none",
},
});
cityOverlays.push(marker, label);
});
map.add(cityOverlays);
}
AMap.plugin("AMap.DistrictSearch", () => {
if (disposed || !mapInstance.current) return;
const district = new AMap.DistrictSearch({
level: isLibo ? "district" : "province",
level: regionLevel,
subdistrict: 0,
extensions: "all",
});
district.search(isLibo ? "522722" : "贵州省", (status: string, result: any) => {
district.search(regionAdcode || activeRegionName, (status: string, result: any) => {
if (disposed) return;
if (status !== "complete") {
if (isLibo) {
setMapStatus("error");
setMapMessage("荔波县行政边界查询失败,请稍后刷新重试。");
}
setMapStatus("error");
setMapMessage(`${activeRegionName}行政边界查询失败,请稍后刷新重试。`);
return;
}
const districtItem = result?.districtList?.[0];
const boundaries = districtItem?.boundaries || [];
if (!boundaries.length) {
if (isLibo) {
setMapStatus("error");
setMapMessage("高德未返回荔波县行政边界数据。");
}
setMapStatus("error");
setMapMessage(`高德未返回${activeRegionName}行政边界数据。`);
return;
}
const polygons = boundaries.map((path: any) => new AMap.Polygon({
@@ -2683,7 +2654,7 @@ export function KnowledgeMap({
if (isLibo && districtItem?.center) {
map.setZoomAndCenter(9.5, districtItem.center);
} else {
map.setFitView([...polygons, ...cityOverlays], false, [36, 28, 36, 28]);
map.setFitView(polygons, false, [36, 28, 36, 28]);
}
if (isLibo && districtItem?.center) {
const center = districtItem.center;
@@ -2718,7 +2689,6 @@ export function KnowledgeMap({
});
});
if (!isLibo) setMapStatus("ready");
} catch (e: any) {
setMapStatus("error");
setMapMessage(e?.message || "高德地图加载失败");
@@ -2740,7 +2710,7 @@ export function KnowledgeMap({
mapInstance.current = null;
amapRef.current = null;
};
}, [cov, isLibo, mapPois.length]);
}, [activeRegionName, isLibo, mapPois.length, projectId, regionAdcode, regionLevel]);
// 荔波县实时天气:复用已加载的高德 JS SDK(AMap.Weather 插件),
// getLive 取实时天气现象+温度,getForecast 取今日最低/最高温,合成成一张天气卡。
@@ -2854,7 +2824,7 @@ export function KnowledgeMap({
useEffect(() => {
const AMap = amapRef.current;
const map = mapInstance.current;
if (!isLibo || mapStatus !== "ready" || !AMap || !map) return;
if (mapStatus !== "ready" || !AMap || !map) return;
let disposed = false;
const lookup = new Map<string, MapPoi>();
@@ -2958,7 +2928,7 @@ export function KnowledgeMap({
clusterRef.current?.setMap?.(null);
clusterRef.current = null;
};
}, [isLibo, mapStatus, mapRenderPois]);
}, [mapStatus, mapRenderPois]);
useEffect(() => {
const AMap = amapRef.current;
@@ -3546,13 +3516,13 @@ export function KnowledgeMap({
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, marginBottom: 10 }}>
<Space size={8}>
<EnvironmentOutlined style={{ color: "#1677ff" }} />
<b>{isLibo ? "贵州省荔波县知识地图" : "贵州省知识地图"}</b>
<b>{mapTitle}</b>
<Tag color="blue">高德 JS API</Tag>
</Space>
<span style={{ fontSize: 12, color: "#888" }}>
{isLibo
? "当前图谱:荔波县已接入 · 突出显示荔波县行政区域"
: "当前图谱:贵阳已接入 · 省域底图为后续扩展预留"}
: `当前图谱:${projectName} · 展示当前项目空间数据`}
</span>
</div>
@@ -3574,23 +3544,16 @@ export function KnowledgeMap({
}}
>
<div style={{ color: "#888", fontSize: 12, marginBottom: 4 }}>
{isLibo ? "荔波县馆藏汇总" : "省域馆藏汇总"}
{isLibo ? `${activeRegionName}馆藏汇总` : "省域馆藏汇总"}
</div>
<div style={{ fontSize: 28, fontWeight: 700, lineHeight: 1.15 }}>
{(isLibo ? regionPois.length : (cov?.total ?? 0)).toLocaleString()}
{(isLibo ? regionPois.length : (cov?.total ?? regionPois.length)).toLocaleString()}
</div>
<div style={{ color: "#888", fontSize: 12, marginTop: 4 }}>
{isLibo && selectedTowncode
? `${selectedZone?.zone_name || selectedTown?.name} / 共 ${mapPois.length.toLocaleString()} 条`
: "当前已入藏知识 / POI 条目"}
</div>
{!isLibo && <div style={{ marginTop: 14 }}>
<Progress
percent={cov?.grid_overall_pct ?? 0}
size="small"
status={(cov?.grid_overall_pct ?? 0) >= 100 ? "success" : "active"}
/>
</div>}
{isLibo && (
<div style={{ marginTop: 14 }}>
<Tag color="green">82 个细网格采集已完成</Tag>
@@ -3609,8 +3572,8 @@ export function KnowledgeMap({
</div>
<div style={{ marginTop: 14, color: "#888", fontSize: 12, lineHeight: 1.7 }}>
{isLibo
? "地图正常展示荔波县及周边区域,并用蓝色边界与浅蓝填充突出荔波县主体;所有 POI 均按荔波县行政区划代码 522722 过滤。"
: "地图先展示贵州省边界与九市州锚点;贵阳为当前知识图谱主库,后续扩展遵义、黔东南等城市时可直接挂接同一省域画布。"}
? "地图展示荔波县及周边区域,并用蓝色边界突出荔波县主体;POI 来自当前项目图谱。"
: "地图统一读取当前项目中包含经纬度的节点,并按真实业态分类、搜索和聚合展示。"}
</div>
</div>}
@@ -3631,8 +3594,7 @@ export function KnowledgeMap({
}}
>
<div ref={mapRef} style={{ width: "100%", height: isFullscreen ? "100%" : 320 }} />
{isLibo && (
<div
<div
style={{
position: "absolute",
left: 12,
@@ -3745,12 +3707,9 @@ export function KnowledgeMap({
)
) : (
<>
{/* 高德式快捷分类:无标题,一排大图标 + 文字,占满宽度。
固定顺序与占位文字一致:美食 → 酒店 → 景点 → 交通设施。 */}
{/* 用户确认的四个快捷入口及其图片属于固定模板,不随项目业态排序替换。 */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 2 }}>
{(["美食", "酒店", "景点", "交通设施"] as string[])
.filter((category) => poiCategories.some((item) => item.category === category))
.map((category) => {
{MAP_SHORTCUT_CATEGORIES.map((category) => {
const iconUrl = poiCategoryIcon(category);
const showImg = iconUrl && !catIconErr[category];
return (
@@ -3776,7 +3735,21 @@ export function KnowledgeMap({
style={{ display: "block", objectFit: "contain" }}
/>
) : (
<span style={{ fontSize: 40, lineHeight: 1, height: 46, display: "grid", placeItems: "center" }}>{poiCategoryEmoji(category)}</span>
<span
aria-hidden="true"
style={{
width: 46,
height: 46,
display: "grid",
placeItems: "center",
borderRadius: 12,
background: "#f0f5ff",
color: poiCategoryColor(category),
fontSize: 26,
}}
>
<EnvironmentOutlined />
</span>
)}
<span style={{ fontSize: 13, color: "#1f2329", whiteSpace: "nowrap" }}>{category}</span>
</div>
@@ -3859,7 +3832,7 @@ export function KnowledgeMap({
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginTop: 10 }}>
<Space size={6}>
<AimOutlined style={{ color: "#1677ff" }} />
<b style={{ fontSize: 13 }}>荔波POI图层</b>
<b style={{ fontSize: 13 }}>{activeRegionName} POI 图层</b>
<Tag color="blue" style={{ marginInlineEnd: 0 }}>
{mapRenderPois.length.toLocaleString()} / {regionPois.length.toLocaleString()}
</Tag>
@@ -3901,6 +3874,8 @@ export function KnowledgeMap({
<Divider style={{ margin: "10px 0 8px" }} />
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<Space size={6} wrap>
{isLibo && (
<>
<Tag.CheckableTag
checked={zonePickerOpen || Boolean(selectedTowncode)}
onChange={(checked) => setZonePickerOpen(checked)}
@@ -3934,6 +3909,8 @@ export function KnowledgeMap({
<CarOutlined /> 公交图层
</Tag.CheckableTag>
{busLoading && <Spin size="small" />}
</>
)}
{/* 底图图层选择:标准 / 卫星影像 + 实时路况叠加。
用 span 作触发器(CheckableTag 会吞掉点击导致菜单打不开)。 */}
<Dropdown
@@ -3976,20 +3953,22 @@ export function KnowledgeMap({
</span>
</Dropdown>
</Space>
<Button
size="small"
icon={<UnorderedListOutlined />}
disabled={!busRoutes.length}
onClick={() => {
closePoiDetail();
setFocusedBusRouteId("");
setBusPanel({ mode: "list" });
}}
>
线路列表
</Button>
{isLibo && (
<Button
size="small"
icon={<UnorderedListOutlined />}
disabled={!busRoutes.length}
onClick={() => {
closePoiDetail();
setFocusedBusRouteId("");
setBusPanel({ mode: "list" });
}}
>
线路列表
</Button>
)}
</div>
{zonePickerOpen && (
{isLibo && zonePickerOpen && (
<div
style={{
marginTop: 8,
@@ -4080,7 +4059,7 @@ export function KnowledgeMap({
)}
</div>
)}
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }}>
{isLibo && <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }}>
{busLines.map((lineName) => {
const color = busLineColor(lineName);
const checked = focusedBusLineName === lineName;
@@ -4105,14 +4084,13 @@ export function KnowledgeMap({
</Tooltip>
);
})}
</div>
{busError && (
</div>}
{isLibo && busError && (
<div style={{ color: "#cf1322", fontSize: 12, marginTop: 6 }}>{busError}</div>
)}
</>
)}
</div>
)}
{isLibo && weather && (
/* 高德式天气卡:紧贴搜索卡右侧;点击展开未来多日预报(数据来自高德 AMap.Weather)。 */
<div
@@ -5021,7 +4999,7 @@ export default function SuperAgentPanel() {
</div>
)}
<KnowledgeMap cov={cov} />
<KnowledgeMap />
{escalatedCats.length > 0 && (
<Alert
+43
View File
@@ -0,0 +1,43 @@
import type { SpatialMapConfig, SpatialMapRegionLevel, SpatialMapScope } from "./api";
export type SpatialMapRegionOption = {
adcode: string;
label: string;
level: SpatialMapRegionLevel;
name: string;
scope: SpatialMapScope;
};
// 当前平台的生产数据范围。新增城市只需要在这里补一项,项目、地图边界
// 和导入校验都会共同读取同一份行政区配置。
export const SPATIAL_MAP_REGIONS: SpatialMapRegionOption[] = [
{ adcode: "520000", name: "贵州省", label: "贵州省", level: "province", scope: "guizhou" },
{ adcode: "520100", name: "贵阳市", label: "贵阳市", level: "city", scope: "guizhou" },
{ adcode: "520200", name: "六盘水市", label: "六盘水市", level: "city", scope: "guizhou" },
{ adcode: "520300", name: "遵义市", label: "遵义市", level: "city", scope: "guizhou" },
{ adcode: "520400", name: "安顺市", label: "安顺市", level: "city", scope: "guizhou" },
{ adcode: "520500", name: "毕节市", label: "毕节市", level: "city", scope: "guizhou" },
{ adcode: "520600", name: "铜仁市", label: "铜仁市", level: "city", scope: "guizhou" },
{ adcode: "522300", name: "黔西南布依族苗族自治州", label: "黔西南州", level: "city", scope: "guizhou" },
{ adcode: "522600", name: "黔东南苗族侗族自治州", label: "黔东南州", level: "city", scope: "guizhou" },
{ adcode: "522700", name: "黔南布依族苗族自治州", label: "黔南州", level: "city", scope: "guizhou" },
{ adcode: "522722", name: "荔波县", label: "荔波县", level: "district", scope: "libo" },
];
export const spatialMapRegionByAdcode = (adcode?: string | null) => (
SPATIAL_MAP_REGIONS.find((item) => item.adcode === String(adcode || "").trim()) || null
);
export const spatialMapConfigForRegion = (
enabled: boolean,
adcode?: string | null,
): SpatialMapConfig => {
const region = enabled ? spatialMapRegionByAdcode(adcode) : null;
return {
enabled,
scope: region?.scope || "guizhou",
region_name: region?.name || "",
region_adcode: region?.adcode || "",
region_level: region?.level || "province",
};
};
+11 -4
View File
@@ -2529,10 +2529,6 @@ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ro
}
.project-map-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin: 2px 0 18px;
padding: 16px 18px;
border: 1px solid #d9e6f8;
@@ -2540,6 +2536,17 @@ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ro
background: #f7faff;
}
.project-map-option-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.project-map-region.ant-form-item {
margin: 14px 0 0;
}
.project-json-step {
display: flex;
flex-direction: column;
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const overviewSource = readFileSync(
new URL("../src/panels/plaza/PlazaOverview.tsx", import.meta.url),
"utf8",
);
const mapSource = readFileSync(
new URL("../src/panels/plaza/SuperAgentPanel.tsx", import.meta.url),
"utf8",
);
const projectLandingSource = readFileSync(
new URL("../src/ProjectLanding.tsx", import.meta.url),
"utf8",
);
const regionsSource = readFileSync(
new URL("../src/spatialMapRegions.ts", import.meta.url),
"utf8",
);
test("every map-enabled project uses the single KnowledgeMap template", () => {
assert.match(overviewSource, /isKnowledgeMapProject && \(/);
assert.match(overviewSource, /<KnowledgeMap/);
assert.doesNotMatch(overviewSource, /<SuperAgentPanel/);
});
test("the shared map preserves the four approved shortcut logos and province summary", () => {
assert.match(mapSource, /MAP_SHORTCUT_CATEGORIES = \["美食", "酒店", "景点", "交通设施"\]/);
assert.match(mapSource, /MAP_SHORTCUT_CATEGORIES\.map/);
assert.match(mapSource, /省域馆藏汇总/);
assert.match(mapSource, /cov\?\.total \?\? regionPois\.length/);
assert.match(mapSource, /当前已入藏知识 \/ POI 条目/);
});
test("map-enabled project creation requires an explicit administrative region", () => {
assert.match(projectLandingSource, /name="map_region_adcode"/);
assert.match(projectLandingSource, /请选择地图区域/);
assert.match(projectLandingSource, /spatialMapConfigForRegion/);
assert.match(regionsSource, /adcode: "520100", name: "贵阳市"/);
assert.match(regionsSource, /adcode: "522722", name: "荔波县"/);
});