feat: Phase 3 — 视角/perspective presets + URL-driven granularity + doctor privacy
Phase 3 of the UX modernization. Frontend-only role view-presets (D2 — no backend, no JWT role claim, honestly labeled 视角 not 权限). Three conflict-free lanes. Role infrastructure (worker-session): - sessionStore with single swappable getRoleSource() seam (localStorage today, one-line swap to /api/auth/me for future RBAC); Role = official|community|doctor|admin - 视角 switcher in TopNav (replaces hardcoded "admin"); on change persists role + navigates to that perspective's default landing - roleViews.ts: ROLE_LABELS + roleDefaultPath (official→/overview?granularity=district, community→/monitoring?granularity=street, doctor→/alerts?view=cluster, admin→/monitoring) - RoleRedirect index route → current role's default; * fallback unchanged URL-driven granularity (worker-monitoring): - granularity (city|district|street) query param is the source of truth; drilldownStore DERIVES from it via a one-way effect; deep-linkable + reload-safe - reconciled the imperative desync — DistrictBreakdown no longer calls useDrilldownStore.getState(); MonitoringDashboard owns useSearchParams, writes URL - granularity-control Segmented (全市/区域/街道); district-rollup testid Role-aware alerts + privacy (worker-alerts): - doctor/cluster view: individual alert markers hard-locked off (effective flag is single source of truth; toggle not rendered) → aggregated density only; DiseaseFilter mounted; privacy invariant testable via hidden patient-point DOM mirror (count=0) - 官员: 100m grid hidden (grid-layer-wrapper unrendered); admin/community unchanged Gates: tsc 0 · vitest 75 · e2e 30/30 (incl 10 new P3 tests) · build ok Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TESTIDS } from '@/utils/testids';
|
||||
import { useSessionStore, ROLES, type Role } from '@/stores/sessionStore';
|
||||
import { ROLE_LABELS, roleDefaultPath } from '@/utils/roleViews';
|
||||
|
||||
interface TopNavProps {
|
||||
onLogout?: () => void;
|
||||
@@ -18,6 +21,38 @@ function Clock() {
|
||||
return <span>{time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</span>;
|
||||
}
|
||||
|
||||
// 视角切换器:纯前端视图预设(D2)。刻意标注「视角」而非「权限」——不是访问控制。
|
||||
// 切换时持久化角色并跳转到该视角的默认落地页。
|
||||
function PerspectiveSwitcher() {
|
||||
const role = useSessionStore((s) => s.role);
|
||||
const setRole = useSessionStore((s) => s.setRole);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (next: Role) => {
|
||||
setRole(next);
|
||||
navigate(roleDefaultPath(next));
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-1.5 text-[13px] text-text-secondary">
|
||||
<span className="text-text-muted hidden sm:inline">视角</span>
|
||||
<select
|
||||
data-testid={TESTIDS.perspectiveSwitcher}
|
||||
value={role}
|
||||
onChange={(e) => handleChange(e.target.value as Role)}
|
||||
aria-label="切换视角"
|
||||
className="bg-bg-card border border-border rounded-md px-2 py-1 text-[13px] text-text-primary hover:bg-bg-hover focus:outline-none focus:ring-1 focus:ring-primary cursor-pointer"
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r} data-testid={`${TESTIDS.perspectiveOption}-${r}`}>
|
||||
{ROLE_LABELS[r]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopNav({ onLogout, onToggleMenu, isMenuOpen = false }: TopNavProps) {
|
||||
return (
|
||||
<nav className="h-[52px] bg-bg-card border-b border-border flex items-center px-5 z-50">
|
||||
@@ -62,7 +97,7 @@ export function TopNav({ onLogout, onToggleMenu, isMenuOpen = false }: TopNavPro
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||
</svg>
|
||||
admin
|
||||
<PerspectiveSwitcher />
|
||||
</div>
|
||||
{onLogout && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user