From ad2b1aa4b6dc5199f9977a217d5aaf794b528abb Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 12 Mar 2026 02:11:48 +0800 Subject: [PATCH] feat: Implement CitySidebar component with risk-level grouping and internationalization utilities. --- frontend/components/dashboard/CitySidebar.tsx | 166 ++++++++++++++---- .../components/dashboard/Dashboard.module.css | 96 ++++++++-- frontend/lib/i18n.ts | 8 + 3 files changed, 212 insertions(+), 58 deletions(-) diff --git a/frontend/components/dashboard/CitySidebar.tsx b/frontend/components/dashboard/CitySidebar.tsx index 4b5c440f..df198a28 100644 --- a/frontend/components/dashboard/CitySidebar.tsx +++ b/frontend/components/dashboard/CitySidebar.tsx @@ -1,20 +1,73 @@ "use client"; +import { useEffect, useMemo, useState } from "react"; import clsx from "clsx"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; +import { CityListItem } from "@/lib/dashboard-types"; + +type RiskGroupKey = "high" | "medium" | "low" | "other"; + +function toRiskGroup(level?: string): RiskGroupKey { + if (level === "high" || level === "medium" || level === "low") return level; + return "other"; +} export function CitySidebar() { const store = useDashboardStore(); const { t } = useI18n(); - const sortedCities = [...store.cities].sort((a, b) => { - const order = { high: 0, medium: 1, low: 2 }; - return ( - (order[a.risk_level as keyof typeof order] ?? 3) - - (order[b.risk_level as keyof typeof order] ?? 3) - ); + const selectedCity = store.selectedCity; + const riskOrder = { high: 0, medium: 1, low: 2, other: 3 }; + const [expandedGroups, setExpandedGroups] = useState>({ + high: true, + medium: true, + low: false, + other: false, }); + const sortedCities = useMemo(() => [...store.cities].sort((a, b) => { + const aSelected = a.name === selectedCity; + const bSelected = b.name === selectedCity; + if (aSelected !== bSelected) return aSelected ? -1 : 1; + const aGroup = toRiskGroup(a.risk_level); + const bGroup = toRiskGroup(b.risk_level); + return ( + (riskOrder[aGroup] ?? 3) - + (riskOrder[bGroup] ?? 3) || + a.display_name.localeCompare(b.display_name) + ); + }), [store.cities, selectedCity]); + + const groupedCities = useMemo(() => { + const groups: Record = { + high: [], + medium: [], + low: [], + other: [], + }; + sortedCities.forEach((city) => { + groups[toRiskGroup(city.risk_level)].push(city); + }); + return groups; + }, [sortedCities]); + + useEffect(() => { + if (!selectedCity) return; + const selected = store.cities.find((city) => city.name === selectedCity); + if (!selected) return; + const groupKey = toRiskGroup(selected.risk_level); + setExpandedGroups((current) => + current[groupKey] ? current : { ...current, [groupKey]: true }, + ); + }, [selectedCity, store.cities]); + + const groupMeta: Array<{ key: RiskGroupKey; label: string }> = [ + { key: "high", label: t("sidebar.group.high") }, + { key: "medium", label: t("sidebar.group.medium") }, + { key: "low", label: t("sidebar.group.low") }, + { key: "other", label: t("sidebar.group.other") }, + ]; + return (