fix: bind knowledge maps to explicit regions
This commit is contained in:
1 parent
329a94d457
commit
90f75d41ae
15 files changed
+1126
-275
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
};
|
||||
@@ -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: "荔波县"/);
|
||||
});
|
||||
Reference in new issue
Block a user