feat(client): vendor digital employee source

This commit is contained in:
baiyanyun
2026-06-05 15:02:42 +08:00
parent c6fdff20e8
commit 2eb9a99da3
66 changed files with 15160 additions and 2646 deletions

View File

@@ -0,0 +1,158 @@
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
export type ConsoleRole = 'finance' | 'engineering' | 'executive' | 'hr' | 'sales';
export type MotionLevel = 'none' | 'minimal';
export type Density = 'compact' | 'balanced' | 'presentation';
export type Tone = 'strict' | 'technical' | 'executive' | 'warm-professional';
export interface RoleProfile {
id: ConsoleRole;
labelKey: string;
density: Density;
tone: Tone;
contentPriority: string[];
motion: {
defaultLevel: MotionLevel;
allowParticles: false;
allowCelebrations: false;
allowDecorativeBadges: false;
};
actionPolicy: {
requireConfirmationForDestructive: boolean;
showReversalHint: boolean;
preferOneClickApproval: boolean;
};
staleDataThresholdMs: number;
cssVars: Record<string, string>;
}
export const ROLE_PROFILES: Record<ConsoleRole, RoleProfile> = {
finance: {
id: 'finance',
labelKey: 'console.role.finance',
density: 'compact',
tone: 'strict',
contentPriority: ['audit', 'sources', 'exactNumbers'],
motion: { defaultLevel: 'none', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 30000,
cssVars: {
'--role-accent': '#2563eb',
'--role-accent-soft': 'rgba(37,99,235,0.12)',
'--role-surface-tint': 'rgba(248,250,252,0.78)',
'--role-border': 'rgba(37,99,235,0.22)',
'--role-focus-ring': '#1d4ed8',
},
},
engineering: {
id: 'engineering',
labelKey: 'console.role.engineering',
density: 'compact',
tone: 'technical',
contentPriority: ['alarms', 'timestamps', 'reliability'],
motion: { defaultLevel: 'none', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 15000,
cssVars: {
'--role-accent': '#0f766e',
'--role-accent-soft': 'rgba(15,118,110,0.12)',
'--role-surface-tint': 'rgba(240,253,250,0.72)',
'--role-border': 'rgba(15,118,110,0.24)',
'--role-focus-ring': '#0f766e',
},
},
executive: {
id: 'executive',
labelKey: 'console.role.executive',
density: 'presentation',
tone: 'executive',
contentPriority: ['kpi', 'approvals', 'exactNumbers'],
motion: { defaultLevel: 'minimal', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: true },
staleDataThresholdMs: 60000,
cssVars: {
'--role-accent': '#334155',
'--role-accent-soft': 'rgba(51,65,85,0.10)',
'--role-surface-tint': 'rgba(248,250,252,0.82)',
'--role-border': 'rgba(51,65,85,0.20)',
'--role-focus-ring': '#475569',
},
},
hr: {
id: 'hr',
labelKey: 'console.role.hr',
density: 'balanced',
tone: 'warm-professional',
contentPriority: ['queues', 'timestamps', 'approvals'],
motion: { defaultLevel: 'minimal', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 45000,
cssVars: {
'--role-accent': '#7c3aed',
'--role-accent-soft': 'rgba(124,58,237,0.10)',
'--role-surface-tint': 'rgba(250,245,255,0.72)',
'--role-border': 'rgba(124,58,237,0.20)',
'--role-focus-ring': '#6d28d9',
},
},
sales: {
id: 'sales',
labelKey: 'console.role.sales',
density: 'balanced',
tone: 'warm-professional',
contentPriority: ['pipeline', 'sources', 'exactNumbers'],
motion: { defaultLevel: 'minimal', allowParticles: false, allowCelebrations: false, allowDecorativeBadges: false },
actionPolicy: { requireConfirmationForDestructive: true, showReversalHint: true, preferOneClickApproval: false },
staleDataThresholdMs: 45000,
cssVars: {
'--role-accent': '#b45309',
'--role-accent-soft': 'rgba(180,83,9,0.10)',
'--role-surface-tint': 'rgba(255,251,235,0.70)',
'--role-border': 'rgba(180,83,9,0.20)',
'--role-focus-ring': '#92400e',
},
},
};
interface RoleContextValue {
role: ConsoleRole;
profile: RoleProfile;
setRole: (role: ConsoleRole) => void;
}
const RoleContext = createContext<RoleContextValue | null>(null);
const STORAGE_KEY = 'sgrobot-console-role';
function getStoredRole(): ConsoleRole {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && stored in ROLE_PROFILES) return stored as ConsoleRole;
} catch { /* localStorage blocked */ }
return 'sales';
}
export function RoleProvider({ children }: { children: ReactNode }) {
const [role, setRoleState] = useState<ConsoleRole>(getStoredRole);
const setRole = useCallback((newRole: ConsoleRole) => {
setRoleState(newRole);
try { localStorage.setItem(STORAGE_KEY, newRole); } catch { /* noop */ }
}, []);
const profile = ROLE_PROFILES[role];
return (
<RoleContext.Provider value={{ role, profile, setRole }}>
<div style={profile.cssVars as React.CSSProperties} data-role={role}>
{children}
</div>
</RoleContext.Provider>
);
}
export function useRole(): RoleContextValue {
const ctx = useContext(RoleContext);
if (!ctx) throw new Error('useRole must be used within RoleProvider');
return ctx;
}